137 lines
4.6 KiB
Python
137 lines
4.6 KiB
Python
"""Tests for config overlay system."""
|
|
import os
|
|
import json
|
|
import tempfile
|
|
import yaml
|
|
import pytest
|
|
|
|
|
|
def test_deep_merge_dicts():
|
|
"""Deep merge should recursively merge dicts."""
|
|
from config_overlay import deep_merge
|
|
base = {"a": {"b": 1, "c": 2}, "d": 3}
|
|
overlay = {"a": {"b": 10, "e": 5}}
|
|
result = deep_merge(base, overlay)
|
|
assert result == {"a": {"b": 10, "c": 2, "e": 5}, "d": 3}
|
|
|
|
|
|
def test_deep_merge_lists_replaced():
|
|
"""Lists should be replaced, not extended."""
|
|
from config_overlay import deep_merge
|
|
base = {"items": [1, 2, 3]}
|
|
overlay = {"items": [4, 5]}
|
|
result = deep_merge(base, overlay)
|
|
assert result == {"items": [4, 5]}
|
|
|
|
|
|
def test_deep_merge_scalars_overridden():
|
|
"""Scalar values should be overridden by overlay."""
|
|
from config_overlay import deep_merge
|
|
base = {"name": "base", "count": 10}
|
|
overlay = {"name": "override", "count": 20}
|
|
result = deep_merge(base, overlay)
|
|
assert result == {"name": "override", "count": 20}
|
|
|
|
|
|
def test_deep_merge_none_removes_key():
|
|
"""None in overlay should remove the key from base."""
|
|
from config_overlay import deep_merge
|
|
base = {"a": 1, "b": 2, "c": 3}
|
|
overlay = {"b": None}
|
|
result = deep_merge(base, overlay)
|
|
assert result == {"a": 1, "c": 3}
|
|
|
|
|
|
def test_deep_merge_empty_overlay():
|
|
"""Empty overlay should return base unchanged."""
|
|
from config_overlay import deep_merge
|
|
base = {"a": 1, "b": {"c": 2}}
|
|
result = deep_merge(base, {})
|
|
assert result == base
|
|
|
|
|
|
def test_load_config_no_env(tmp_path):
|
|
"""Load config without overlay should return base."""
|
|
from config_overlay import load_config
|
|
base = {"model": "test", "agent": {"max_turns": 10}}
|
|
path = tmp_path / "config.yaml"
|
|
path.write_text(yaml.dump(base))
|
|
result = load_config(str(path))
|
|
assert result == base
|
|
|
|
|
|
def test_load_config_with_overlay(tmp_path):
|
|
"""Load config with overlay should merge."""
|
|
from config_overlay import load_config
|
|
base = {"model": "base", "agent": {"max_turns": 10, "verbose": False}}
|
|
overlay = {"model": "override", "agent": {"verbose": True}}
|
|
(tmp_path / "config.yaml").write_text(yaml.dump(base))
|
|
(tmp_path / "config.dev.yaml").write_text(yaml.dump(overlay))
|
|
result = load_config(str(tmp_path / "config.yaml"), env="dev")
|
|
assert result["model"] == "override"
|
|
assert result["agent"]["max_turns"] == 10
|
|
assert result["agent"]["verbose"] is True
|
|
|
|
|
|
def test_load_config_missing_overlay(tmp_path):
|
|
"""Missing overlay should silently return base."""
|
|
from config_overlay import load_config
|
|
base = {"model": "base"}
|
|
(tmp_path / "config.yaml").write_text(yaml.dump(base))
|
|
result = load_config(str(tmp_path / "config.yaml"), env="nonexistent")
|
|
assert result == base
|
|
|
|
|
|
def test_find_config(tmp_path):
|
|
"""find_config should locate base and overlay."""
|
|
from config_overlay import find_config
|
|
base = tmp_path / "config.yaml"
|
|
base.write_text("a: 1")
|
|
overlay = tmp_path / "config.prod.yaml"
|
|
overlay.write_text("a: 2")
|
|
b, o = find_config(str(base), "prod")
|
|
assert b == base
|
|
assert o == overlay
|
|
|
|
|
|
def test_list_overlays(tmp_path):
|
|
"""list_overlays should find all overlay files."""
|
|
from config_overlay import list_overlays
|
|
(tmp_path / "config.yaml").write_text("a: 1")
|
|
(tmp_path / "config.dev.yaml").write_text("a: 2")
|
|
(tmp_path / "config.prod.yaml").write_text("a: 3")
|
|
overlays = list_overlays(str(tmp_path / "config.yaml"))
|
|
envs = [o['env'] for o in overlays]
|
|
assert 'dev' in envs
|
|
assert 'prod' in envs
|
|
|
|
|
|
def test_detect_env_from_var(tmp_path, monkeypatch):
|
|
"""detect_env should check TIMMY_ENV first."""
|
|
from config_overlay import detect_env
|
|
monkeypatch.setenv("TIMMY_ENV", "prod")
|
|
assert detect_env() == "prod"
|
|
|
|
|
|
def test_detect_env_fallback(tmp_path, monkeypatch):
|
|
"""detect_env should fall back through vars."""
|
|
from config_overlay import detect_env
|
|
monkeypatch.delenv("TIMMY_ENV", raising=False)
|
|
monkeypatch.delenv("HERMES_ENV", raising=False)
|
|
monkeypatch.setenv("ENVIRONMENT", "cron")
|
|
assert detect_env() == "cron"
|
|
|
|
|
|
def test_real_config_overlay():
|
|
"""Test against actual config files in the repo."""
|
|
import sys
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
from config_overlay import load_config
|
|
|
|
base_path = os.path.join(os.path.dirname(__file__), '..', 'config.yaml')
|
|
if os.path.exists(base_path):
|
|
config = load_config(base_path, env='dev')
|
|
assert config['model']['default'] == 'qwen3:30b' # dev overrides
|
|
assert config['agent']['max_turns'] == 50 # dev overrides
|
|
assert 'terminal' in config # base keys preserved
|