stock/tests/unit/test_config.py
2026-02-27 03:17:12 +08:00

279 lines
9.9 KiB
Python

"""Unit tests for configuration management system."""
import os
import tempfile
from pathlib import Path
import pytest
import yaml
from openclaw.core.config import (
ConfigLoader,
CostStructure,
LLMConfig,
OpenClawConfig,
SurvivalThresholds,
get_config,
reload_config,
)
class TestCostStructure:
"""Test CostStructure configuration model."""
def test_default_values(self) -> None:
"""Test default cost structure values."""
cost = CostStructure()
assert cost.llm_input_per_1m == 2.5
assert cost.llm_output_per_1m == 10.0
assert cost.market_data_per_call == 0.01
assert cost.trade_fee_rate == 0.001
def test_custom_values(self) -> None:
"""Test custom cost structure values."""
cost = CostStructure(
llm_input_per_1m=5.0,
llm_output_per_1m=20.0,
market_data_per_call=0.02,
trade_fee_rate=0.002,
)
assert cost.llm_input_per_1m == 5.0
assert cost.llm_output_per_1m == 20.0
assert cost.market_data_per_call == 0.02
assert cost.trade_fee_rate == 0.002
def test_validation_positive_values(self) -> None:
"""Test validation of positive values."""
with pytest.raises(ValueError):
CostStructure(llm_input_per_1m=-1.0)
with pytest.raises(ValueError):
CostStructure(llm_output_per_1m=0)
with pytest.raises(ValueError):
CostStructure(market_data_per_call=-0.01)
with pytest.raises(ValueError):
CostStructure(trade_fee_rate=-0.001)
with pytest.raises(ValueError):
CostStructure(trade_fee_rate=1.5)
class TestSurvivalThresholds:
"""Test SurvivalThresholds configuration model."""
def test_default_values(self) -> None:
"""Test default survival threshold values."""
thresholds = SurvivalThresholds()
assert thresholds.thriving_multiplier == 3.0
assert thresholds.stable_multiplier == 1.5
assert thresholds.struggling_multiplier == 0.8
assert thresholds.bankrupt_multiplier == 0.1
def test_threshold_order(self) -> None:
"""Test that thresholds are in correct order."""
thresholds = SurvivalThresholds()
assert thresholds.thriving_multiplier > thresholds.stable_multiplier
assert thresholds.stable_multiplier > thresholds.struggling_multiplier
assert thresholds.struggling_multiplier > thresholds.bankrupt_multiplier
def test_validation_greater_than_one(self) -> None:
"""Test thriving multiplier must be > 1."""
with pytest.raises(ValueError):
SurvivalThresholds(thriving_multiplier=0.5)
with pytest.raises(ValueError):
SurvivalThresholds(thriving_multiplier=1.0)
class TestLLMConfig:
"""Test LLMConfig configuration model."""
def test_default_values(self) -> None:
"""Test default LLM configuration values."""
config = LLMConfig()
assert config.model == "gpt-4o"
assert config.temperature == 0.7
assert config.timeout == 30
assert config.api_key is None
assert config.base_url is None
def test_custom_values(self) -> None:
"""Test custom LLM configuration values."""
config = LLMConfig(
model="claude-3-5-sonnet",
temperature=0.5,
timeout=60,
api_key="test-key",
base_url="https://api.example.com",
)
assert config.model == "claude-3-5-sonnet"
assert config.temperature == 0.5
assert config.timeout == 60
assert config.api_key == "test-key"
assert config.base_url == "https://api.example.com"
def test_temperature_validation(self) -> None:
"""Test temperature must be between 0 and 2."""
with pytest.raises(ValueError):
LLMConfig(temperature=-0.1)
with pytest.raises(ValueError):
LLMConfig(temperature=2.1)
class TestOpenClawConfig:
"""Test OpenClawConfig main configuration model."""
def test_default_initialization(self) -> None:
"""Test default configuration initialization."""
config = OpenClawConfig()
assert config.initial_capital["trader"] == 10000.0
assert config.initial_capital["analyst"] == 5000.0
assert config.cost_structure.llm_input_per_1m == 2.5
assert config.simulation_days == 30
assert config.log_level == "INFO"
def test_initial_capital_validation(self) -> None:
"""Test initial capital must be positive."""
with pytest.raises(ValueError):
OpenClawConfig(initial_capital={"trader": -1000.0})
with pytest.raises(ValueError):
OpenClawConfig(initial_capital={"analyst": 0.0})
def test_nested_models(self) -> None:
"""Test nested configuration models."""
config = OpenClawConfig(
cost_structure=CostStructure(llm_input_per_1m=5.0),
survival_thresholds=SurvivalThresholds(thriving_multiplier=4.0),
)
assert config.cost_structure.llm_input_per_1m == 5.0
assert config.survival_thresholds.thriving_multiplier == 4.0
class TestConfigLoading:
"""Test configuration loading from files."""
def test_load_from_yaml(self, tmp_path: Path) -> None:
"""Test loading configuration from YAML file."""
config_file = tmp_path / "test_config.yaml"
config_data = {
"initial_capital": {"trader": 20000.0, "analyst": 10000.0},
"simulation_days": 60,
"log_level": "DEBUG",
}
config_file.write_text(yaml.dump(config_data))
config = ConfigLoader.load(config_file)
assert config.initial_capital["trader"] == 20000.0
assert config.initial_capital["analyst"] == 10000.0
assert config.simulation_days == 60
assert config.log_level == "DEBUG"
def test_load_from_json(self, tmp_path: Path) -> None:
"""Test loading configuration from JSON file."""
config_file = tmp_path / "test_config.json"
config_data = {
"initial_capital": {"trader": 15000.0},
"cost_structure": {"llm_input_per_1m": 3.0},
}
import json
config_file.write_text(json.dumps(config_data))
config = ConfigLoader.load(config_file)
assert config.initial_capital["trader"] == 15000.0
assert config.cost_structure.llm_input_per_1m == 3.0
def test_load_nonexistent_file(self) -> None:
"""Test loading from non-existent file raises FileNotFoundError."""
with pytest.raises(FileNotFoundError):
ConfigLoader.load("/nonexistent/config.yaml")
def test_create_default_config(self, tmp_path: Path) -> None:
"""Test creating default configuration file."""
output_path = tmp_path / "default_config.yaml"
path = ConfigLoader.create_default_config(output_path)
assert path.exists()
# Verify it can be loaded
config = ConfigLoader.load(path)
assert config.initial_capital["trader"] == 10000.0
class TestEnvironmentVariables:
"""Test environment variable overrides."""
def test_env_prefix_filtering(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that environment variables with correct prefix are used."""
# This tests the SettingsConfigDict env_prefix behavior
monkeypatch.setenv("OPENCLAW_SIMULATION_DAYS", "100")
monkeypatch.setenv("OPENCLAW_LOG_LEVEL", "ERROR")
# Reload to pick up environment variables
config = reload_config()
assert config.simulation_days == 100
assert config.log_level == "ERROR"
def test_env_nested_values(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test environment variables for nested config values."""
# Nested values should be accessible via double underscore
monkeypatch.setenv("OPENCLAW_COST_STRUCTURE__LLM_INPUT_PER_1M", "5.5")
config = reload_config()
assert config.cost_structure.llm_input_per_1m == 5.5
class TestGlobalConfig:
"""Test global configuration instance."""
def test_get_config_singleton(self) -> None:
"""Test that get_config returns the same instance."""
config1 = get_config()
config2 = get_config()
assert config1 is config2
def test_reload_config_updates_global(self) -> None:
"""Test that reload_config updates the global instance."""
# First clear any existing config
from openclaw.core.config import set_config
set_config(None)
config1 = get_config()
config2 = reload_config()
# reload_config creates a new instance and updates the global
# config1 is the old instance, config2 is the new global instance
assert config1 is not config2 # Different instances
assert config2 is get_config() # But config2 is now the global
class TestConfigValidation:
"""Test configuration validation edge cases."""
def test_empty_initial_capital(self) -> None:
"""Test empty initial capital dict."""
config = OpenClawConfig(initial_capital={})
# Should use empty dict, not defaults
assert config.initial_capital == {}
def test_partial_config_file(self, tmp_path: Path) -> None:
"""Test loading partial configuration from file."""
config_file = tmp_path / "partial.yaml"
config_file.write_text("simulation_days: 45\n")
config = ConfigLoader.load(config_file)
assert config.simulation_days == 45
# Other values should use defaults
assert config.initial_capital["trader"] == 10000.0
def test_invalid_yaml_raises_error(self, tmp_path: Path) -> None:
"""Test that invalid YAML raises ValueError."""
config_file = tmp_path / "invalid.yaml"
config_file.write_text("invalid: yaml: content: [")
# Should raise ValueError for invalid YAML
with pytest.raises(ValueError, match="Invalid YAML"):
ConfigLoader.load(config_file)