201 lines
4.4 KiB
Python
201 lines
4.4 KiB
Python
"""
|
|
Tests for the Voice Assistant WebSocket protocol.
|
|
|
|
Purpose:
|
|
Verify that protocol message models enforce their declared
|
|
event types and canonical audio contract.
|
|
|
|
Responsibilities:
|
|
- Validate inbound protocol event discriminators.
|
|
- Validate SessionStart audio configuration.
|
|
- Reject messages whose type does not match their model.
|
|
|
|
Non-responsibilities:
|
|
- Testing WebSocket transport behavior.
|
|
- Testing session or turn lifecycle.
|
|
- Testing STT, Agent, or TTS providers.
|
|
"""
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
from voice_assistant.audio.models import CANONICAL_AUDIO_FORMAT
|
|
from voice_assistant.audio_gateway.app.protocol import (
|
|
AgentDelta,
|
|
AgentResponse,
|
|
AudioChunk,
|
|
AudioStart,
|
|
AudioStop,
|
|
ErrorEvent,
|
|
Interrupt,
|
|
Interrupted,
|
|
SessionEnd,
|
|
SessionEnded,
|
|
SessionReady,
|
|
SessionStart,
|
|
STTFinal,
|
|
STTPartial,
|
|
TurnEnded,
|
|
TurnStarted,
|
|
)
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model", "expected_type"),
|
|
[
|
|
(SessionStart, "session.start"),
|
|
(AudioStart, "audio.start"),
|
|
(AudioChunk, "audio.chunk"),
|
|
(AudioStop, "audio.stop"),
|
|
(Interrupt, "interrupt"),
|
|
(SessionEnd, "session.end"),
|
|
],
|
|
)
|
|
def test_protocol_event_has_expected_type(
|
|
model,
|
|
expected_type,
|
|
):
|
|
payload = {
|
|
"type": expected_type,
|
|
}
|
|
|
|
if model in (AudioChunk, AudioStop, Interrupt):
|
|
payload["turn_id"] = 1
|
|
|
|
if model is AudioChunk:
|
|
payload["sequence_number"] = 0
|
|
|
|
event = model.model_validate(payload)
|
|
|
|
assert event.type == expected_type
|
|
|
|
|
|
def test_protocol_rejects_wrong_event_type():
|
|
with pytest.raises(ValidationError):
|
|
AudioStart.model_validate(
|
|
{
|
|
"type": "audio.stop",
|
|
}
|
|
)
|
|
|
|
def test_session_ready_output():
|
|
event = SessionReady(
|
|
protocol_version="1.0",
|
|
session_id="session-1",
|
|
audio=CANONICAL_AUDIO_FORMAT,
|
|
)
|
|
|
|
assert event.model_dump() == {
|
|
"type": "session.ready",
|
|
"protocol_version": "1.0",
|
|
"session_id": "session-1",
|
|
"audio": {
|
|
"encoding": "pcm_s16le",
|
|
"sample_rate": 16000,
|
|
"channels": 1,
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model", "event_type"),
|
|
[
|
|
(TurnStarted, "turn.started"),
|
|
(STTPartial, "stt.partial"),
|
|
(STTFinal, "stt.final"),
|
|
(AgentDelta, "agent.delta"),
|
|
(AgentResponse, "agent.response"),
|
|
(TurnEnded, "turn.ended"),
|
|
(Interrupted, "interrupted"),
|
|
],
|
|
)
|
|
def test_turn_output_event_types(
|
|
model,
|
|
event_type,
|
|
):
|
|
data = {
|
|
"session_id": "session-1",
|
|
"turn_id": 1,
|
|
}
|
|
|
|
if model in (STTPartial, STTFinal, AgentDelta, AgentResponse):
|
|
data["text"] = "test"
|
|
|
|
event = model(**data)
|
|
|
|
assert event.type == event_type
|
|
|
|
|
|
def test_session_ended_output():
|
|
event = SessionEnded(
|
|
session_id="session-1",
|
|
)
|
|
|
|
assert event.type == "session.ended"
|
|
assert event.session_id == "session-1"
|
|
|
|
def test_error_event():
|
|
event = ErrorEvent(
|
|
session_id="session-1",
|
|
turn_id=1,
|
|
code="TEST_ERROR",
|
|
message="Something went wrong.",
|
|
retryable=True,
|
|
details={
|
|
"reason": "test",
|
|
},
|
|
)
|
|
|
|
assert event.model_dump() == {
|
|
"type": "error",
|
|
"session_id": "session-1",
|
|
"turn_id": 1,
|
|
"code": "TEST_ERROR",
|
|
"message": "Something went wrong.",
|
|
"retryable": True,
|
|
"details": {
|
|
"reason": "test",
|
|
},
|
|
}
|
|
|
|
|
|
def test_error_event_defaults():
|
|
event = ErrorEvent(
|
|
code="TEST_ERROR",
|
|
message="Something went wrong.",
|
|
)
|
|
|
|
assert event.type == "error"
|
|
assert event.session_id is None
|
|
assert event.turn_id is None
|
|
assert event.retryable is False
|
|
assert event.details == {}
|
|
|
|
|
|
def test_agent_delta_serialization():
|
|
event = AgentDelta(
|
|
session_id="session-1",
|
|
turn_id=7,
|
|
text=" mundo",
|
|
)
|
|
|
|
assert event.model_dump() == {
|
|
"type": "agent.delta",
|
|
"session_id": "session-1",
|
|
"turn_id": 7,
|
|
"text": " mundo",
|
|
}
|
|
|
|
|
|
def test_agent_delta_rejects_wrong_type():
|
|
with pytest.raises(ValidationError):
|
|
AgentDelta.model_validate(
|
|
{
|
|
"type": "agent.response",
|
|
"session_id": "session-1",
|
|
"turn_id": 7,
|
|
"text": "Olá",
|
|
}
|
|
)
|
|
|