"""Tests for OpenRAEnvironment using mocked bridge and process manager.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from openra_env.generated import rl_bridge_pb2 from openra_env.models import ActionType, CommandModel, OpenRAAction from openra_env.server.openra_environment import OpenRAEnvironment def _make_proto_observation(tick=0, cash=1000, done=False, result=""): """Create a minimal protobuf GameObservation for testing.""" obs = rl_bridge_pb2.GameObservation() obs.tick = tick obs.economy.cash = cash obs.economy.ore = 0 obs.economy.power_provided = 100 obs.economy.power_drained = 50 obs.economy.resource_capacity = 2000 obs.economy.harvester_count = 1 obs.military.units_killed = 0 obs.military.units_lost = 0 obs.military.buildings_killed = 0 obs.military.buildings_lost = 0 obs.military.army_value = 500 obs.military.active_unit_count = 3 obs.map_info.width = 64 obs.map_info.height = 64 obs.map_info.map_name = "Test Map" obs.done = done obs.result = result obs.reward = 0.0 # Add a unit unit = obs.units.add() unit.actor_id = 1 unit.type = "e1" unit.pos_x = 100 unit.pos_y = 200 unit.cell_x = 4 unit.cell_y = 8 unit.hp_percent = 1.0 unit.is_idle = True unit.owner = "Player" # Add a building bldg = obs.buildings.add() bldg.actor_id = 10 bldg.type = "powr" bldg.pos_x = 50 bldg.pos_y = 50 bldg.hp_percent = 1.0 bldg.owner = "Player" bldg.is_powered = True return obs class TestOpenRAEnvironmentReset: @patch("openra_env.server.openra_environment.OpenRAProcessManager") @patch("openra_env.server.openra_environment.BridgeClient") def test_reset_returns_observation(self, MockBridge, MockProcess): mock_bridge = MockBridge.return_value mock_bridge.close = AsyncMock() mock_bridge.wait_for_ready = AsyncMock(return_value=True) mock_bridge.start_session = AsyncMock(return_value=_make_proto_observation(tick=0)) mock_bridge.session_started = False # Mock get_state to return a GameState proto mock_game_state = MagicMock() mock_game_state.tick = 0 mock_game_state.player_faction = "england" mock_game_state.enemy_faction = "russia" mock_bridge.get_state = AsyncMock(return_value=mock_game_state) mock_process = MockProcess.return_value mock_process.kill = MagicMock() mock_process.launch = MagicMock(return_value=12345) env = OpenRAEnvironment(openra_path="/fake/path") env._bridge = mock_bridge env._process = mock_process obs = env.reset() # reset() now returns a minimal observation (game is paused, # session not yet started). Full obs available after session starts. assert obs.tick == 0 assert obs.economy.cash == 0 # Minimal obs — no economy data yet mock_process.kill.assert_called_once() mock_process.launch.assert_called_once() # start_session should NOT be called during reset (deferred) mock_bridge.start_session.assert_not_called() @patch("openra_env.server.openra_environment.OpenRAProcessManager") @patch("openra_env.server.openra_environment.BridgeClient") def test_reset_with_seed(self, MockBridge, MockProcess): mock_bridge = MockBridge.return_value mock_bridge.close = AsyncMock() mock_bridge.wait_for_ready = AsyncMock(return_value=True) mock_bridge.start_session = AsyncMock(return_value=_make_proto_observation()) mock_bridge.session_started = False mock_bridge.get_state = AsyncMock(return_value=MagicMock(tick=0, player_faction="", enemy_faction="")) mock_process = MockProcess.return_value mock_process.kill = MagicMock() mock_process.launch = MagicMock(return_value=12345) env = OpenRAEnvironment(openra_path="/fake/path") env._bridge = mock_bridge env._process = mock_process env.reset(seed=42) assert env._config.seed == 42 @patch("openra_env.server.openra_environment.OpenRAProcessManager") @patch("openra_env.server.openra_environment.BridgeClient") def test_reset_with_episode_id(self, MockBridge, MockProcess): mock_bridge = MockBridge.return_value mock_bridge.close = AsyncMock() mock_bridge.wait_for_ready = AsyncMock(return_value=True) mock_bridge.start_session = AsyncMock(return_value=_make_proto_observation()) mock_bridge.session_started = False mock_bridge.get_state = AsyncMock(return_value=MagicMock(tick=0, player_faction="", enemy_faction="")) mock_process = MockProcess.return_value mock_process.kill = MagicMock() mock_process.launch = MagicMock(return_value=12345) env = OpenRAEnvironment(openra_path="/fake/path") env._bridge = mock_bridge env._process = mock_process env.reset(episode_id="custom-ep-001") assert env.state.episode_id == "custom-ep-001" @patch("openra_env.server.openra_environment.OpenRAProcessManager") @patch("openra_env.server.openra_environment.BridgeClient") def test_reset_raises_if_bridge_not_ready(self, MockBridge, MockProcess): mock_bridge = MockBridge.return_value mock_bridge.close = AsyncMock() mock_bridge.wait_for_ready = AsyncMock(return_value=False) mock_process = MockProcess.return_value mock_process.kill = MagicMock() mock_process.launch = MagicMock() env = OpenRAEnvironment(openra_path="/fake/path") env._bridge = mock_bridge env._process = mock_process with pytest.raises(RuntimeError, match="gRPC bridge failed to start"): env.reset() class TestOpenRAEnvironmentStep: def _setup_env(self, MockBridge, MockProcess): mock_bridge = MockBridge.return_value mock_bridge.close = AsyncMock() mock_bridge.wait_for_ready = AsyncMock(return_value=True) mock_bridge.start_session = AsyncMock(return_value=_make_proto_observation(tick=0)) mock_process = MockProcess.return_value mock_process.kill = MagicMock() mock_process.launch = MagicMock(return_value=12345) env = OpenRAEnvironment(openra_path="/fake/path") env._bridge = mock_bridge env._process = mock_process return env, mock_bridge, mock_process @patch("openra_env.server.openra_environment.OpenRAProcessManager") @patch("openra_env.server.openra_environment.BridgeClient") def test_step_returns_observation(self, MockBridge, MockProcess): env, mock_bridge, _ = self._setup_env(MockBridge, MockProcess) env.reset() mock_bridge.step = AsyncMock(return_value=_make_proto_observation(tick=10, cash=1500)) action = OpenRAAction(commands=[CommandModel(action=ActionType.NO_OP)]) obs = env.step(action) assert obs.tick == 10 assert obs.economy.cash == 1500 assert env.state.step_count == 1 assert env.state.game_tick == 10 @patch("openra_env.server.openra_environment.OpenRAProcessManager") @patch("openra_env.server.openra_environment.BridgeClient") def test_step_increments_step_count(self, MockBridge, MockProcess): env, mock_bridge, _ = self._setup_env(MockBridge, MockProcess) env.reset() mock_bridge.step = AsyncMock(return_value=_make_proto_observation(tick=10)) action = OpenRAAction(commands=[CommandModel(action=ActionType.NO_OP)]) env.step(action) assert env.state.step_count == 1 mock_bridge.step = AsyncMock(return_value=_make_proto_observation(tick=20)) env.step(action) assert env.state.step_count == 2 @patch("openra_env.server.openra_environment.OpenRAProcessManager") @patch("openra_env.server.openra_environment.BridgeClient") def test_step_with_multiple_commands(self, MockBridge, MockProcess): env, mock_bridge, _ = self._setup_env(MockBridge, MockProcess) env.reset() mock_bridge.step = AsyncMock(return_value=_make_proto_observation(tick=10)) action = OpenRAAction(commands=[ CommandModel(action=ActionType.MOVE, actor_id=1, target_x=10, target_y=20), CommandModel(action=ActionType.BUILD, item_type="powr"), ]) obs = env.step(action) assert obs.tick == 10 @patch("openra_env.server.openra_environment.OpenRAProcessManager") @patch("openra_env.server.openra_environment.BridgeClient") def test_step_terminal_observation(self, MockBridge, MockProcess): env, mock_bridge, _ = self._setup_env(MockBridge, MockProcess) env.reset() mock_bridge.step = AsyncMock( return_value=_make_proto_observation(tick=1000, done=True, result="win") ) action = OpenRAAction(commands=[CommandModel(action=ActionType.NO_OP)]) obs = env.step(action) assert obs.done is True assert obs.result == "win" assert obs.reward > 0 # Should include victory reward class TestOpenRAEnvironmentState: @patch("openra_env.server.openra_environment.OpenRAProcessManager") @patch("openra_env.server.openra_environment.BridgeClient") def test_initial_state(self, MockBridge, MockProcess): env = OpenRAEnvironment(openra_path="/fake/path") state = env.state assert state.step_count == 0 assert state.game_tick == 0 @patch("openra_env.server.openra_environment.OpenRAProcessManager") @patch("openra_env.server.openra_environment.BridgeClient") def test_state_after_reset(self, MockBridge, MockProcess): mock_bridge = MockBridge.return_value mock_bridge.close = AsyncMock() mock_bridge.wait_for_ready = AsyncMock(return_value=True) mock_bridge.start_session = AsyncMock(return_value=_make_proto_observation()) mock_process = MockProcess.return_value mock_process.kill = MagicMock() mock_process.launch = MagicMock() env = OpenRAEnvironment(openra_path="/fake/path", map_name="test_map") env._bridge = mock_bridge env._process = mock_process env.reset(episode_id="ep-001") assert env.state.episode_id == "ep-001" assert env.state.map_name == "test_map" assert env.state.step_count == 0 class TestOpenRAEnvironmentClose: @patch("openra_env.server.openra_environment.OpenRAProcessManager") @patch("openra_env.server.openra_environment.BridgeClient") def test_close_cleans_up(self, MockBridge, MockProcess): mock_bridge = MockBridge.return_value mock_bridge.close = AsyncMock() mock_process = MockProcess.return_value mock_process.kill = MagicMock() env = OpenRAEnvironment(openra_path="/fake/path") env._bridge = mock_bridge env._process = mock_process env.close() mock_bridge.close.assert_called_once() mock_process.kill.assert_called_once()