"""HRL 协议执行的分支回归;所有模拟器交互仅在本地进行。""" from unittest.mock import patch import pytest pytest.importorskip("numpy") from mcm26b.config import ProblemConfig from mcm26b.harness import LocalSimTransport from mcm26b.hrl.constants import FALLBACK_OFFSET, N_CANDIDATES, STATION_OFFSET from mcm26b.hrl.scheduler import Candidate from mcm26b.planner.hrl_loop import HRLLoop from mcm26b.protocol import SimulatorClient from referee import LocalSimulator, Source def make_loop(*, problem=4, high_policy=None): simulator = LocalSimulator( [Source(channel=20, x=45.0, y=0.0, r_eff_m=1000.0)], robot_id="hrl-branch-test") client = SimulatorClient("hrl-branch-test", transport=LocalSimTransport(simulator)) client.enter() return HRLLoop(client, ProblemConfig(), problem=problem, high_policy=high_policy) def test_fallback_failure_then_success_records_actual_time_position_and_clear(): loop = make_loop() try: observation = loop.client.measure((0.0, 0.0), 20) loop._record_measure(20, (0.0, 0.0), observation) loop.blocked_probes[20] = [(0.0, 0.0)] candidate = Candidate(FALLBACK_OFFSET + 19, "fallback", (999.0, 999.0), channel=20) with patch("mcm26b.hrl.scheduler.fallback_clear_cells", return_value=[(20.0, 0.0), (40.0, 0.0)]): outcome = loop.hrl_step(candidate.slot, [candidate]) assert outcome.clear_success == (20,) assert loop.cleared == [20] assert outcome.position_reached == (40.0, 0.0) # 40m / 5m/s + 3s失败 + 5s成功;clear不产生换频时间。 assert outcome.delta_virtual_s == pytest.approx(16.0) assert loop.client.state.channel == 20 assert loop.fallback_log[-1]["attempts"] == 2 assert 20 not in loop.blocked_probes assert not loop.anomalies finally: loop.client.exit() def test_invalid_policy_output_is_logged_and_replaced_by_valid_action(): class InvalidPolicy: def select(self, _observation, _mask): return N_CANDIDATES + 1 loop = make_loop(problem=3, high_policy=InvalidPolicy()) candidate = Candidate(STATION_OFFSET, "station", (0.0, 0.0), station_index=0) executed = [] def execute(action, candidates): # pylint: disable=unused-argument executed.append(action) loop.stop_reason = "test_complete" try: with patch.object(loop, "build_candidates", return_value=[candidate]), \ patch.object(loop, "hrl_observation", return_value=None), \ patch.object(loop, "heuristic_action", return_value=candidate.slot), \ patch.object(loop, "hrl_step", side_effect=execute): loop._coverage_phase() assert executed == [candidate.slot] assert loop.anomalies[0]["kind"] == "hrl_invalid_policy_action" finally: loop.client.exit() def test_opportunistic_measure_precedes_candidate_snapshot(): loop = make_loop(problem=3) order = [] try: assert loop.config.bayes.opportunistic_measure with patch.object(loop, "_measure_at_stop", side_effect=lambda: order.append("measure")), \ patch.object(loop, "build_candidates", side_effect=lambda: order.append("snapshot") or []): loop._coverage_phase() assert order == ["measure", "snapshot"] finally: loop.client.exit()