"""Composition selects VAD input and routes application events to the channel.""" import json from pathlib import Path from unittest.mock import AsyncMock import pytest from voice_assistant.agent.mock import MockAgentAdapter from voice_assistant.application.events import TurnOpened, TurnInterrupted from voice_assistant.audio_gateway.app import main from voice_assistant.config.loader import load_config from voice_assistant.tts.mock import MockTTSAdapter from voice_assistant.vad.adapter import VADAdapter @pytest.fixture def anyio_backend(): return 'asyncio' class VAD(VADAdapter): async def process(self, audio): return () async def flush(self): return () async def reset(self): pass async def aclose(self): pass @pytest.mark.anyio @pytest.mark.parametrize('server_vad', [False, True]) async def test_composition_selects_vad_and_accepts_environment_policy(monkeypatch, server_vad): config = load_config('development', config_dir=Path(__file__).resolve().parents[1] / 'config') assert config.voice_input == { 'pre_roll_ms': 200, 'history_ms': 2000, 'max_input_chunk_bytes': 65536, } monkeypatch.setattr(main, 'app_config', config) monkeypatch.setattr(main, 'create_agent_adapter', lambda **kwargs: MockAgentAdapter()) monkeypatch.setattr(main, 'create_tts_adapter', lambda **kwargs: MockTTSAdapter()) detector = VAD() calls = [] def factory(**kwargs): calls.append(kwargs) return detector monkeypatch.setattr(main, 'create_vad_adapter', factory) session = await main.create_voice_session(AsyncMock(), None, server_vad=server_vad) try: if server_vad: assert len(calls) == 1 assert calls[0]['config'] is config await session.receive_audio(bytes(512)) else: assert calls == [] with pytest.raises(Exception, match='requires VAD'): await session.receive_audio(bytes(512)) finally: await session.aclose() @pytest.mark.anyio async def test_bad_policy_releases_all_created_providers(monkeypatch): config = main.app_config.model_copy(update={'voice_input': {'history_ms': 0}}) agent, tts, vad = MockAgentAdapter(), MockTTSAdapter(), VAD() for provider in [agent, tts, vad]: provider.aclose = AsyncMock() monkeypatch.setattr(main, 'app_config', config) monkeypatch.setattr(main, 'create_agent_adapter', lambda **kwargs: agent) monkeypatch.setattr(main, 'create_tts_adapter', lambda **kwargs: tts) monkeypatch.setattr(main, 'create_vad_adapter', lambda **kwargs: vad) with pytest.raises(ValueError, match='history_ms'): await main.create_voice_session(AsyncMock(), None, server_vad=True) for provider in [agent, tts, vad]: provider.aclose.assert_awaited_once() @pytest.mark.anyio async def test_automatic_turn_events_map_to_existing_protocol(): socket = AsyncMock() await main.send_voice_event(socket, TurnOpened('session', 2)) await main.send_voice_event(socket, TurnInterrupted('session', 1)) events = [json.loads(call.args[0]) for call in socket.send_text.await_args_list] assert events == [ {'type': 'turn.started', 'session_id': 'session', 'turn_id': 2}, {'type': 'interrupted', 'session_id': 'session', 'turn_id': 1}, ]