"""Tests for ServerConfig - configuration loading""" import os import tempfile from pathlib import Path import pytest from agentkit.server.config import ServerConfig, find_config_path, _resolve_env_vars, _deep_resolve class TestEnvVarResolution: """Test ${VAR:-default} pattern resolution""" def test_resolve_simple_var(self): os.environ["TEST_AK_KEY"] = "sk-123" assert _resolve_env_vars("${TEST_AK_KEY}") == "sk-123" del os.environ["TEST_AK_KEY"] def test_resolve_var_with_default(self): # Var not set -> use default assert _resolve_env_vars("${TEST_MISSING_VAR:-fallback}") == "fallback" def test_resolve_var_with_default_and_env_set(self): os.environ["TEST_AK_KEY"] = "sk-456" assert _resolve_env_vars("${TEST_AK_KEY:-fallback}") == "sk-456" del os.environ["TEST_AK_KEY"] def test_resolve_non_string(self): assert _resolve_env_vars(42) == 42 assert _resolve_env_vars(None) is None def test_deep_resolve_dict(self): os.environ["TEST_AK_KEY"] = "sk-789" data = {"api_key": "${TEST_AK_KEY}", "port": 8001} result = _deep_resolve(data) assert result["api_key"] == "sk-789" assert result["port"] == 8001 del os.environ["TEST_AK_KEY"] def test_deep_resolve_nested(self): os.environ["TEST_AK_KEY"] = "sk-nested" data = {"llm": {"providers": {"openai": {"api_key": "${TEST_AK_KEY}"}}}} result = _deep_resolve(data) assert result["llm"]["providers"]["openai"]["api_key"] == "sk-nested" del os.environ["TEST_AK_KEY"] class TestServerConfigFromYaml: """Test loading ServerConfig from YAML""" def test_load_minimal_config(self): yaml_content = """ server: host: "127.0.0.1" port: 9000 """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() config = ServerConfig.from_yaml(f.name) assert config.host == "127.0.0.1" assert config.port == 9000 os.unlink(f.name) def test_load_full_config(self): yaml_content = """ server: host: "0.0.0.0" port: 8001 workers: 4 api_key: "test-key-123" rate_limit: 120 llm: default_provider: "openai" providers: openai: api_key: "sk-test" base_url: "https://api.openai.com/v1" models: gpt-4o: alias: "default" gpt-4o-mini: alias: "fast" deepseek: api_key: "sk-deepseek" base_url: "https://api.deepseek.com/v1" models: deepseek-chat: alias: "deepseek" skills: auto_discover: true paths: - "./skills" logging: level: "DEBUG" format: "json" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() config = ServerConfig.from_yaml(f.name) assert config.host == "0.0.0.0" assert config.port == 8001 assert config.workers == 4 assert config.api_key == "test-key-123" assert config.rate_limit == 120 assert "openai" in config.llm_config.providers assert "deepseek" in config.llm_config.providers assert config.llm_config.providers["openai"].api_key == "sk-test" assert config.llm_config.model_aliases["default"] == "openai/gpt-4o" assert config.llm_config.model_aliases["fast"] == "openai/gpt-4o-mini" assert config.skill_paths == ["./skills"] assert config.auto_discover_skills is True assert config.log_level == "DEBUG" assert config.log_format == "json" os.unlink(f.name) def test_load_config_with_env_vars(self): os.environ["TEST_AK_OPENAI_KEY"] = "sk-from-env" yaml_content = """ server: host: "0.0.0.0" port: 8001 llm: providers: openai: api_key: "${TEST_AK_OPENAI_KEY}" base_url: "https://api.openai.com/v1" models: gpt-4o: alias: "default" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write(yaml_content) f.flush() config = ServerConfig.from_yaml(f.name) assert config.llm_config.providers["openai"].api_key == "sk-from-env" del os.environ["TEST_AK_OPENAI_KEY"] os.unlink(f.name) class TestServerConfigLoadSkillConfigs: """Test loading skill configs from skill paths""" def test_load_skills_from_directory(self): yaml_content = """ server: host: "0.0.0.0" port: 8001 skills: paths: - "./skills" """ with tempfile.TemporaryDirectory() as tmpdir: skills_dir = Path(tmpdir) / "skills" skills_dir.mkdir() # Create a test skill YAML skill_yaml = skills_dir / "test_skill.yaml" skill_yaml.write_text(""" name: test_skill agent_type: test task_mode: llm_generate supported_tasks: - test_task prompt: identity: "Test skill" """) # Update yaml_content with absolute path yaml_content_updated = yaml_content.replace("./skills", str(skills_dir)) with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, dir=tmpdir) as f: f.write(yaml_content_updated) f.flush() config = ServerConfig.from_yaml(f.name) configs = config.load_skill_configs() assert len(configs) == 1 assert configs[0].name == "test_skill" os.unlink(f.name) def test_load_skills_from_single_file(self): with tempfile.TemporaryDirectory() as tmpdir: skill_yaml = Path(tmpdir) / "my_skill.yaml" skill_yaml.write_text(""" name: my_skill agent_type: test task_mode: llm_generate supported_tasks: - test_task prompt: identity: "My skill" """) yaml_content = f""" server: host: "0.0.0.0" port: 8001 skills: paths: - "{skill_yaml}" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, dir=tmpdir) as f: f.write(yaml_content) f.flush() config = ServerConfig.from_yaml(f.name) configs = config.load_skill_configs() assert len(configs) == 1 assert configs[0].name == "my_skill" os.unlink(f.name) def test_load_skills_skips_invalid(self): with tempfile.TemporaryDirectory() as tmpdir: skills_dir = Path(tmpdir) / "skills" skills_dir.mkdir() # Valid skill (skills_dir / "valid.yaml").write_text(""" name: valid_skill agent_type: test task_mode: llm_generate supported_tasks: - test prompt: identity: "Valid skill" """) # Invalid skill (missing required fields) (skills_dir / "invalid.yaml").write_text("not_a_valid: yaml") yaml_content = f""" server: host: "0.0.0.0" port: 8001 skills: paths: - "{skills_dir}" """ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, dir=tmpdir) as f: f.write(yaml_content) f.flush() config = ServerConfig.from_yaml(f.name) configs = config.load_skill_configs() assert len(configs) == 1 assert configs[0].name == "valid_skill" os.unlink(f.name) class TestServerConfigLoadDotenv: """Test loading .env file""" def test_load_dotenv(self): with tempfile.TemporaryDirectory() as tmpdir: env_file = Path(tmpdir) / ".env" env_file.write_text("MY_TEST_VAR=hello_world\n# comment\nEMPTY_VAR=\n") config = ServerConfig() config.load_dotenv(str(env_file)) assert os.environ.get("MY_TEST_VAR") == "hello_world" # Cleanup del os.environ["MY_TEST_VAR"] def test_load_dotenv_no_overwrite(self): os.environ["EXISTING_VAR"] = "original" with tempfile.TemporaryDirectory() as tmpdir: env_file = Path(tmpdir) / ".env" env_file.write_text("EXISTING_VAR=should_not_overwrite\n") config = ServerConfig() config.load_dotenv(str(env_file)) assert os.environ["EXISTING_VAR"] == "original" del os.environ["EXISTING_VAR"] def test_load_dotenv_missing_file(self): config = ServerConfig() config.load_dotenv("/nonexistent/.env") # Should not raise class TestFindConfigPath: """Test config file discovery""" def test_explicit_path_exists(self): with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as f: f.write(b"test: true") f.flush() result = find_config_path(f.name) assert result == f.name os.unlink(f.name) def test_explicit_path_not_exists(self): result = find_config_path("/nonexistent/agentkit.yaml") assert result is None def test_find_in_cwd(self): original_cwd = os.getcwd() with tempfile.TemporaryDirectory() as tmpdir: os.chdir(tmpdir) config_file = Path(tmpdir) / "agentkit.yaml" config_file.write_text("test: true") result = find_config_path() assert result is not None os.chdir(original_cwd) def test_no_config_found(self): original_cwd = os.getcwd() with tempfile.TemporaryDirectory() as tmpdir: os.chdir(tmpdir) result = find_config_path() # May find home dir config, so just check it doesn't crash assert result is None or result.endswith("agentkit.yaml") os.chdir(original_cwd)