a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
201 lines
6.9 KiB
Python
201 lines
6.9 KiB
Python
"""Unit tests for eval.config module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from config import (
|
|
list_configs,
|
|
load_mode_config,
|
|
load_model_config,
|
|
merge_configs,
|
|
validate_config,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture()
|
|
def tmp_configs(tmp_path, monkeypatch):
|
|
"""Create a temporary configs directory and patch _CONFIGS_DIR."""
|
|
models_dir = tmp_path / "models"
|
|
modes_dir = tmp_path / "modes"
|
|
models_dir.mkdir()
|
|
modes_dir.mkdir()
|
|
|
|
import config as config_mod
|
|
|
|
monkeypatch.setattr(config_mod, "_CONFIGS_DIR", tmp_path)
|
|
return tmp_path
|
|
|
|
|
|
def _write_yaml(path: Path, data: dict) -> None:
|
|
with open(path, "w") as f:
|
|
yaml.safe_dump(data, f)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# load_model_config
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestLoadModelConfig:
|
|
def test_loads_valid_yaml(self, tmp_configs):
|
|
data = {"model": {"model_name": "test-model", "cost_tracking": "ignore_errors"}}
|
|
_write_yaml(tmp_configs / "models" / "test.yaml", data)
|
|
result = load_model_config("test")
|
|
assert result == data
|
|
|
|
def test_missing_config_raises(self, tmp_configs):
|
|
with pytest.raises(FileNotFoundError, match="Model config not found"):
|
|
load_model_config("nonexistent")
|
|
|
|
def test_empty_yaml_returns_empty_dict(self, tmp_configs):
|
|
(tmp_configs / "models" / "empty.yaml").write_text("")
|
|
result = load_model_config("empty")
|
|
assert result == {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# load_mode_config
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestLoadModeConfig:
|
|
def test_loads_valid_yaml(self, tmp_configs):
|
|
data = {
|
|
"agent": {"agent_class": "eval.agents.gortex_agent.GortexAgent"},
|
|
"environment": {"environment_class": "docker"},
|
|
}
|
|
_write_yaml(tmp_configs / "modes" / "baseline.yaml", data)
|
|
result = load_mode_config("baseline")
|
|
assert result == data
|
|
|
|
def test_missing_config_raises(self, tmp_configs):
|
|
with pytest.raises(FileNotFoundError, match="Mode config not found"):
|
|
load_mode_config("nonexistent")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# merge_configs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestMergeConfigs:
|
|
def test_mode_overrides_model_on_conflict(self):
|
|
model = {"model": {"model_name": "old"}, "shared": "model_val"}
|
|
mode = {"shared": "mode_val"}
|
|
result = merge_configs(model, mode)
|
|
assert result["shared"] == "mode_val"
|
|
assert result["model"]["model_name"] == "old"
|
|
|
|
def test_deep_merge_nested_dicts(self):
|
|
model = {"agent": {"step_limit": 30, "cost_limit": 3.0}}
|
|
mode = {"agent": {"step_limit": 50, "extra": True}}
|
|
result = merge_configs(model, mode)
|
|
assert result["agent"]["step_limit"] == 50
|
|
assert result["agent"]["cost_limit"] == 3.0
|
|
assert result["agent"]["extra"] is True
|
|
|
|
def test_unique_keys_preserved(self):
|
|
model = {"model": {"model_name": "test"}}
|
|
mode = {"agent": {"agent_class": "MyAgent"}}
|
|
result = merge_configs(model, mode)
|
|
assert result["model"]["model_name"] == "test"
|
|
assert result["agent"]["agent_class"] == "MyAgent"
|
|
|
|
def test_empty_configs(self):
|
|
assert merge_configs({}, {}) == {}
|
|
assert merge_configs({"a": 1}, {}) == {"a": 1}
|
|
assert merge_configs({}, {"b": 2}) == {"b": 2}
|
|
|
|
def test_scalar_override_replaces_dict(self):
|
|
model = {"key": {"nested": "value"}}
|
|
mode = {"key": "scalar"}
|
|
result = merge_configs(model, mode)
|
|
assert result["key"] == "scalar"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# validate_config
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestValidateConfig:
|
|
def test_valid_config_passes(self):
|
|
config = {
|
|
"model": {"model_name": "test"},
|
|
"agent": {"agent_class": "MyAgent"},
|
|
"environment": {"environment_class": "docker"},
|
|
}
|
|
validate_config(config) # should not raise
|
|
|
|
def test_missing_single_field(self):
|
|
config = {
|
|
"agent": {"agent_class": "MyAgent"},
|
|
"environment": {"environment_class": "docker"},
|
|
}
|
|
with pytest.raises(ValueError, match="model.model_name"):
|
|
validate_config(config)
|
|
|
|
def test_missing_multiple_fields(self):
|
|
with pytest.raises(ValueError) as exc_info:
|
|
validate_config({})
|
|
msg = str(exc_info.value)
|
|
assert "model.model_name" in msg
|
|
assert "agent.agent_class" in msg
|
|
assert "environment.environment_class" in msg
|
|
|
|
def test_missing_nested_key(self):
|
|
config = {
|
|
"model": {}, # model_name missing inside model dict
|
|
"agent": {"agent_class": "MyAgent"},
|
|
"environment": {"environment_class": "docker"},
|
|
}
|
|
with pytest.raises(ValueError, match="model.model_name"):
|
|
validate_config(config)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# list_configs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestListConfigs:
|
|
def test_discovers_yaml_files(self, tmp_configs):
|
|
_write_yaml(tmp_configs / "models" / "claude-sonnet.yaml", {"model": {}})
|
|
_write_yaml(tmp_configs / "models" / "claude-haiku.yaml", {"model": {}})
|
|
_write_yaml(tmp_configs / "modes" / "baseline.yaml", {"agent": {}})
|
|
_write_yaml(tmp_configs / "modes" / "native.yaml", {"agent": {}})
|
|
|
|
result = list_configs()
|
|
assert "claude-haiku" in result["models"]
|
|
assert "claude-sonnet" in result["models"]
|
|
assert "baseline" in result["modes"]
|
|
assert "native" in result["modes"]
|
|
|
|
def test_excludes_non_yaml_files(self, tmp_configs):
|
|
_write_yaml(tmp_configs / "models" / "valid.yaml", {"model": {}})
|
|
(tmp_configs / "models" / ".gitkeep").write_text("")
|
|
(tmp_configs / "models" / "readme.txt").write_text("not yaml")
|
|
|
|
result = list_configs()
|
|
assert result["models"] == ["valid"]
|
|
|
|
def test_empty_directories(self, tmp_configs):
|
|
result = list_configs()
|
|
assert result == {"models": [], "modes": []}
|
|
|
|
def test_returns_sorted_names(self, tmp_configs):
|
|
for name in ["zebra", "alpha", "middle"]:
|
|
_write_yaml(tmp_configs / "models" / f"{name}.yaml", {})
|
|
result = list_configs()
|
|
assert result["models"] == ["alpha", "middle", "zebra"]
|