diff --git a/tests/test_config_template.py b/tests/test_config_template.py new file mode 100644 index 00000000..943c806d --- /dev/null +++ b/tests/test_config_template.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Tests for config_template.py — issue #696""" +import os +import sys +import tempfile +import pytest +from pathlib import Path + +# Add parent dir for import +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) +from config_template import ConfigTemplate, _deep_merge, _get_dotted, _diff_dicts, _validate_config + + +@pytest.fixture +def tmp_config_dir(tmp_path): + """Create a temp config dir with base + overlay files.""" + import yaml + base = { + "model": {"name": "base-model", "temperature": 0.7}, + "cron": {"enabled": False, "interval": 300}, + "display": {"colors": True}, + } + overlay = { + "model": {"temperature": 0.9}, + "cron": {"enabled": True}, + "logging": {"level": "DEBUG"}, + } + with open(tmp_path / "base.yaml", "w") as f: + yaml.dump(base, f) + with open(tmp_path / "test.overlay.yaml", "w") as f: + yaml.dump(overlay, f) + return tmp_path + + +class TestDeepMerge: + def test_overlay_wins(self): + base = {"a": 1, "b": 2} + overlay = {"b": 99} + result = _deep_merge(base, overlay) + assert result == {"a": 1, "b": 99} + + def test_deep_merge_nested(self): + base = {"model": {"name": "x", "temp": 0.7}} + overlay = {"model": {"temp": 0.9}} + result = _deep_merge(base, overlay) + assert result["model"]["name"] == "x" + assert result["model"]["temp"] == 0.9 + + def test_new_keys_added(self): + base = {"a": 1} + overlay = {"b": 2} + result = _deep_merge(base, overlay) + assert result == {"a": 1, "b": 2} + + def test_originals_unchanged(self): + base = {"a": {"inner": 1}} + overlay = {"a": {"inner": 99}} + _deep_merge(base, overlay) + assert base["a"]["inner"] == 1 + + +class TestDottedAccess: + def test_simple_key(self): + assert _get_dotted({"a": 1}, "a") == 1 + + def test_nested_key(self): + assert _get_dotted({"a": {"b": {"c": 42}}}, "a.b.c") == 42 + + def test_missing_key_returns_default(self): + assert _get_dotted({"a": 1}, "x", "fallback") == "fallback" + + def test_partial_path(self): + assert _get_dotted({"a": 1}, "a.b.c", None) is None + + +class TestDiff: + def test_no_diff(self): + assert _diff_dicts({"a": 1}, {"a": 1}) == [] + + def test_changed_value(self): + diffs = _diff_dicts({"a": 1}, {"a": 2}) + assert len(diffs) == 1 + assert diffs[0]["type"] == "changed" + + def test_added_key(self): + diffs = _diff_dicts({"a": 1}, {"a": 1, "b": 2}) + added = [d for d in diffs if d["type"] == "added_in_overlay"] + assert len(added) == 1 + assert added[0]["key"] == "b" + + +class TestValidation: + def test_valid_config(self): + config = {"model": {"name": "x"}, "provider": {"name": "y"}} + assert _validate_config(config) == [] + + def test_missing_model(self): + warnings = _validate_config({"provider": {}}) + assert any("model" in w for w in warnings) + + +class TestConfigTemplate: + def test_load(self, tmp_config_dir): + t = ConfigTemplate(str(tmp_config_dir)) + merged = t.load("test") + assert merged["model"]["name"] == "base-model" + assert merged["model"]["temperature"] == 0.9 + assert merged["cron"]["enabled"] is True + assert merged["logging"]["level"] == "DEBUG" + + def test_get_dotted(self, tmp_config_dir): + t = ConfigTemplate(str(tmp_config_dir)) + t.load("test") + assert t.get("model.temperature") == 0.9 + assert t.get("nonexistent", "default") == "default" + + def test_diff(self, tmp_config_dir): + t = ConfigTemplate(str(tmp_config_dir)) + t.load("test") + diffs = t.diff() + assert len(diffs) > 0 + + def test_unknown_env_raises(self, tmp_config_dir): + t = ConfigTemplate(str(tmp_config_dir)) + with pytest.raises(ValueError, match="Unknown environment"): + t.load("nonexistent") + + def test_list_environments(self): + envs = ConfigTemplate.list_environments() + assert "dev" in envs + assert "prod" in envs + assert "cron" in envs + assert "gateway" in envs