Files
2026-07-13 13:39:38 +08:00

118 lines
3.7 KiB
Python

"""Unit tests for base STT `RecognizeStream` fields (start_time, etc.)."""
from __future__ import annotations
import asyncio
import time
import pytest
from livekit.agents import APIConnectionError
from livekit.agents.stt import (
STT,
RecognizeStream,
SpeechData,
SpeechEvent,
SpeechEventType,
STTCapabilities,
)
from livekit.agents.types import DEFAULT_API_CONNECT_OPTIONS
from livekit.agents.utils.audio import AudioBuffer
pytestmark = [pytest.mark.unit, pytest.mark.virtual_time, pytest.mark.no_concurrent]
class _DummyStream(RecognizeStream):
"""Minimal RecognizeStream for unit tests — does not hit the network."""
def __init__(
self,
*,
stt: STT,
fail_first_run: bool = False,
) -> None:
super().__init__(stt=stt, conn_options=DEFAULT_API_CONNECT_OPTIONS)
self._fail_first_run = fail_first_run
self._run_count = 0
async def _run(self) -> None:
self._run_count += 1
if self._fail_first_run and self._run_count == 1:
raise APIConnectionError("fake failure to trigger retry")
# emit a final and exit so _main_task can complete normally
self._event_ch.send_nowait(
SpeechEvent(
type=SpeechEventType.FINAL_TRANSCRIPT,
alternatives=[SpeechData(language="", text="hello")],
)
)
class _DummySTT(STT):
def __init__(self) -> None:
super().__init__(capabilities=STTCapabilities(streaming=True, interim_results=False))
async def _recognize_impl(self, buffer: AudioBuffer, *, language, conn_options) -> SpeechEvent:
raise NotImplementedError
def stream(self, *, language=None, conn_options=DEFAULT_API_CONNECT_OPTIONS) -> _DummyStream:
return _DummyStream(stt=self)
async def test_start_time_seeded_on_init() -> None:
"""start_time is initialized to approximately time.time() when the stream is created."""
stt = _DummySTT()
before = time.time()
stream = stt.stream()
after = time.time()
assert before <= stream.start_time <= after
await stream.aclose()
async def test_start_time_setter_accepts_valid_values() -> None:
"""Plugins can override start_time by assigning to the public property."""
stt = _DummySTT()
stream = stt.stream()
new_anchor = time.time() + 10.0
stream.start_time = new_anchor
assert stream.start_time == new_anchor
await stream.aclose()
async def test_start_time_setter_rejects_negative() -> None:
"""start_time setter validates non-negative, matching start_time_offset behavior."""
stt = _DummySTT()
stream = stt.stream()
with pytest.raises(ValueError, match="start_time must be non-negative"):
stream.start_time = -1.0
await stream.aclose()
async def test_start_time_reseeded_on_retry() -> None:
"""When _main_task retries after an APIError, start_time is re-seeded so plugin
overrides from the previous connection don't leak into the new one."""
stt = _DummySTT()
stream = _DummyStream(stt=stt, fail_first_run=True)
# Simulate a plugin overriding start_time during the first (failing) _run()
# by assigning a sentinel value before the task picks up.
sentinel = 1.0
stream.start_time = sentinel
# Let the main task run: it should retry past the first-run APIError, and
# on each attempt re-seed start_time to a fresh time.time() value before
# _run() is called.
await asyncio.wait_for(stream._task, timeout=5.0)
# After the retry, start_time must have been re-seeded (not equal to sentinel).
assert stream.start_time != sentinel
# And it should be a recent wall-clock value.
assert time.time() - stream.start_time < 5.0
await stream.aclose()