mirror of
https://github.com/Xe138/AI-Trader.git
synced 2026-04-01 17:17:24 -04:00
feat: add main merge-and-validate entry point with error formatting
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user