| """Tests for the unified configuration system.""" |
|
|
| import os |
| import tempfile |
| from pathlib import Path |
| from unittest.mock import patch |
|
|
| import pytest |
| import yaml |
|
|
| from openra_env.config import ( |
| TOOL_CATEGORIES, |
| AlertPromptsConfig, |
| AlertsConfig, |
| AgentConfig, |
| CompressionConfig, |
| GameConfig, |
| LLMConfig, |
| OpenRARLConfig, |
| OpponentConfig, |
| PlanningConfig, |
| PromptsConfig, |
| RewardConfig, |
| RewardVectorConfig, |
| ToolCategoriesConfig, |
| ToolsConfig, |
| _coerce_value, |
| _deep_merge, |
| _set_nested, |
| load_config, |
| should_register_tool, |
| ) |
|
|
|
|
| |
|
|
|
|
| class TestDefaults: |
| def test_default_config_has_sane_values(self): |
| cfg = OpenRARLConfig() |
| assert cfg.game.mod == "ra" |
| assert cfg.game.grpc_port == 9999 |
| assert cfg.opponent.bot_type == "easy" |
| assert cfg.planning.enabled is True |
| assert cfg.reward.victory == 1.0 |
| assert cfg.llm.model == "qwen/qwen3-coder-next" |
| assert cfg.agent.max_time_s == 1800 |
|
|
| def test_all_tool_categories_enabled_by_default(self): |
| cfg = OpenRARLConfig() |
| cats = cfg.tools.categories |
| for field in ToolCategoriesConfig.model_fields: |
| assert getattr(cats, field) is True, f"Category {field} should default to True" |
|
|
| def test_all_alerts_enabled_by_default(self): |
| cfg = OpenRARLConfig() |
| for field in AlertsConfig.model_fields: |
| if field == "max_alerts": |
| continue |
| assert getattr(cfg.alerts, field) is True, f"Alert {field} should default to True" |
|
|
| def test_disabled_tools_list_empty_by_default(self): |
| cfg = OpenRARLConfig() |
| assert cfg.tools.disabled == [] |
|
|
| def test_load_config_no_file_returns_defaults(self): |
| """load_config() with no file and no env vars should return defaults.""" |
| with _clean_env(): |
| cfg = load_config(config_path="__nonexistent__.yaml") |
| assert cfg.game.mod == "ra" |
| assert cfg.llm.base_url == "https://openrouter.ai/api/v1/chat/completions" |
|
|
|
|
| |
|
|
|
|
| class TestYAMLLoading: |
| def test_load_from_yaml(self): |
| data = {"game": {"mod": "cnc", "grpc_port": 5555}, "opponent": {"bot_type": "hard"}} |
| with _temp_yaml(data) as path, _clean_env(): |
| cfg = load_config(config_path=path) |
| assert cfg.game.mod == "cnc" |
| assert cfg.game.grpc_port == 5555 |
| assert cfg.opponent.bot_type == "hard" |
| |
| assert cfg.game.map_name == "singles.oramap" |
|
|
| def test_partial_yaml_merges_with_defaults(self): |
| data = {"reward": {"victory": 5.0}} |
| with _temp_yaml(data) as path, _clean_env(): |
| cfg = load_config(config_path=path) |
| assert cfg.reward.victory == 5.0 |
| assert cfg.reward.defeat == -1.0 |
|
|
| def test_empty_yaml_returns_defaults(self): |
| with _temp_yaml({}) as path, _clean_env(): |
| cfg = load_config(config_path=path) |
| assert cfg.game.mod == "ra" |
|
|
| def test_llm_config_from_yaml(self): |
| data = { |
| "llm": { |
| "base_url": "http://localhost:11434/v1/chat/completions", |
| "model": "llama3.1:70b", |
| "api_key": "", |
| "extra_headers": {}, |
| } |
| } |
| with _temp_yaml(data) as path, _clean_env(): |
| cfg = load_config(config_path=path) |
| assert cfg.llm.base_url == "http://localhost:11434/v1/chat/completions" |
| assert cfg.llm.model == "llama3.1:70b" |
| assert cfg.llm.api_key == "" |
| assert cfg.llm.extra_headers == {} |
|
|
|
|
| |
|
|
|
|
| class TestEnvVarPrecedence: |
| def test_env_var_overrides_yaml(self): |
| data = {"opponent": {"bot_type": "easy"}} |
| with _temp_yaml(data) as path: |
| with _clean_env(BOT_TYPE="hard"): |
| cfg = load_config(config_path=path) |
| assert cfg.opponent.bot_type == "hard" |
|
|
| def test_env_var_overrides_default(self): |
| with _clean_env(BOT_TYPE="hard"): |
| cfg = load_config(config_path="__nonexistent__.yaml") |
| assert cfg.opponent.bot_type == "hard" |
|
|
| def test_openra_path_env(self): |
| with _clean_env(OPENRA_PATH="/custom/openra"): |
| cfg = load_config(config_path="__nonexistent__.yaml") |
| assert cfg.game.openra_path == "/custom/openra" |
|
|
| def test_planning_enabled_env(self): |
| with _clean_env(PLANNING_ENABLED="false"): |
| cfg = load_config(config_path="__nonexistent__.yaml") |
| assert cfg.planning.enabled is False |
|
|
| def test_record_replays_env(self): |
| with _clean_env(RECORD_REPLAYS="yes"): |
| cfg = load_config(config_path="__nonexistent__.yaml") |
| assert cfg.game.record_replays is True |
|
|
| def test_openrouter_api_key_env(self): |
| with _clean_env(OPENROUTER_API_KEY="sk-test-123"): |
| cfg = load_config(config_path="__nonexistent__.yaml") |
| assert cfg.llm.api_key == "sk-test-123" |
|
|
| def test_llm_api_key_overrides_openrouter(self): |
| """LLM_API_KEY should take precedence over OPENROUTER_API_KEY.""" |
| with _clean_env(OPENROUTER_API_KEY="sk-old", LLM_API_KEY="sk-new"): |
| cfg = load_config(config_path="__nonexistent__.yaml") |
| assert cfg.llm.api_key == "sk-new" |
|
|
| def test_llm_base_url_env(self): |
| with _clean_env(LLM_BASE_URL="http://localhost:1234/v1/chat/completions"): |
| cfg = load_config(config_path="__nonexistent__.yaml") |
| assert cfg.llm.base_url == "http://localhost:1234/v1/chat/completions" |
|
|
| def test_llm_model_env(self): |
| with _clean_env(LLM_MODEL="my-local-model"): |
| cfg = load_config(config_path="__nonexistent__.yaml") |
| assert cfg.llm.model == "my-local-model" |
|
|
| def test_max_time_env(self): |
| with _clean_env(MAX_TIME="3600"): |
| cfg = load_config(config_path="__nonexistent__.yaml") |
| assert cfg.agent.max_time_s == 3600 |
|
|
|
|
| |
|
|
|
|
| class TestOverridePrecedence: |
| def test_overrides_beat_yaml(self): |
| data = {"game": {"mod": "cnc"}} |
| with _temp_yaml(data) as path, _clean_env(): |
| cfg = load_config(config_path=path, game={"mod": "d2k"}) |
| assert cfg.game.mod == "d2k" |
|
|
| def test_env_beats_overrides(self): |
| with _clean_env(BOT_TYPE="hard"): |
| cfg = load_config(config_path="__nonexistent__.yaml", opponent={"bot_type": "easy"}) |
| assert cfg.opponent.bot_type == "hard" |
|
|
| def test_cli_overrides_beat_env(self): |
| """Explicit CLI flags should beat environment variables.""" |
| with _clean_env(OPENROUTER_MODEL="env-model"): |
| cfg = load_config( |
| config_path="__nonexistent__.yaml", |
| cli_overrides={"llm": {"model": "cli-model"}}, |
| ) |
| assert cfg.llm.model == "cli-model" |
|
|
| def test_cli_overrides_beat_yaml_and_env(self): |
| data = {"llm": {"model": "yaml-model"}} |
| with _temp_yaml(data) as path, _clean_env(LLM_MODEL="env-model"): |
| cfg = load_config( |
| config_path=path, |
| cli_overrides={"llm": {"model": "cli-model"}}, |
| ) |
| assert cfg.llm.model == "cli-model" |
|
|
|
|
| |
|
|
|
|
| class TestCoercion: |
| @pytest.mark.parametrize("val,expected", [ |
| ("true", True), ("True", True), ("TRUE", True), |
| ("1", True), ("yes", True), ("Yes", True), |
| ("false", False), ("False", False), ("FALSE", False), |
| ("0", False), ("no", False), ("No", False), |
| ]) |
| def test_bool_coercion(self, val, expected): |
| assert _coerce_value(val) is expected |
|
|
| def test_int_coercion(self): |
| assert _coerce_value("42") == 42 |
| assert isinstance(_coerce_value("42"), int) |
|
|
| def test_float_coercion(self): |
| assert _coerce_value("3.14") == 3.14 |
| assert isinstance(_coerce_value("3.14"), float) |
|
|
| def test_string_passthrough(self): |
| assert _coerce_value("hello") == "hello" |
|
|
|
|
| |
|
|
|
|
| class TestDeepMerge: |
| def test_simple_merge(self): |
| base = {"a": 1, "b": 2} |
| _deep_merge(base, {"b": 3, "c": 4}) |
| assert base == {"a": 1, "b": 3, "c": 4} |
|
|
| def test_nested_merge(self): |
| base = {"game": {"mod": "ra", "port": 9999}} |
| _deep_merge(base, {"game": {"mod": "cnc"}}) |
| assert base == {"game": {"mod": "cnc", "port": 9999}} |
|
|
| def test_override_replaces_non_dict(self): |
| base = {"a": {"b": 1}} |
| _deep_merge(base, {"a": "flat"}) |
| assert base == {"a": "flat"} |
|
|
|
|
| class TestSetNested: |
| def test_single_level(self): |
| d: dict = {} |
| _set_nested(d, "key", "val") |
| assert d == {"key": "val"} |
|
|
| def test_multi_level(self): |
| d: dict = {} |
| _set_nested(d, "game.mod", "cnc") |
| assert d == {"game": {"mod": "cnc"}} |
|
|
| def test_preserves_siblings(self): |
| d = {"game": {"mod": "ra", "port": 9999}} |
| _set_nested(d, "game.mod", "cnc") |
| assert d == {"game": {"mod": "cnc", "port": 9999}} |
|
|
|
|
| |
|
|
|
|
| class TestToolFiltering: |
| def test_all_tools_enabled_by_default(self): |
| cfg = ToolsConfig() |
| for tool_name in TOOL_CATEGORIES: |
| assert should_register_tool(tool_name, cfg) is True |
|
|
| def test_disable_category(self): |
| cfg = ToolsConfig(categories=ToolCategoriesConfig(knowledge=False)) |
| assert should_register_tool("lookup_unit", cfg) is False |
| assert should_register_tool("lookup_building", cfg) is False |
| assert should_register_tool("lookup_tech_tree", cfg) is False |
| assert should_register_tool("lookup_faction", cfg) is False |
| |
| assert should_register_tool("advance", cfg) is True |
| assert should_register_tool("move_units", cfg) is True |
|
|
| def test_disable_individual_tool(self): |
| cfg = ToolsConfig(disabled=["surrender", "sell_building"]) |
| assert should_register_tool("surrender", cfg) is False |
| assert should_register_tool("sell_building", cfg) is False |
| |
| assert should_register_tool("get_replay_path", cfg) is True |
|
|
| def test_disabled_list_overrides_category_enable(self): |
| cfg = ToolsConfig( |
| categories=ToolCategoriesConfig(movement=True), |
| disabled=["move_units"], |
| ) |
| assert should_register_tool("move_units", cfg) is False |
| assert should_register_tool("attack_move", cfg) is True |
|
|
| def test_unknown_tool_defaults_to_enabled(self): |
| cfg = ToolsConfig() |
| assert should_register_tool("some_future_tool", cfg) is True |
|
|
| def test_all_tools_have_categories(self): |
| """Every tool in TOOL_CATEGORIES should map to a valid category field.""" |
| valid_categories = set(ToolCategoriesConfig.model_fields.keys()) |
| for tool_name, category in TOOL_CATEGORIES.items(): |
| assert category in valid_categories, f"Tool {tool_name} maps to unknown category {category}" |
|
|
| def test_tool_count(self): |
| """Verify we have all 48 tools mapped.""" |
| assert len(TOOL_CATEGORIES) == 48 |
|
|
|
|
| |
|
|
|
|
| class TestPlanningSync: |
| def test_planning_disabled_auto_disables_planning_tools(self): |
| cfg = OpenRARLConfig(planning=PlanningConfig(enabled=False)) |
| assert cfg.tools.categories.planning is False |
|
|
| def test_planning_enabled_keeps_planning_tools(self): |
| cfg = OpenRARLConfig(planning=PlanningConfig(enabled=True)) |
| assert cfg.tools.categories.planning is True |
|
|
| def test_planning_disabled_via_yaml(self): |
| data = {"planning": {"enabled": False}} |
| with _temp_yaml(data) as path, _clean_env(): |
| cfg = load_config(config_path=path) |
| assert cfg.planning.enabled is False |
| assert cfg.tools.categories.planning is False |
|
|
|
|
| |
|
|
|
|
| class TestLLMConfig: |
| def test_local_model_no_key(self): |
| cfg = LLMConfig( |
| base_url="http://localhost:11434/v1/chat/completions", |
| api_key="", |
| model="llama3.1:70b", |
| ) |
| assert cfg.api_key == "" |
| assert "localhost" in cfg.base_url |
|
|
| def test_remote_model_with_key(self): |
| cfg = LLMConfig(api_key="sk-test-123") |
| assert cfg.api_key == "sk-test-123" |
|
|
| def test_extra_headers_default(self): |
| cfg = LLMConfig() |
| assert "HTTP-Referer" in cfg.extra_headers |
| assert "X-Title" in cfg.extra_headers |
|
|
| def test_extra_headers_empty_for_local(self): |
| cfg = LLMConfig(extra_headers={}) |
| assert cfg.extra_headers == {} |
|
|
| def test_temperature_default_none(self): |
| cfg = LLMConfig() |
| assert cfg.temperature is None |
|
|
| def test_temperature_set(self): |
| cfg = LLMConfig(temperature=0.7) |
| assert cfg.temperature == 0.7 |
|
|
|
|
| |
|
|
|
|
| class TestAlertConfig: |
| def test_disable_specific_alerts(self): |
| cfg = AlertsConfig(under_attack=False, low_power=False) |
| assert cfg.under_attack is False |
| assert cfg.low_power is False |
| assert cfg.damaged_building is True |
|
|
|
|
| |
|
|
|
|
| class TestBackwardsCompat: |
| def test_load_config_with_no_args(self): |
| """Calling load_config() with no args should not raise.""" |
| with _clean_env(): |
| cfg = load_config(config_path="__nonexistent__.yaml") |
| assert isinstance(cfg, OpenRARLConfig) |
|
|
| def test_reward_config_matches_reward_weights(self): |
| """RewardConfig fields should match the existing RewardWeights dataclass.""" |
| from openra_env.reward import RewardWeights |
|
|
| rw = RewardWeights() |
| rc = RewardConfig() |
| assert rc.survival == rw.survival |
| assert rc.economic_efficiency == rw.economic_efficiency |
| assert rc.aggression == rw.aggression |
| assert rc.defense == rw.defense |
| assert rc.victory == rw.victory |
| assert rc.defeat == rw.defeat |
|
|
|
|
| class TestRewardVectorConfig: |
| """Test reward vector configuration.""" |
|
|
| def test_enabled_by_default(self): |
| cfg = RewardVectorConfig() |
| assert cfg.enabled is True |
|
|
| def test_default_weights(self): |
| cfg = RewardVectorConfig() |
| assert cfg.weights["combat"] == 0.30 |
| assert cfg.weights["economy"] == 0.15 |
| assert cfg.weights["outcome"] == 1.00 |
| assert len(cfg.weights) == 8 |
|
|
| def test_present_in_root_config(self): |
| cfg = OpenRARLConfig() |
| assert hasattr(cfg, "reward_vector") |
| assert isinstance(cfg.reward_vector, RewardVectorConfig) |
| assert cfg.reward_vector.enabled is True |
|
|
| def test_enable_via_yaml(self): |
| with _clean_env(): |
| import tempfile |
| with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: |
| yaml.dump({"reward_vector": {"enabled": True}}, f) |
| f.flush() |
| cfg = load_config(config_path=f.name) |
| assert cfg.reward_vector.enabled is True |
|
|
| def test_custom_weights_via_yaml(self): |
| with _clean_env(): |
| import tempfile |
| with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: |
| yaml.dump({"reward_vector": {"enabled": True, "weights": {"combat": 0.5}}}, f) |
| f.flush() |
| cfg = load_config(config_path=f.name) |
| assert cfg.reward_vector.weights["combat"] == 0.5 |
|
|
|
|
| |
|
|
|
|
| class TestValidation: |
| def test_invalid_grpc_port_type(self): |
| with pytest.raises(Exception): |
| GameConfig(grpc_port="not_a_number") |
|
|
| def test_invalid_reward_weight(self): |
| with pytest.raises(Exception): |
| RewardConfig(victory="not_a_float") |
|
|
|
|
| |
|
|
| _CONFIG_ENV_VARS = [ |
| "OPENRA_PATH", "RECORD_REPLAYS", "BOT_TYPE", "AI_SLOT", |
| "PLANNING_ENABLED", "PLANNING_MAX_TURNS", "PLANNING_MAX_TIME", |
| "OPENROUTER_API_KEY", "OPENROUTER_MODEL", |
| "LLM_BASE_URL", "LLM_API_KEY", "LLM_MODEL", |
| "OPENRA_URL", "MAX_TIME", "LLM_AGENT_LOG", |
| ] |
|
|
|
|
| class _clean_env: |
| """Context manager that temporarily clears config-related env vars and sets new ones.""" |
|
|
| def __init__(self, **overrides): |
| self._overrides = overrides |
| self._saved: dict[str, str | None] = {} |
|
|
| def __enter__(self): |
| |
| for var in _CONFIG_ENV_VARS: |
| self._saved[var] = os.environ.pop(var, None) |
| |
| for key, val in self._overrides.items(): |
| os.environ[key] = str(val) |
| return self |
|
|
| def __exit__(self, *args): |
| |
| for key in self._overrides: |
| os.environ.pop(key, None) |
| |
| for var, val in self._saved.items(): |
| if val is not None: |
| os.environ[var] = val |
|
|
|
|
| def _temp_yaml(data: dict): |
| """Context manager that writes *data* to a temp YAML file and yields its path.""" |
| import contextlib |
|
|
| @contextlib.contextmanager |
| def _ctx(): |
| with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: |
| yaml.dump(data, f) |
| path = f.name |
| try: |
| yield path |
| finally: |
| Path(path).unlink(missing_ok=True) |
|
|
| return _ctx() |
|
|
|
|
| |
|
|
|
|
| class TestPromptsConfig: |
| """Tests for the PromptsConfig system.""" |
|
|
| def test_default_prompts_have_values(self): |
| """PromptsConfig defaults should have non-empty values for key fields.""" |
| p = PromptsConfig() |
| assert "end_planning_phase" in p.planning_nudge |
| assert "tool" in p.no_tool_nudge.lower() |
| assert "{building}" in p.power_warning |
| assert "{count}" in p.alerts.idle_army |
|
|
| def test_prompts_in_root_config(self): |
| """OpenRARLConfig should have prompts field with defaults.""" |
| config = OpenRARLConfig() |
| assert isinstance(config.prompts, PromptsConfig) |
| assert isinstance(config.prompts.alerts, AlertPromptsConfig) |
| assert config.prompts.planning_complete == "Planning complete. Game is now live." |
|
|
| def test_prompts_from_yaml(self): |
| """Override prompts via config YAML.""" |
| data = { |
| "prompts": { |
| "no_tool_nudge": "Please call a tool now.", |
| "alerts": { |
| "low_power": "Power is low: {balance}", |
| }, |
| }, |
| } |
| with _temp_yaml(data) as path: |
| config = load_config(config_path=path) |
| assert config.prompts.no_tool_nudge == "Please call a tool now." |
| assert config.prompts.alerts.low_power == "Power is low: {balance}" |
| |
| assert "combat units idle" in config.prompts.alerts.idle_army |
|
|
| def test_alert_template_format(self): |
| """Alert templates should render with .format().""" |
| p = AlertPromptsConfig() |
| result = p.low_power.format(balance="-30") |
| assert "LOW POWER" in result |
| assert "-30" in result |
|
|
| def test_placement_template_format(self): |
| """Placement templates should render with .format().""" |
| p = PromptsConfig() |
| result = p.placement_failed.format(building="powr", reason="no valid position") |
| assert "powr" in result |
| assert "no valid position" in result |
|
|
| def test_planning_prompt_template(self): |
| """Planning prompt template should accept all expected variables.""" |
| p = PromptsConfig() |
| result = p.planning_prompt.format( |
| max_turns=10, map_name="test", map_width=64, map_height=64, |
| base_x=10, base_y=10, enemy_x=50, enemy_y=50, |
| faction="russia", side="Soviet", |
| opponent_summary="Easy AI", planning_nudge=p.planning_nudge, |
| ) |
| assert "10 turns" in result |
| assert "russia" in result |
| assert "end_planning_phase" in result |
|
|
| def test_backward_compat_system_prompt_migration(self): |
| """agent.system_prompt should migrate to prompts.system_prompt.""" |
| config = OpenRARLConfig(agent=AgentConfig(system_prompt="My custom prompt")) |
| assert config.prompts.system_prompt == "My custom prompt" |
|
|
| def test_prompts_system_prompt_takes_precedence(self): |
| """prompts.system_prompt should win over agent.system_prompt.""" |
| config = OpenRARLConfig( |
| agent=AgentConfig(system_prompt="agent version"), |
| prompts=PromptsConfig(system_prompt="prompts version"), |
| ) |
| assert config.prompts.system_prompt == "prompts version" |
|
|
| def test_backward_compat_system_prompt_file_migration(self): |
| """agent.system_prompt_file should migrate to prompts.system_prompt_file.""" |
| config = OpenRARLConfig(agent=AgentConfig(system_prompt_file="/tmp/test.txt")) |
| assert config.prompts.system_prompt_file == "/tmp/test.txt" |
|
|
| def test_env_var_prompts_file(self): |
| """PROMPTS_FILE env var should set prompts.prompts_file.""" |
| with patch.dict(os.environ, {"PROMPTS_FILE": "/tmp/prompts.yaml"}, clear=False): |
| config = load_config(config_path="/nonexistent/config.yaml") |
| assert config.prompts.prompts_file == "/tmp/prompts.yaml" |
|
|
| def test_game_start_template(self): |
| """Game start template should render correctly.""" |
| p = PromptsConfig() |
| result = p.game_start.format( |
| strategy_section="\n\nRush strategy", |
| briefing="Map: test", |
| barracks_type="barr", |
| mcv_note=" Your MCV is unit 42.", |
| ) |
| assert "Game started!" in result |
| assert "Rush strategy" in result |
| assert "barr" in result |
| assert "unit 42" in result |
|
|
| def test_insufficient_funds_template(self): |
| """Insufficient funds template should render correctly.""" |
| p = PromptsConfig() |
| result = p.insufficient_funds.format(available=500, item="3tnk", cost=950) |
| assert "500" in result |
| assert "3tnk" in result |
| assert "950" in result |
|
|
| def test_build_queued_template(self): |
| """Build queued template should render correctly.""" |
| p = PromptsConfig() |
| result = p.build_queued.format(building="powr", cost=300, ticks=180, seconds=7.2) |
| assert "powr" in result |
| assert "300" in result |
| assert "180" in result |
| assert "auto-places" in result |
|
|
| def test_build_unit_queued_template(self): |
| """Build unit queued template should render correctly.""" |
| p = PromptsConfig() |
| result = p.build_unit_queued.format( |
| count=3, unit="e1", cost=100, ticks_each=60, |
| ticks_total=180, seconds_total=7.2) |
| assert "3x" in result |
| assert "e1" in result |
| assert "60" in result |
| assert "180" in result |
|
|
| def test_build_already_pending_template(self): |
| """Build already pending template should render correctly.""" |
| p = PromptsConfig() |
| result = p.build_already_pending.format(building="powr") |
| assert "powr" in result |
| assert "already queued" in result |
|
|
| def test_max_alerts_default(self): |
| """AlertsConfig max_alerts should default to 0 (unlimited).""" |
| cfg = AlertsConfig() |
| assert cfg.max_alerts == 0 |
|
|
|
|
| |
|
|
|
|
| class TestCompressionConfig: |
| def test_defaults(self): |
| c = CompressionConfig() |
| assert c.include_strategy is True |
| assert c.include_military is True |
| assert c.include_production is True |
|
|
| def test_disable_strategy(self): |
| c = CompressionConfig(include_strategy=False) |
| assert c.include_strategy is False |
|
|
| def test_llm_compression_strategy_default(self): |
| llm = LLMConfig() |
| assert llm.compression_strategy == "sliding_window" |
|
|
| def test_llm_compression_trigger_default(self): |
| llm = LLMConfig() |
| assert llm.compression_trigger == 0 |
|
|
| def test_compression_strategy_none(self): |
| llm = LLMConfig(compression_strategy="none") |
| assert llm.compression_strategy == "none" |
|
|
| def test_compression_trigger_custom(self): |
| llm = LLMConfig(compression_trigger=60) |
| assert llm.compression_trigger == 60 |
|
|
| def test_prompts_compression_field(self): |
| p = PromptsConfig() |
| assert isinstance(p.compression, CompressionConfig) |
| assert p.compression.include_strategy is True |
|
|
| def test_move_eta_template(self): |
| p = PromptsConfig() |
| result = p.move_eta.format(ticks=183, seconds=7.3) |
| assert "183" in result |
| assert "7.3" in result |
|
|
| def test_full_config_compression_yaml(self): |
| """Compression fields round-trip through YAML config loading.""" |
| with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: |
| yaml.dump({ |
| "llm": { |
| "compression_strategy": "none", |
| "compression_trigger": 60, |
| "keep_last_messages": 20, |
| }, |
| "prompts": { |
| "compression": { |
| "include_strategy": False, |
| "include_military": True, |
| "include_production": False, |
| } |
| } |
| }, f) |
| f.flush() |
| cfg = load_config(f.name) |
| os.unlink(f.name) |
| assert cfg.llm.compression_strategy == "none" |
| assert cfg.llm.compression_trigger == 60 |
| assert cfg.llm.keep_last_messages == 20 |
| assert cfg.prompts.compression.include_strategy is False |
| assert cfg.prompts.compression.include_production is False |
| assert cfg.prompts.compression.include_military is True |
|
|
|
|
| |
|
|
|
|
| class TestOpponentConfig: |
| def test_default_spawns_enemy(self): |
| """Default opponent config spawns an enemy in Multi0.""" |
| cfg = OpponentConfig() |
| assert cfg.ai_slot == "Multi0" |
| assert cfg.bot_type == "easy" |
|
|
| def test_disable_enemy_via_empty_slot(self): |
| cfg = OpponentConfig(ai_slot="") |
| assert cfg.ai_slot == "" |
|
|
| def test_custom_bot_type(self): |
| cfg = OpponentConfig(bot_type="hard") |
| assert cfg.bot_type == "hard" |
|
|
|
|
| |
|
|
|
|
| class TestBotTypeMapping: |
| def test_beginner_maps_to_beginner(self): |
| from openra_env.server.openra_process import BOT_TYPE_MAP |
| assert BOT_TYPE_MAP["beginner"] == "beginner" |
|
|
| def test_easy_maps_to_easy(self): |
| from openra_env.server.openra_process import BOT_TYPE_MAP |
| assert BOT_TYPE_MAP["easy"] == "easy" |
|
|
| def test_medium_maps_to_medium(self): |
| from openra_env.server.openra_process import BOT_TYPE_MAP |
| assert BOT_TYPE_MAP["medium"] == "medium" |
|
|
| def test_hard_maps_to_normal(self): |
| from openra_env.server.openra_process import BOT_TYPE_MAP |
| assert BOT_TYPE_MAP["hard"] == "normal" |
|
|
| def test_brutal_maps_to_rush(self): |
| from openra_env.server.openra_process import BOT_TYPE_MAP |
| assert BOT_TYPE_MAP["brutal"] == "rush" |
|
|
| def test_raw_names_pass_through(self): |
| from openra_env.server.openra_process import BOT_TYPE_MAP |
| for raw in ["rush", "normal", "turtle", "naval", "beginner", "easy", "medium"]: |
| assert BOT_TYPE_MAP.get(raw, raw) == raw |
|
|
| def test_build_command_maps_hard(self): |
| from openra_env.server.openra_process import OpenRAConfig, OpenRAProcessManager |
| openra_path = str(Path(__file__).parent.parent / "OpenRA") |
| config = OpenRAConfig(openra_path=openra_path, bot_type="hard") |
| manager = OpenRAProcessManager(config) |
| cmd = manager._build_command() |
| bots_arg = [a for a in cmd if "Launch.Bots" in a][0] |
| assert "normal" in bots_arg |
| assert "hard" not in bots_arg |
|
|
| def test_build_command_maps_brutal(self): |
| from openra_env.server.openra_process import OpenRAConfig, OpenRAProcessManager |
| openra_path = str(Path(__file__).parent.parent / "OpenRA") |
| config = OpenRAConfig(openra_path=openra_path, bot_type="brutal") |
| manager = OpenRAProcessManager(config) |
| cmd = manager._build_command() |
| bots_arg = [a for a in cmd if "Launch.Bots" in a][0] |
| assert "rush" in bots_arg |
| assert "brutal" not in bots_arg |
|
|
| def test_build_command_no_enemy_with_empty_slot(self): |
| from openra_env.server.openra_process import OpenRAConfig, OpenRAProcessManager |
| openra_path = str(Path(__file__).parent.parent / "OpenRA") |
| config = OpenRAConfig(openra_path=openra_path, ai_slot="") |
| manager = OpenRAProcessManager(config) |
| cmd = manager._build_command() |
| bots_arg = [a for a in cmd if "Launch.Bots" in a][0] |
| assert bots_arg == "Launch.Bots=Multi1:rl-agent" |
|
|
| def test_default_config_spawns_enemy(self): |
| from openra_env.server.openra_process import OpenRAConfig, OpenRAProcessManager |
| openra_path = str(Path(__file__).parent.parent / "OpenRA") |
| config = OpenRAConfig(openra_path=openra_path) |
| manager = OpenRAProcessManager(config) |
| cmd = manager._build_command() |
| bots_arg = [a for a in cmd if "Launch.Bots" in a][0] |
| |
| assert "Multi0" in bots_arg |
| assert "normal" in bots_arg |
|
|