From 7e95ce356bd1c74026d984b70a32c57f4b13b120 Mon Sep 17 00:00:00 2001 From: Bill Date: Sat, 1 Nov 2025 16:59:02 -0400 Subject: [PATCH] feat: add root-level config merging Add merge_configs function that performs root-level merging of custom config into default config. Custom config sections completely replace default sections. Implementation does not mutate input dictionaries. Includes comprehensive tests for: - Empty custom config - Section override behavior - Adding new sections - Non-mutating behavior All 7 tests pass. --- tests/unit/test_config_merger.py | 43 +++++++++++++++++++++++++++++++- tools/config_merger.py | 22 ++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_config_merger.py b/tests/unit/test_config_merger.py index 9ea7ace..1e2682b 100644 --- a/tests/unit/test_config_merger.py +++ b/tests/unit/test_config_merger.py @@ -2,7 +2,7 @@ import pytest import json import tempfile from pathlib import Path -from tools.config_merger import load_config, ConfigValidationError +from tools.config_merger import load_config, ConfigValidationError, merge_configs def test_load_config_valid_json(): @@ -35,3 +35,44 @@ def test_load_config_invalid_json(): load_config(temp_path) finally: Path(temp_path).unlink() + + +def test_merge_configs_empty_custom(): + """Test merge with no custom config""" + default = {"a": 1, "b": 2} + custom = {} + result = merge_configs(default, custom) + assert result == {"a": 1, "b": 2} + + +def test_merge_configs_override_section(): + """Test custom config overrides entire sections""" + default = { + "models": [{"name": "default-model", "enabled": True}], + "agent_config": {"max_steps": 30} + } + custom = { + "models": [{"name": "custom-model", "enabled": False}] + } + result = merge_configs(default, custom) + + assert result["models"] == [{"name": "custom-model", "enabled": False}] + assert result["agent_config"] == {"max_steps": 30} + + +def test_merge_configs_add_new_section(): + """Test custom config adds new sections""" + default = {"a": 1} + custom = {"b": 2} + result = merge_configs(default, custom) + assert result == {"a": 1, "b": 2} + + +def test_merge_configs_does_not_mutate_inputs(): + """Test merge doesn't modify original dicts""" + default = {"a": 1} + custom = {"a": 2} + result = merge_configs(default, custom) + + assert default["a"] == 1 # Original unchanged + assert result["a"] == 2 diff --git a/tools/config_merger.py b/tools/config_merger.py index b79569c..3024c82 100644 --- a/tools/config_merger.py +++ b/tools/config_merger.py @@ -34,3 +34,25 @@ def load_config(path: str) -> Dict[str, Any]: return json.load(f) except json.JSONDecodeError as e: raise ConfigValidationError(f"Invalid JSON in {path}: {e}") + + +def merge_configs(default: Dict[str, Any], custom: Dict[str, Any]) -> Dict[str, Any]: + """ + Merge custom config into default config (root-level override). + + Custom config sections completely replace default sections. + Does not mutate input dictionaries. + + Args: + default: Default configuration dict + custom: Custom configuration dict (overrides) + + Returns: + Merged configuration dict + """ + merged = dict(default) # Shallow copy + + for key, value in custom.items(): + merged[key] = value + + return merged