307 lines
6.3 KiB
Python
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 |