Files
livekit--agents/tests/test_inference_utils.py
2026-07-13 13:39:38 +08:00

155 lines
4.9 KiB
Python

"""Unit tests for livekit.agents.inference._utils.get_inference_headers.
Regression coverage for the crash where accessing ``ctx.agent`` before the room
is connected raised ``Exception("cannot access local participant before
connecting")`` and propagated out of ``get_inference_headers`` (introduced by
#5937, surfaced during STT websocket connection).
Note the two distinct rooms on a JobContext:
- ``ctx.room`` is the live ``rtc.Room`` (exposes ``isconnected()``); the fix
guards on it before touching the local participant.
- ``ctx.job.room`` is room metadata (the source of the room sid).
The fakes below keep them separate so the tests exercise the real code path.
"""
from __future__ import annotations
import pytest
from livekit.agents.inference._utils import (
HEADER_AGENT_ID,
HEADER_JOB_ID,
HEADER_ROOM_ID,
HEADER_USER_AGENT,
get_inference_headers,
)
pytestmark = pytest.mark.unit
class _FakeLiveRoom:
"""Stands in for ``JobContext.room`` (the live rtc.Room with isconnected())."""
def __init__(self, *, connected: bool) -> None:
self._connected = connected
def isconnected(self) -> bool:
return self._connected
class _FakeJobRoom:
"""Stands in for ``ctx.job.room`` (metadata; source of the room sid)."""
sid = "RM_test_room"
class _FakeJob:
id = "AJ_test_job"
room = _FakeJobRoom()
class _FakeAgent:
def __init__(self, sid: object) -> None:
self.sid = sid
class _CtxConnected:
"""Connected room: ``isconnected()`` is True and ``agent.sid`` is reachable."""
job = _FakeJob()
def __init__(self, agent_sid: object) -> None:
self.room = _FakeLiveRoom(connected=True)
self.agent = _FakeAgent(agent_sid)
class _CtxDisconnected:
"""Room not connected: ``isconnected()`` is False and ``agent`` access raises.
Mirrors ``rtc.Room.local_participant`` raising a bare ``Exception`` when
``_local_participant`` is still ``None`` before the first connect.
"""
job = _FakeJob()
room = _FakeLiveRoom(connected=False)
@property
def agent(self): # noqa: ANN201 - matches the raising property under test
raise Exception("cannot access local participant before connecting")
def _patch_ctx(monkeypatch: pytest.MonkeyPatch, ctx: object) -> None:
monkeypatch.setattr("livekit.agents.job.get_job_context", lambda: ctx)
def test_omits_agent_header_when_room_not_connected(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Before the room connects, the agent header is omitted without crashing.
This is the regression test for the reported STT crash. The fake ``agent``
raises exactly like ``rtc.Room.local_participant`` does pre-connect: on the
pre-fix code (no ``isconnected()`` guard) the access raises and the test
fails; with the guard the access is skipped and room/job headers remain.
"""
_patch_ctx(monkeypatch, _CtxDisconnected())
headers = get_inference_headers()
assert headers[HEADER_ROOM_ID] == "RM_test_room"
assert headers[HEADER_JOB_ID] == "AJ_test_job"
assert HEADER_AGENT_ID not in headers
assert HEADER_USER_AGENT in headers
def test_includes_agent_header_when_connected(monkeypatch: pytest.MonkeyPatch) -> None:
"""A connected room with a string sid populates the agent header."""
_patch_ctx(monkeypatch, _CtxConnected("PA_agent_sid"))
headers = get_inference_headers()
assert headers[HEADER_AGENT_ID] == "PA_agent_sid"
assert headers[HEADER_ROOM_ID] == "RM_test_room"
assert headers[HEADER_JOB_ID] == "AJ_test_job"
def test_omits_agent_header_when_sid_not_str(monkeypatch: pytest.MonkeyPatch) -> None:
"""Non-string sids (e.g. a Mock participant in tests) are not leaked.
Guards the ``isinstance(..., str)`` check added by #5943. The room is
connected so the test reaches the isinstance check rather than the
connection guard.
"""
_patch_ctx(monkeypatch, _CtxConnected(object()))
headers = get_inference_headers()
assert HEADER_AGENT_ID not in headers
assert headers[HEADER_ROOM_ID] == "RM_test_room"
assert headers[HEADER_JOB_ID] == "AJ_test_job"
def test_omits_agent_header_when_sid_empty(monkeypatch: pytest.MonkeyPatch) -> None:
"""An empty-string sid is falsy and must be omitted (connected room)."""
_patch_ctx(monkeypatch, _CtxConnected(""))
headers = get_inference_headers()
assert HEADER_AGENT_ID not in headers
def test_no_job_context_returns_only_user_agent(monkeypatch: pytest.MonkeyPatch) -> None:
"""Outside a job context (console mode / tests) only User-Agent is set."""
def _raise() -> object:
raise RuntimeError("no job context found")
monkeypatch.setattr("livekit.agents.job.get_job_context", _raise)
headers = get_inference_headers()
assert HEADER_USER_AGENT in headers
assert HEADER_ROOM_ID not in headers
assert HEADER_JOB_ID not in headers
assert HEADER_AGENT_ID not in headers