fischer-agentkit/tests/unit/test_server_config.py

325 lines
9.5 KiB
Python

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