Files
voiceagent/tests/test_stt_abort.py

307 lines
6.3 KiB
Python

"""
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