from __future__ import annotations import asyncio import struct from fastapi import WebSocketDisconnect from app.utils.background import BridgeOutputStats, ws_out_loop class _ClosingWebSocket: def __init__(self, exc: BaseException) -> None: self.exc = exc self.sent_frames = 0 async def send_bytes(self, frame: bytes) -> None: self.sent_frames += 1 raise self.exc class _CollectThenDisconnectWebSocket: def __init__(self, disconnect_after: int) -> None: self.disconnect_after = disconnect_after self.sent_frames: list[bytes] = [] async def send_bytes(self, frame: bytes) -> None: self.sent_frames.append(frame) if len(self.sent_frames) >= self.disconnect_after: raise WebSocketDisconnect(code=1000) class _CaptureThenDisconnectWebSocket: def __init__(self) -> None: self.sent_frames: list[bytes] = [] async def send_bytes(self, frame: bytes) -> None: self.sent_frames.append(frame) raise WebSocketDisconnect(code=1000) async def _capture_one_agent_frame(frame: bytes, **ws_out_kwargs) -> _CaptureThenDisconnectWebSocket: ws = _CaptureThenDisconnectWebSocket() agent_q: asyncio.Queue[bytes] = asyncio.Queue() await agent_q.put(frame) await asyncio.wait_for( ws_out_loop( ws, agent_q, frame_ms=20, bytes_per_frame=len(frame), **ws_out_kwargs, ), timeout=0.2, ) return ws def test_ws_out_loop_exits_when_close_was_already_sent() -> None: async def _run() -> None: ws = _ClosingWebSocket(RuntimeError('Cannot call "send" once a close message has been sent.')) await asyncio.wait_for( ws_out_loop( ws, asyncio.Queue(), frame_ms=20, bytes_per_frame=4, ), timeout=0.2, ) assert ws.sent_frames == 1 asyncio.run(_run()) def test_ws_out_loop_applies_default_output_gain(monkeypatch) -> None: monkeypatch.delenv("WS_OUTPUT_GAIN", raising=False) frame = struct.pack(" None: monkeypatch.setenv("WS_OUTPUT_GAIN", "1.5") frame = struct.pack(" None: monkeypatch.setenv("WS_OUTPUT_GAIN", "1.5") frame = struct.pack(" None: async def _run() -> None: ws = _ClosingWebSocket(WebSocketDisconnect(code=1000)) await asyncio.wait_for( ws_out_loop( ws, asyncio.Queue(), frame_ms=20, bytes_per_frame=4, ), timeout=0.2, ) assert ws.sent_frames == 1 asyncio.run(_run()) def test_ws_out_loop_waits_for_speech_before_signaling_first_agent_audio() -> None: async def _run() -> None: silent_frame = struct.pack("