feat: add main merge-and-validate entry point with error formatting

This commit is contained in:
2025-11-01 17:11:56 -04:00
parent c220211c3a
commit 7d66f90810
2 changed files with 198 additions and 0 deletions

View File

@@ -174,3 +174,120 @@ def test_validate_config_end_before_init():
with pytest.raises(ConfigValidationError, match="init_date must be <= end_date"): with pytest.raises(ConfigValidationError, match="init_date must be <= end_date"):
validate_config(config) validate_config(config)
import os
from tools.config_merger import merge_and_validate
def test_merge_and_validate_success(tmp_path, monkeypatch):
"""Test successful merge and validation"""
# Create default config
default_config = {
"agent_type": "BaseAgent",
"models": [{"name": "default", "basemodel": "openai/gpt-4", "signature": "default", "enabled": True}],
"agent_config": {"max_steps": 30, "max_retries": 3, "initial_cash": 10000.0},
"log_config": {"log_path": "./data"}
}
default_path = tmp_path / "default_config.json"
with open(default_path, 'w') as f:
json.dump(default_config, f)
# Create custom config (only overrides models)
custom_config = {
"models": [{"name": "custom", "basemodel": "openai/gpt-5", "signature": "custom", "enabled": True}]
}
custom_path = tmp_path / "config.json"
with open(custom_path, 'w') as f:
json.dump(custom_config, f)
output_path = tmp_path / "runtime_config.json"
# Mock file paths
monkeypatch.setattr("tools.config_merger.DEFAULT_CONFIG_PATH", str(default_path))
monkeypatch.setattr("tools.config_merger.CUSTOM_CONFIG_PATH", str(custom_path))
monkeypatch.setattr("tools.config_merger.OUTPUT_CONFIG_PATH", str(output_path))
# Run merge and validate
merge_and_validate()
# Verify output file was created
assert output_path.exists()
# Verify merged content
with open(output_path, 'r') as f:
result = json.load(f)
assert result["models"] == [{"name": "custom", "basemodel": "openai/gpt-5", "signature": "custom", "enabled": True}]
assert result["agent_config"] == {"max_steps": 30, "max_retries": 3, "initial_cash": 10000.0}
def test_merge_and_validate_no_custom_config(tmp_path, monkeypatch):
"""Test when no custom config exists (uses default only)"""
default_config = {
"agent_type": "BaseAgent",
"models": [{"name": "default", "basemodel": "openai/gpt-4", "signature": "default", "enabled": True}],
"agent_config": {"max_steps": 30, "max_retries": 3, "initial_cash": 10000.0},
"log_config": {"log_path": "./data"}
}
default_path = tmp_path / "default_config.json"
with open(default_path, 'w') as f:
json.dump(default_config, f)
custom_path = tmp_path / "config.json" # Does not exist
output_path = tmp_path / "runtime_config.json"
monkeypatch.setattr("tools.config_merger.DEFAULT_CONFIG_PATH", str(default_path))
monkeypatch.setattr("tools.config_merger.CUSTOM_CONFIG_PATH", str(custom_path))
monkeypatch.setattr("tools.config_merger.OUTPUT_CONFIG_PATH", str(output_path))
merge_and_validate()
# Verify output matches default
with open(output_path, 'r') as f:
result = json.load(f)
assert result == default_config
def test_merge_and_validate_validation_fails(tmp_path, monkeypatch, capsys):
"""Test validation failure exits with error"""
default_config = {
"agent_type": "BaseAgent",
"models": [{"name": "default", "basemodel": "openai/gpt-4", "signature": "default", "enabled": True}],
"agent_config": {"max_steps": 30, "max_retries": 3, "initial_cash": 10000.0},
"log_config": {"log_path": "./data"}
}
default_path = tmp_path / "default_config.json"
with open(default_path, 'w') as f:
json.dump(default_config, f)
# Custom config with no enabled models
custom_config = {
"models": [{"name": "custom", "basemodel": "openai/gpt-5", "signature": "custom", "enabled": False}]
}
custom_path = tmp_path / "config.json"
with open(custom_path, 'w') as f:
json.dump(custom_config, f)
output_path = tmp_path / "runtime_config.json"
monkeypatch.setattr("tools.config_merger.DEFAULT_CONFIG_PATH", str(default_path))
monkeypatch.setattr("tools.config_merger.CUSTOM_CONFIG_PATH", str(custom_path))
monkeypatch.setattr("tools.config_merger.OUTPUT_CONFIG_PATH", str(output_path))
# Should exit with error
with pytest.raises(SystemExit) as exc_info:
merge_and_validate()
assert exc_info.value.code == 1
# Check error output (should be in stderr, not stdout)
captured = capsys.readouterr()
assert "CONFIG VALIDATION FAILED" in captured.err
assert "At least one model must be enabled" in captured.err

View File

@@ -145,3 +145,84 @@ def validate_config(config: Dict[str, Any]) -> None:
raise ConfigValidationError( raise ConfigValidationError(
f"init_date must be <= end_date (got {date_range['init_date']} > {date_range['end_date']})" f"init_date must be <= end_date (got {date_range['init_date']} > {date_range['end_date']})"
) )
# File path constants (can be overridden for testing)
DEFAULT_CONFIG_PATH = "configs/default_config.json"
CUSTOM_CONFIG_PATH = "user-configs/config.json"
OUTPUT_CONFIG_PATH = "/tmp/runtime_config.json"
def format_error_message(error: str, location: str, file: str) -> str:
"""Format validation error for display."""
border = "" * 60
return f"""
❌ CONFIG VALIDATION FAILED
{border}
Error: {error}
Location: {location}
File: {file}
Merged config written to: {OUTPUT_CONFIG_PATH} (for debugging)
Container will exit. Fix config and restart.
"""
def merge_and_validate() -> None:
"""
Main entry point for config merging and validation.
Loads default config, optionally merges custom config,
validates the result, and writes to output path.
Exits with code 1 on any error.
"""
try:
# Load default config
print(f"📄 Loading default config from {DEFAULT_CONFIG_PATH}")
default_config = load_config(DEFAULT_CONFIG_PATH)
# Load custom config if exists
custom_config = {}
if Path(CUSTOM_CONFIG_PATH).exists():
print(f"📝 Loading custom config from {CUSTOM_CONFIG_PATH}")
custom_config = load_config(CUSTOM_CONFIG_PATH)
else:
print(f" No custom config found at {CUSTOM_CONFIG_PATH}, using defaults")
# Merge configs
print("🔧 Merging configurations...")
merged_config = merge_configs(default_config, custom_config)
# Write merged config (for debugging even if validation fails)
output_path = Path(OUTPUT_CONFIG_PATH)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w') as f:
json.dump(merged_config, f, indent=2)
# Validate merged config
print("✅ Validating merged configuration...")
validate_config(merged_config)
print(f"✅ Configuration validated successfully")
print(f"📦 Merged config written to {OUTPUT_CONFIG_PATH}")
except ConfigValidationError as e:
# Determine which file caused the error
error_file = CUSTOM_CONFIG_PATH if Path(CUSTOM_CONFIG_PATH).exists() else DEFAULT_CONFIG_PATH
error_msg = format_error_message(
error=str(e),
location="Root level",
file=error_file
)
print(error_msg, file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"❌ Unexpected error during config processing: {e}", file=sys.stderr)
sys.exit(1)