Files
voiceagent/tests/test_vad_session_composition.py
2026-11-03 17:44:30 -03:00

94 lines
3.3 KiB
Python

"""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},
]