155 lines
4.9 KiB
Python
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
|