180 lines
6.3 KiB
Python
180 lines
6.3 KiB
Python
"""Config precedence tests.
|
|
|
|
These tests simulate the full lifecycle of an AgentServer to verify the config
|
|
override order matches what the simulation system depends on:
|
|
url/keys: CLI update_options, then constructor arg, then env var
|
|
agent_name: LIVEKIT_AGENT_NAME_OVERRIDE, then the rtc_session decorator arg,
|
|
then LIVEKIT_AGENT_NAME
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from livekit.agents.cli import proto
|
|
from livekit.agents.cli.cli import _run_worker
|
|
from livekit.agents.worker import AgentServer
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
class _TestableServer(AgentServer):
|
|
"""AgentServer that captures config at run() time instead of blocking."""
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.captured_config: dict | None = None
|
|
|
|
async def run(self, **kwargs):
|
|
self.captured_config = {
|
|
"ws_url": self._ws_url,
|
|
"api_key": self._api_key,
|
|
"api_secret": self._api_secret,
|
|
"agent_name": self._agent_name,
|
|
}
|
|
raise SystemExit(0)
|
|
|
|
|
|
def _run_and_capture(server: _TestableServer, args: proto.CliArgs) -> dict:
|
|
"""Run _run_worker and return the config that was active when run() was called."""
|
|
try:
|
|
_run_worker(server, args)
|
|
except SystemExit:
|
|
pass
|
|
assert server.captured_config is not None, "run() was never called"
|
|
return server.captured_config
|
|
|
|
|
|
class TestCLIOverridesConstructorAndEnv:
|
|
def test_cli_url_wins(self):
|
|
with patch.dict(os.environ, {"LIVEKIT_URL": "ws://env"}):
|
|
server = _TestableServer(ws_url="ws://constructor")
|
|
config = _run_and_capture(server, proto.CliArgs(log_level="INFO", url="ws://cli"))
|
|
assert config["ws_url"] == "ws://cli"
|
|
|
|
def test_cli_keys_win(self):
|
|
with patch.dict(
|
|
os.environ, {"LIVEKIT_API_KEY": "env-key", "LIVEKIT_API_SECRET": "env-secret"}
|
|
):
|
|
server = _TestableServer(api_key="constructor-key", api_secret="constructor-secret")
|
|
config = _run_and_capture(
|
|
server,
|
|
proto.CliArgs(
|
|
log_level="INFO",
|
|
api_key="cli-key",
|
|
api_secret="cli-secret",
|
|
),
|
|
)
|
|
assert config["api_key"] == "cli-key"
|
|
assert config["api_secret"] == "cli-secret"
|
|
|
|
def test_constructor_wins_over_env_when_no_cli(self):
|
|
with patch.dict(os.environ, {"LIVEKIT_URL": "ws://env"}):
|
|
server = _TestableServer(ws_url="ws://constructor")
|
|
config = _run_and_capture(server, proto.CliArgs(log_level="INFO"))
|
|
assert config["ws_url"] == "ws://constructor"
|
|
|
|
def test_env_fallback_when_no_constructor_no_cli(self):
|
|
with patch.dict(os.environ, {"LIVEKIT_URL": "ws://env"}, clear=False):
|
|
server = _TestableServer()
|
|
config = _run_and_capture(server, proto.CliArgs(log_level="INFO"))
|
|
assert config["ws_url"] == "ws://env"
|
|
|
|
|
|
class TestAgentNamePrecedence:
|
|
def test_override_wins_over_decorator_and_default(self):
|
|
with patch.dict(
|
|
os.environ,
|
|
{"LIVEKIT_AGENT_NAME_OVERRIDE": "override-agent", "LIVEKIT_AGENT_NAME": "env-agent"},
|
|
clear=True,
|
|
):
|
|
server = _TestableServer()
|
|
|
|
@server.rtc_session(agent_name="decorator-agent")
|
|
async def entrypoint(ctx):
|
|
pass
|
|
|
|
config = _run_and_capture(server, proto.CliArgs(log_level="INFO"))
|
|
assert config["agent_name"] == "override-agent"
|
|
|
|
def test_decorator_wins_over_env_default(self):
|
|
with patch.dict(os.environ, {"LIVEKIT_AGENT_NAME": "env-agent"}, clear=True):
|
|
server = _TestableServer()
|
|
|
|
@server.rtc_session(agent_name="decorator-agent")
|
|
async def entrypoint(ctx):
|
|
pass
|
|
|
|
config = _run_and_capture(server, proto.CliArgs(log_level="INFO"))
|
|
assert config["agent_name"] == "decorator-agent"
|
|
|
|
def test_env_default_when_decorator_has_no_name(self):
|
|
with patch.dict(os.environ, {"LIVEKIT_AGENT_NAME": "env-agent"}, clear=True):
|
|
server = _TestableServer()
|
|
|
|
@server.rtc_session()
|
|
async def entrypoint(ctx):
|
|
pass
|
|
|
|
config = _run_and_capture(server, proto.CliArgs(log_level="INFO"))
|
|
assert config["agent_name"] == "env-agent"
|
|
|
|
def test_decorator_when_no_env(self):
|
|
with patch.dict(os.environ, {}, clear=True):
|
|
server = _TestableServer()
|
|
|
|
@server.rtc_session(agent_name="decorator-agent")
|
|
async def entrypoint(ctx):
|
|
pass
|
|
|
|
config = _run_and_capture(server, proto.CliArgs(log_level="INFO"))
|
|
assert config["agent_name"] == "decorator-agent"
|
|
|
|
def test_empty_when_neither(self):
|
|
with patch.dict(os.environ, {}, clear=True):
|
|
server = _TestableServer()
|
|
config = _run_and_capture(server, proto.CliArgs(log_level="INFO"))
|
|
assert config["agent_name"] == ""
|
|
|
|
|
|
class TestFullPrecedenceChain:
|
|
def test_everything_set(self):
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"LIVEKIT_URL": "ws://env",
|
|
"LIVEKIT_API_KEY": "env-key",
|
|
"LIVEKIT_API_SECRET": "env-secret",
|
|
"LIVEKIT_AGENT_NAME": "env-agent",
|
|
},
|
|
):
|
|
server = _TestableServer(
|
|
ws_url="ws://constructor",
|
|
api_key="constructor-key",
|
|
api_secret="constructor-secret",
|
|
)
|
|
|
|
@server.rtc_session(agent_name="decorator-agent")
|
|
async def entrypoint(ctx):
|
|
pass
|
|
|
|
config = _run_and_capture(
|
|
server,
|
|
proto.CliArgs(
|
|
log_level="INFO",
|
|
url="ws://cli",
|
|
api_key="cli-key",
|
|
api_secret="cli-secret",
|
|
),
|
|
)
|
|
|
|
assert config["ws_url"] == "ws://cli"
|
|
assert config["api_key"] == "cli-key"
|
|
assert config["api_secret"] == "cli-secret"
|
|
# no LIVEKIT_AGENT_NAME_OVERRIDE set, so the explicit decorator arg wins
|
|
# over the LIVEKIT_AGENT_NAME default
|
|
assert config["agent_name"] == "decorator-agent"
|