"""Metadata-only high-water contracts for the G7 voice runtime.""" from __future__ import annotations import asyncio import unittest from pathlib import Path from unittest.mock import AsyncMock, patch from .routes import admin as admin_routes from .routes import voice as voice_routes from .services import voice as voice_service_module from .services.voice import DeepgramStreamingSession from .services.voice_runtime import ( VOICE_AUDIO_BUFFER_MAX_BYTES, VOICE_STREAMING_EVENT_QUEUE_MAX_ITEMS, VOICE_UVICORN_WS_MAX_QUEUE, VoiceRuntimeMetrics, ) from .test_voice_ws import ( SESSION_ID, VOICE_PRESET, FakeWebSocket, _binary, _control, _principal, ) class VoiceRuntimeMetricsTests(unittest.TestCase): def test_connection_buffer_and_queue_high_water_survive_cleanup(self) -> None: metrics = VoiceRuntimeMetrics() first = metrics.websocket_opened() second = metrics.websocket_opened() metrics.audio_chunk_received( first, current_buffer_bytes=640, chunk_bytes=640, ) metrics.audio_chunk_received( second, current_buffer_bytes=384, chunk_bytes=384, ) metrics.streaming_queue_observed(first, queue_items=7) metrics.streaming_queue_observed( second, queue_items=VOICE_STREAMING_EVENT_QUEUE_MAX_ITEMS, saturated=True, wait_seconds=0.125, ) metrics.audio_overflow_rejected(second) during = metrics.snapshot() self.assertEqual(2, during.counters.active_websockets) self.assertEqual(2, during.counters.websocket_high_water) self.assertEqual(640, during.counters.route_audio_buffer_bytes) self.assertEqual(1024, during.counters.route_audio_buffer_high_water_bytes) self.assertEqual(1024, during.counters.audio_bytes_received_total) self.assertEqual(1, during.counters.audio_overflow_rejections_total) self.assertEqual( 7 + VOICE_STREAMING_EVENT_QUEUE_MAX_ITEMS, during.counters.streaming_event_queue_high_water_items, ) self.assertEqual(1, during.counters.streaming_event_queue_saturation_total) self.assertEqual( 0.125, during.counters.streaming_event_queue_wait_seconds_total, ) metrics.audio_buffer_cleared(first) metrics.streaming_queue_observed(first, queue_items=0) metrics.streaming_queue_observed(second, queue_items=0) metrics.websocket_closed(first) metrics.websocket_closed(second) after = metrics.snapshot() self.assertEqual(0, after.counters.active_websockets) self.assertEqual(0, after.counters.route_audio_buffer_bytes) self.assertEqual(0, after.counters.streaming_event_queue_items) self.assertEqual(2, after.counters.websocket_high_water) self.assertFalse(after.reset_supported) def test_streaming_provider_lifecycle_is_bounded_and_counted(self) -> None: metrics = VoiceRuntimeMetrics() metrics.streaming_provider_opened() metrics.streaming_provider_opened() metrics.streaming_provider_closed(outcome="finalized") metrics.streaming_provider_closed(outcome="aborted") metrics.provider_fallback() metrics.websocket_error() snapshot = metrics.snapshot() self.assertEqual(0, snapshot.counters.active_streaming_provider_sessions) self.assertEqual(2, snapshot.counters.streaming_provider_session_high_water) self.assertEqual(2, snapshot.counters.streaming_provider_sessions_opened_total) self.assertEqual(1, snapshot.counters.provider_finalize_total) self.assertEqual(1, snapshot.counters.provider_abort_total) self.assertEqual(1, snapshot.counters.provider_fallback_total) self.assertEqual(1, snapshot.counters.websocket_error_total) self.assertEqual("single_api_worker", snapshot.scope) self.assertEqual( "metadata_only_no_audio_transcript_or_session_ids", snapshot.privacy_boundary, ) def test_runtime_limits_match_the_production_route_and_docker_cmd(self) -> None: snapshot = VoiceRuntimeMetrics().snapshot() self.assertEqual( VOICE_AUDIO_BUFFER_MAX_BYTES, snapshot.limits.max_utterance_audio_bytes, ) self.assertEqual( VOICE_STREAMING_EVENT_QUEUE_MAX_ITEMS, snapshot.limits.streaming_event_queue_max_items, ) self.assertEqual( VOICE_UVICORN_WS_MAX_QUEUE, snapshot.limits.uvicorn_ws_max_queue, ) dockerfile = (Path(__file__).resolve().parents[1] / "Dockerfile").read_text( encoding="utf-8" ) self.assertIn( '"--ws", "websockets", "--ws-max-queue", "4"', dockerfile, ) def test_admin_snapshot_is_structured_and_metadata_only(self) -> None: response = asyncio.run(admin_routes.admin_voice_runtime(principal=None)) # type: ignore[arg-type] payload = response.model_dump(mode="json") self.assertEqual("vignette.voice-runtime.v1", payload["schema_version"]) keys: set[str] = set() def collect_keys(value: object) -> None: if isinstance(value, dict): keys.update(str(key) for key in value) for child in value.values(): collect_keys(child) elif isinstance(value, list): for child in value: collect_keys(child) collect_keys(payload) for forbidden in ( "session_id", "transcript_text", "raw_audio", "provider_payload", ): self.assertNotIn(forbidden, keys) class _StreamingSocket: def __init__(self) -> None: self._frames = iter( [ '{"type":"Results","is_final":true,"speech_final":true,' '"channel":{"alternatives":[{"transcript":"ok","words":[]}]}}' ] ) self.sent: list[object] = [] self.close = AsyncMock() def __aiter__(self): return self async def __anext__(self) -> str: try: return next(self._frames) except StopIteration as exc: raise StopAsyncIteration from exc async def send(self, payload: object) -> None: self.sent.append(payload) class DeepgramRuntimeLifecycleTests(unittest.IsolatedAsyncioTestCase): async def test_finalize_and_abort_release_global_active_session(self) -> None: baseline = voice_service_module.voice_runtime_metrics.snapshot().counters finalized = DeepgramStreamingSession( _StreamingSocket(), model="nova-3", language="ko", on_event=AsyncMock(), keepalive_seconds=60, finalize_timeout_seconds=1, ) await finalized.finish() after_finalize = voice_service_module.voice_runtime_metrics.snapshot().counters self.assertEqual( baseline.active_streaming_provider_sessions, after_finalize.active_streaming_provider_sessions, ) self.assertEqual( baseline.provider_finalize_total + 1, after_finalize.provider_finalize_total, ) aborted = DeepgramStreamingSession( _StreamingSocket(), model="nova-3", language="ko", on_event=AsyncMock(), keepalive_seconds=60, finalize_timeout_seconds=1, ) await aborted.abort() after_abort = voice_service_module.voice_runtime_metrics.snapshot().counters self.assertEqual( baseline.active_streaming_provider_sessions, after_abort.active_streaming_provider_sessions, ) self.assertEqual( after_finalize.provider_abort_total + 1, after_abort.provider_abort_total, ) class VoiceRouteRuntimeIntegrationTests(unittest.IsolatedAsyncioTestCase): async def test_route_records_audio_high_water_and_cleans_connection(self) -> None: metrics = VoiceRuntimeMetrics() websocket = FakeWebSocket( [ _control({"type": "audio_start", "format": "webm"}), _binary(b"first"), _binary(b"second"), _control({"type": "audio_end", "format": "webm"}), _control({"type": "close"}), ] ) with ( patch.object(voice_routes, "voice_runtime_metrics", metrics), patch.object( voice_routes, "_principal_from_websocket", AsyncMock(return_value=_principal()), ), patch.object( voice_routes, "_bind_session", AsyncMock( return_value=(SESSION_ID, VOICE_PRESET, None, {}) ), ), patch.object( voice_routes.voice_service, "is_available", return_value=True, ), patch.object( voice_routes.voice_service, "can_stream_audio", return_value=False, ), patch.object(voice_routes, "_handle_utterance", AsyncMock()), ): await voice_routes.voice_ws(websocket) # type: ignore[arg-type] snapshot = metrics.snapshot().counters self.assertEqual(1, snapshot.websockets_opened_total) self.assertEqual(1, snapshot.websocket_high_water) self.assertEqual(0, snapshot.active_websockets) self.assertEqual(11, snapshot.audio_bytes_received_total) self.assertEqual(11, snapshot.route_audio_buffer_high_water_bytes) self.assertEqual(0, snapshot.route_audio_buffer_bytes) if __name__ == "__main__": unittest.main()