94 lines
3.3 KiB
Python
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},
|
|
]
|