feat: establish voice assistant V1 baseline
This commit is contained in:
307
tests/test_stt_abort.py
Normal file
307
tests/test_stt_abort.py
Normal file
@@ -0,0 +1,307 @@
|
||||
"""
|
||||
Tests for STT session abort behavior.
|
||||
|
||||
Purpose:
|
||||
Verify that STT implementations support interruption
|
||||
without delivering stale transcription results.
|
||||
|
||||
Responsibilities:
|
||||
- Validate immediate abort.
|
||||
- Verify pending transcript events are discarded.
|
||||
- Verify repeated abort calls are safe.
|
||||
- Verify audio is rejected after abort.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from voice_assistant.stt.nvidia import NvidiaSTTAdapter
|
||||
from voice_assistant.stt.models import TranscriptEvent
|
||||
|
||||
from voice_assistant.stt.mock import MockSTTAdapter
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mock_stt_abort_discards_pending_events():
|
||||
stt = MockSTTAdapter()
|
||||
|
||||
await stt.start_session()
|
||||
|
||||
await stt.emit_partial("Olá")
|
||||
await stt.emit_final("Olá, tudo bem?")
|
||||
|
||||
await stt.abort_session()
|
||||
|
||||
events = [
|
||||
event
|
||||
async for event in stt.events()
|
||||
]
|
||||
|
||||
assert events == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mock_stt_abort_rejects_audio():
|
||||
stt = MockSTTAdapter()
|
||||
|
||||
await stt.start_session()
|
||||
await stt.abort_session()
|
||||
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match="STT session has not been started",
|
||||
):
|
||||
await stt.send_audio(b"\x00\x01")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mock_stt_abort_is_idempotent():
|
||||
stt = MockSTTAdapter()
|
||||
|
||||
await stt.start_session()
|
||||
|
||||
await stt.abort_session()
|
||||
await stt.abort_session()
|
||||
|
||||
assert stt._started is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_mock_stt_abort_unblocks_event_consumer():
|
||||
stt = MockSTTAdapter()
|
||||
|
||||
await stt.start_session()
|
||||
|
||||
async def consume():
|
||||
return [
|
||||
event
|
||||
async for event in stt.events()
|
||||
]
|
||||
|
||||
import asyncio
|
||||
|
||||
consumer = asyncio.create_task(consume())
|
||||
|
||||
# Allow the consumer to start waiting for events.
|
||||
await asyncio.sleep(0)
|
||||
|
||||
await stt.abort_session()
|
||||
|
||||
events = await asyncio.wait_for(
|
||||
consumer,
|
||||
timeout=1.0,
|
||||
)
|
||||
|
||||
assert events == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_nvidia_abort_without_started_session():
|
||||
stt = NvidiaSTTAdapter()
|
||||
|
||||
await stt.abort_session()
|
||||
await stt.abort_session()
|
||||
|
||||
assert stt._started is False
|
||||
assert stt._websocket is None
|
||||
assert stt._http is None
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_nvidia_abort_discards_pending_events():
|
||||
stt = NvidiaSTTAdapter()
|
||||
|
||||
stt._started = True
|
||||
|
||||
await stt._events.put(
|
||||
TranscriptEvent(
|
||||
type="partial",
|
||||
text="Transcrição antiga",
|
||||
)
|
||||
)
|
||||
|
||||
await stt.abort_session()
|
||||
|
||||
events = [
|
||||
event
|
||||
async for event in stt.events()
|
||||
]
|
||||
|
||||
assert events == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_nvidia_abort_unblocks_consumer():
|
||||
stt = NvidiaSTTAdapter()
|
||||
stt._started = True
|
||||
|
||||
async def consume():
|
||||
return [
|
||||
event
|
||||
async for event in stt.events()
|
||||
]
|
||||
|
||||
consumer = asyncio.create_task(consume())
|
||||
|
||||
await asyncio.sleep(0)
|
||||
|
||||
await stt.abort_session()
|
||||
|
||||
events = await asyncio.wait_for(
|
||||
consumer,
|
||||
timeout=1.0,
|
||||
)
|
||||
|
||||
assert events == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_nvidia_abort_closes_resources_without_done():
|
||||
stt = NvidiaSTTAdapter()
|
||||
|
||||
websocket = AsyncMock()
|
||||
http_client = AsyncMock()
|
||||
|
||||
stt._started = True
|
||||
stt._websocket = websocket
|
||||
stt._http = http_client
|
||||
|
||||
await stt.abort_session()
|
||||
|
||||
websocket.close.assert_awaited_once()
|
||||
websocket.send.assert_not_awaited()
|
||||
http_client.aclose.assert_awaited_once()
|
||||
|
||||
assert stt._websocket is None
|
||||
assert stt._http is None
|
||||
assert stt._started is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_nvidia_abort_cancels_receiver_task():
|
||||
stt = NvidiaSTTAdapter()
|
||||
stt._started = True
|
||||
|
||||
started = asyncio.Event()
|
||||
finished = asyncio.Event()
|
||||
|
||||
async def receiver():
|
||||
started.set()
|
||||
|
||||
try:
|
||||
await asyncio.Event().wait()
|
||||
finally:
|
||||
finished.set()
|
||||
|
||||
task = asyncio.create_task(receiver())
|
||||
stt._receiver_task = task
|
||||
|
||||
await started.wait()
|
||||
|
||||
await stt.abort_session()
|
||||
|
||||
assert task.done()
|
||||
assert task.cancelled()
|
||||
assert finished.is_set()
|
||||
assert stt._receiver_task is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_nvidia_concurrent_abort():
|
||||
"""
|
||||
Verify that concurrent abort requests produce a single
|
||||
end-of-stream marker and leave the adapter stopped.
|
||||
"""
|
||||
stt = NvidiaSTTAdapter()
|
||||
stt._started = True
|
||||
|
||||
await asyncio.gather(
|
||||
stt.abort_session(),
|
||||
stt.abort_session(),
|
||||
)
|
||||
|
||||
assert stt._started is False
|
||||
assert stt._receiver_task is None
|
||||
assert stt._websocket is None
|
||||
assert stt._http is None
|
||||
|
||||
assert stt._events.qsize() == 1
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_nvidia_abort_does_not_deliver_late_events():
|
||||
"""Queued transcripts must not survive an explicit abort."""
|
||||
stt = NvidiaSTTAdapter()
|
||||
stt._started = True
|
||||
|
||||
await stt._events.put(
|
||||
TranscriptEvent(
|
||||
type="partial",
|
||||
text="Resultado antigo",
|
||||
)
|
||||
)
|
||||
|
||||
await stt.abort_session()
|
||||
|
||||
events = [
|
||||
event
|
||||
async for event in stt.events()
|
||||
]
|
||||
|
||||
assert events == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_nvidia_concurrent_abort_closes_resources_once():
|
||||
"""
|
||||
Verify that concurrent abort requests do not close
|
||||
provider resources more than once.
|
||||
"""
|
||||
stt = NvidiaSTTAdapter()
|
||||
|
||||
websocket = AsyncMock()
|
||||
http_client = AsyncMock()
|
||||
|
||||
stt._started = True
|
||||
stt._websocket = websocket
|
||||
stt._http = http_client
|
||||
|
||||
# Force both abort operations to overlap during cleanup.
|
||||
close_started = asyncio.Event()
|
||||
allow_close = asyncio.Event()
|
||||
|
||||
async def slow_close():
|
||||
close_started.set()
|
||||
await allow_close.wait()
|
||||
|
||||
websocket.close.side_effect = slow_close
|
||||
|
||||
first_abort = asyncio.create_task(
|
||||
stt.abort_session()
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
close_started.wait(),
|
||||
timeout=1.0,
|
||||
)
|
||||
|
||||
second_abort = asyncio.create_task(
|
||||
stt.abort_session()
|
||||
)
|
||||
|
||||
# Give the second abort a chance to enter cleanup.
|
||||
await asyncio.sleep(0)
|
||||
|
||||
finally:
|
||||
allow_close.set()
|
||||
|
||||
await asyncio.gather(
|
||||
first_abort,
|
||||
second_abort,
|
||||
)
|
||||
|
||||
websocket.close.assert_awaited_once()
|
||||
http_client.aclose.assert_awaited_once()
|
||||
|
||||
assert stt._events.qsize() == 1
|
||||
Reference in New Issue
Block a user