325 lines
9.5 KiB
Python
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)
|