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