chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""Tests for the adaptive-interruption threshold negotiation contract.
|
||||
|
||||
The feature is server-driven: the SDK only sends ``threshold`` in ``session.create`` when the user
|
||||
explicitly overrode it, and otherwise omits the field so the server applies its fetched default.
|
||||
These tests lock that serialization contract (which breaks silently if someone "cleans up" the
|
||||
``if is_given(...) else None`` guard or the ``exclude_none=True`` dump) plus the parsing of the
|
||||
server's ``default_threshold`` off ``session.created``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
|
||||
from livekit.agents.inference.interruption import (
|
||||
AdaptiveInterruptionDetector,
|
||||
InterruptionWebSocketStream,
|
||||
InterruptionWSSessionCreatedMessage,
|
||||
InterruptionWSSessionCreateSettings,
|
||||
)
|
||||
from livekit.agents.types import NOT_GIVEN, APIConnectOptions, NotGivenOr
|
||||
from livekit.agents.utils import aio
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
CONN_OPTIONS = APIConnectOptions(max_retry=0, retry_interval=0.0, timeout=1.0)
|
||||
|
||||
|
||||
def _make_detector(*, threshold: NotGivenOr[float] = NOT_GIVEN) -> AdaptiveInterruptionDetector:
|
||||
mock_session = AsyncMock(spec=aiohttp.ClientSession)
|
||||
return AdaptiveInterruptionDetector(
|
||||
threshold=threshold,
|
||||
base_url="http://localhost:9999",
|
||||
api_key="test-key",
|
||||
api_secret="test-secret",
|
||||
http_session=mock_session,
|
||||
)
|
||||
|
||||
|
||||
async def _make_idle_stream(
|
||||
detector: AdaptiveInterruptionDetector,
|
||||
) -> InterruptionWebSocketStream:
|
||||
"""Create a stream and cancel its live receive loop so we can drive methods directly.
|
||||
|
||||
The stream's __init__ spawns a background task that runs the full WS loop; cancel it before it
|
||||
races against the assertions (it would otherwise call _connect_ws itself against the mock).
|
||||
"""
|
||||
stream = detector.stream(conn_options=CONN_OPTIONS)
|
||||
await aio.cancel_and_wait(stream._task)
|
||||
return stream
|
||||
|
||||
|
||||
async def _capture_session_create(detector: AdaptiveInterruptionDetector) -> dict:
|
||||
"""Run the real _connect_ws path and return the parsed session.create payload it sent."""
|
||||
mock_ws = AsyncMock(spec=aiohttp.ClientWebSocketResponse)
|
||||
mock_ws.closed = False
|
||||
detector._session.ws_connect = AsyncMock(return_value=mock_ws) # type: ignore[union-attr]
|
||||
|
||||
stream = await _make_idle_stream(detector)
|
||||
try:
|
||||
await stream._connect_ws()
|
||||
finally:
|
||||
await stream.aclose()
|
||||
|
||||
mock_ws.send_str.assert_called_once()
|
||||
return json.loads(mock_ws.send_str.call_args.args[0])
|
||||
|
||||
|
||||
class TestSessionCreateThreshold:
|
||||
@pytest.mark.asyncio
|
||||
async def test_omits_threshold_when_not_given(self) -> None:
|
||||
payload = await _capture_session_create(_make_detector())
|
||||
assert "threshold" not in payload["settings"], payload
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_includes_threshold_when_overridden(self) -> None:
|
||||
payload = await _capture_session_create(_make_detector(threshold=0.7))
|
||||
assert payload["settings"]["threshold"] == 0.7, payload
|
||||
|
||||
|
||||
class TestSessionCreateSettingsSerialization:
|
||||
"""The contract at the model level: exclude_none must drop an unset threshold."""
|
||||
|
||||
def test_exclude_none_drops_unset_threshold(self) -> None:
|
||||
settings = InterruptionWSSessionCreateSettings(
|
||||
sample_rate=16000, num_channels=1, threshold=None, min_frames=2, encoding="s16le"
|
||||
)
|
||||
dumped = json.loads(settings.model_dump_json(exclude_none=True))
|
||||
assert "threshold" not in dumped, dumped
|
||||
|
||||
def test_explicit_threshold_survives_exclude_none(self) -> None:
|
||||
settings = InterruptionWSSessionCreateSettings(
|
||||
sample_rate=16000, num_channels=1, threshold=0.7, min_frames=2, encoding="s16le"
|
||||
)
|
||||
dumped = json.loads(settings.model_dump_json(exclude_none=True))
|
||||
assert dumped["threshold"] == 0.7, dumped
|
||||
|
||||
|
||||
class TestSessionCreatedDefaultThreshold:
|
||||
def test_parses_default_threshold(self) -> None:
|
||||
msg = InterruptionWSSessionCreatedMessage.model_validate(
|
||||
{"type": "session.created", "default_threshold": 0.42}
|
||||
)
|
||||
assert msg.default_threshold == 0.42
|
||||
|
||||
def test_default_threshold_optional(self) -> None:
|
||||
msg = InterruptionWSSessionCreatedMessage.model_validate({"type": "session.created"})
|
||||
assert msg.default_threshold is None
|
||||
|
||||
|
||||
class TestResolveEffectiveThreshold:
|
||||
"""Observability-only resolution: user override > server default > None."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_override_wins(self) -> None:
|
||||
stream = await _make_idle_stream(_make_detector(threshold=0.7))
|
||||
try:
|
||||
assert stream._resolve_effective_threshold(0.3) == 0.7
|
||||
finally:
|
||||
await stream.aclose()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_falls_back_to_server_default(self) -> None:
|
||||
stream = await _make_idle_stream(_make_detector())
|
||||
try:
|
||||
assert stream._resolve_effective_threshold(0.3) == 0.3
|
||||
finally:
|
||||
await stream.aclose()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_none_when_server_silent(self) -> None:
|
||||
stream = await _make_idle_stream(_make_detector())
|
||||
try:
|
||||
assert stream._resolve_effective_threshold(None) is None
|
||||
finally:
|
||||
await stream.aclose()
|
||||
Reference in New Issue
Block a user