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