"""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"