import os from pathlib import Path import pytest os.environ.setdefault("LLM_API_KEY", "test-openai-key") os.environ.setdefault("TTS_API_KEY", "test-tts-key") os.environ.setdefault("ASR_API_KEY", "test-asr-key") from app.config import load_settings def _write_yaml(path: Path, content: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") def _full_agent_yaml(llm_model: str = "gpt-4o-mini", llm_key: str = "test-openai-key") -> str: return f""" agent: vad: type: silero model_path: data/vad/silero_vad.onnx threshold: 0.63 min_speech_duration_ms: 100 eou_threshold_ms: 800 llm: provider: openai_compatible model: {llm_model} temperature: 0.2 api_key: {llm_key} api_url: https://example-llm.invalid/v1 tts: provider: openai_compatible api_key: test-tts-key api_url: https://example-tts.invalid/v1/audio/speech model: FunAudioLLM/CosyVoice2-0.5B voice: anna speed: 1.0 asr: provider: openai_compatible api_key: test-asr-key api_url: https://example-asr.invalid/v1/audio/transcriptions model: FunAudioLLM/SenseVoiceSmall interim_interval_ms: 500 min_audio_ms: 300 start_min_speech_ms: 160 pre_speech_ms: 240 final_tail_ms: 120 duplex: enabled: true system_prompt: You are a strict test assistant. barge_in: min_duration_ms: 200 silence_tolerance_ms: 60 """.strip() def _dashscope_tts_yaml() -> str: return _full_agent_yaml().replace( """ tts: provider: openai_compatible api_key: test-tts-key api_url: https://example-tts.invalid/v1/audio/speech model: FunAudioLLM/CosyVoice2-0.5B voice: anna speed: 1.0 """, """ tts: provider: dashscope api_key: test-dashscope-key voice: Cherry speed: 1.0 """, ) def test_cli_profile_loads_agent_yaml(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) config_dir = tmp_path / "config" / "agents" _write_yaml( config_dir / "support.yaml", _full_agent_yaml(llm_model="gpt-4.1-mini"), ) settings = load_settings( argv=["--agent-profile", "support"], ) assert settings.llm_model == "gpt-4.1-mini" assert settings.llm_temperature == 0.2 assert settings.vad_threshold == 0.63 assert settings.agent_config_source == "cli_profile" assert settings.agent_config_path == str((config_dir / "support.yaml").resolve()) def test_cli_path_has_higher_priority_than_env(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) env_file = tmp_path / "config" / "agents" / "env.yaml" cli_file = tmp_path / "config" / "agents" / "cli.yaml" _write_yaml(env_file, _full_agent_yaml(llm_model="env-model")) _write_yaml(cli_file, _full_agent_yaml(llm_model="cli-model")) monkeypatch.setenv("AGENT_CONFIG_PATH", str(env_file)) settings = load_settings(argv=["--agent-config", str(cli_file)]) assert settings.llm_model == "cli-model" assert settings.agent_config_source == "cli_path" assert settings.agent_config_path == str(cli_file.resolve()) def test_default_yaml_is_loaded_without_args_or_env(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) default_file = tmp_path / "config" / "agents" / "default.yaml" _write_yaml(default_file, _full_agent_yaml(llm_model="from-default")) monkeypatch.delenv("AGENT_CONFIG_PATH", raising=False) monkeypatch.delenv("AGENT_PROFILE", raising=False) settings = load_settings(argv=[]) assert settings.llm_model == "from-default" assert settings.agent_config_source == "default" assert settings.agent_config_path == str(default_file.resolve()) def test_missing_required_agent_settings_fail(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) file_path = tmp_path / "missing-required.yaml" _write_yaml( file_path, """ agent: llm: model: gpt-4o-mini """.strip(), ) with pytest.raises(ValueError, match="Missing required agent settings in YAML"): load_settings(argv=["--agent-config", str(file_path)]) def test_blank_required_provider_key_fails(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) file_path = tmp_path / "blank-key.yaml" _write_yaml(file_path, _full_agent_yaml(llm_key="")) with pytest.raises(ValueError, match="Missing required agent settings in YAML"): load_settings(argv=["--agent-config", str(file_path)]) def test_missing_tts_api_url_fails(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) file_path = tmp_path / "missing-tts-url.yaml" _write_yaml( file_path, _full_agent_yaml().replace( " api_url: https://example-tts.invalid/v1/audio/speech\n", "", ), ) with pytest.raises(ValueError, match="Missing required agent settings in YAML"): load_settings(argv=["--agent-config", str(file_path)]) def test_dashscope_tts_allows_default_url_and_model(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) file_path = tmp_path / "dashscope-tts.yaml" _write_yaml(file_path, _dashscope_tts_yaml()) settings = load_settings(argv=["--agent-config", str(file_path)]) assert settings.tts_provider == "dashscope" assert settings.tts_api_key == "test-dashscope-key" assert settings.tts_api_url is None assert settings.tts_model is None def test_dashscope_tts_requires_api_key(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) file_path = tmp_path / "dashscope-tts-missing-key.yaml" _write_yaml(file_path, _dashscope_tts_yaml().replace(" api_key: test-dashscope-key\n", "")) with pytest.raises(ValueError, match="Missing required agent settings in YAML"): load_settings(argv=["--agent-config", str(file_path)]) def test_missing_asr_api_url_fails(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) file_path = tmp_path / "missing-asr-url.yaml" _write_yaml( file_path, _full_agent_yaml().replace( " api_url: https://example-asr.invalid/v1/audio/transcriptions\n", "", ), ) with pytest.raises(ValueError, match="Missing required agent settings in YAML"): load_settings(argv=["--agent-config", str(file_path)]) def test_agent_yaml_unknown_key_fails(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) file_path = tmp_path / "bad-agent.yaml" _write_yaml(file_path, _full_agent_yaml() + "\n unknown_option: true") with pytest.raises(ValueError, match="Unknown agent config keys"): load_settings(argv=["--agent-config", str(file_path)]) def test_legacy_siliconflow_section_fails(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) file_path = tmp_path / "legacy-siliconflow.yaml" _write_yaml( file_path, """ agent: siliconflow: api_key: x """.strip(), ) with pytest.raises(ValueError, match="Section 'siliconflow' is no longer supported"): load_settings(argv=["--agent-config", str(file_path)]) def test_agent_yaml_missing_env_reference_fails(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) file_path = tmp_path / "bad-ref.yaml" _write_yaml( file_path, _full_agent_yaml(llm_key="${UNSET_LLM_API_KEY}"), ) with pytest.raises(ValueError, match="Missing environment variable"): load_settings(argv=["--agent-config", str(file_path)]) def test_agent_yaml_tools_list_is_loaded(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) file_path = tmp_path / "tools-agent.yaml" _write_yaml( file_path, _full_agent_yaml() + """ tools: - current_time - name: weather description: Get weather by city. parameters: type: object properties: city: type: string required: [city] executor: server """, ) settings = load_settings(argv=["--agent-config", str(file_path)]) assert isinstance(settings.tools, list) assert settings.tools[0] == "current_time" assert settings.tools[1]["name"] == "weather" assert settings.tools[1]["executor"] == "server" def test_agent_yaml_tools_must_be_list(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) file_path = tmp_path / "bad-tools-agent.yaml" _write_yaml( file_path, _full_agent_yaml() + """ tools: weather: executor: server """, ) with pytest.raises(ValueError, match="Agent config key 'tools' must be a list"): load_settings(argv=["--agent-config", str(file_path)])