from __future__ import annotations import json import time from pathlib import Path import pytest from cognitive_agent.application.self_repair.healing_model import ( PHASE_DIAGNOSED, PHASE_INJECTED, PHASE_PASSED, SCENARIOS, SelfHealingEngine, public_scenarios, ) from cognitive_agent.application.self_repair.service import SelfRepairService from cognitive_agent.application.self_repair.can_runtime import ( StandaloneCanRuntimeManager, ) from cognitive_agent.application.self_repair.signal_diagnostic import SignalDiagnosticModel from cognitive_agent.application.self_repair.zcan_bridge import ZcanBridgeManager from cognitive_agent.infrastructure.can.gateway import ZlgChannelConfig def test_six_production_cases_complete_the_shadow_repair_loop() -> None: assert len(SCENARIOS) == 6 assert len(public_scenarios()) == 6 for scenario in SCENARIOS: engine = SelfHealingEngine(clock=lambda: 1000.0) started = engine.start(scenario["id"], now=1000.0) assert started["execution_mode"] == "shadow" assert started["injection_scope"] == "SHADOW_TELEMETRY_ONLY" assert engine.snapshot(1006.2)["phase"] == PHASE_INJECTED assert engine.snapshot(1008.7)["phase"] == PHASE_DIAGNOSED completed = engine.snapshot(1018.2) assert completed["phase"] == PHASE_PASSED assert completed["pass"] is True assert completed["telemetry"]["safety_protection"] is True assert completed["telemetry"]["task_restored"] is True def test_connector_inventory_matches_physical_housings() -> None: snapshot = SignalDiagnosticModel().analyze(None) connectors = snapshot["connectors"] def pin(pin_id: str) -> dict[str, object]: connector, number = pin_id.split("-", maxsplit=1) return next( item for item in connectors[connector] if item["number"] == int(number) ) assert [pin["number"] for pin in connectors["JA1"]] == list(range(1, 15)) assert [pin["number"] for pin in connectors["JB1"]] == list(range(1, 19)) assert [pin["number"] for pin in connectors["JC1"]] == list(range(1, 41)) assert sum(len(pins) for pins in connectors.values()) == 72 assert snapshot["connector_enabled_count"] == 21 assert pin("JA1-14")["enabled"] is False assert pin("JC1-21")["enabled"] is True def test_service_is_safe_without_machine_specific_tool_paths() -> None: service = SelfRepairService() health = service.health() state = service.snapshot() assert health["ok"] is True assert health["service"] == "seat-self-repair" assert health["configuration"]["diagnostic_config_available"] is True assert health["configuration"]["bridge_script_available"] is True assert len(state["scenarios"]) == 6 assert state["integration"]["auto_start_bridge"] is False assert state["model"]["type"] == "PROCEDURAL_SEAT" assert service.run_self_test() is True def test_runtime_must_start_before_fault_injection() -> None: service = SelfRepairService() with pytest.raises(ValueError, match="请先启动座椅连续运行"): service.start_healing({ "case_id": "FI-FIELD-01", "execution_mode": "shadow", }) runtime = service.set_runtime({"active": True, "execution_mode": "shadow"}) assert runtime["active"] is True assert runtime["motion_profile"] == "四电机协同自由运动" assert runtime["events"][0]["title"] == "座椅自由运动任务已启动" healing = service.start_healing({ "case_id": "FI-FIELD-01", "execution_mode": "shadow", }) assert healing["case_id"] == "FI-FIELD-01" snapshot = service.snapshot() assert snapshot["runtime"]["injected_count"] == 1 def test_bridge_manager_does_not_arm_visible_tooling_by_default(tmp_path: Path) -> None: manager = ZcanBridgeManager( tmp_path / "bridge.json", tmp_path / "bridge.py", zcanpro_exe=tmp_path / "ZCANPRO.exe", script_config_path=tmp_path / "zcanpro.ini", ) snapshot = manager.snapshot() assert snapshot["state"] == "IDLE" assert snapshot["retry_armed"] is False assert snapshot["attempts"] == 0 def test_bridge_manager_does_not_restart_a_live_busy_request(tmp_path: Path) -> None: status_path = tmp_path / "bridge.json" status_path.write_text( json.dumps({ "heartbeat_epoch": time.time() - 5.0, "bridge_state": "RUNNING", "current_request_id": "SELF_HEAL_BENCH_1", }), encoding="utf-8", ) manager = ZcanBridgeManager( status_path, tmp_path / "bridge.py", heartbeat_stale_s=3.0, busy_heartbeat_grace_s=45.0, ) assert manager._heartbeat_online() is False assert manager._bridge_busy_within_grace() is True def test_standalone_runtime_opens_one_owner_for_seat_and_vehicle_channels( tmp_path: Path, ) -> None: required_version = "runtime-test-v1" bridge_script = tmp_path / "seat_bridge.py" bridge_script.write_text( "BRIDGE_VERSION = 'runtime-test-v1'\n" "def run_bridge(transport=None, stop_event=None):\n" " stop_event.wait(2.0)\n", encoding="utf-8", ) python_executable = tmp_path / "python.exe" zlg_bridge_script = tmp_path / "zlg_bridge.py" dll_path = tmp_path / "zlgcan.dll" for path in (python_executable, zlg_bridge_script, dll_path): path.write_bytes(b"test") owners = [] class FakeBus: def __init__(self, channel, config): self.channel = channel self.config = config def send(self, *args, **kwargs): return None def receive(self, *args, **kwargs): return None class FakeOwner: def __init__(self, **kwargs): self.kwargs = kwargs self.closed = False self.configs = {item.channel: item for item in kwargs["channels"]} owners.append(self) def channel_bus(self, channel): return FakeBus(channel, self.configs[channel]) def close(self): self.closed = True manager = StandaloneCanRuntimeManager( tmp_path / "status.json", bridge_script, required_version=required_version, python_executable=python_executable, zlg_bridge_script=zlg_bridge_script, dll_path=dll_path, device_type=41, device_index=0, channels=[ ZlgChannelConfig(channel=0, frame_format="classic_can"), ZlgChannelConfig(channel=1, frame_format="can_fd"), ], seat_channel=0, poll_s=0.01, process_factory=FakeOwner, ) try: assert manager.configured is True assert manager.channel_bus(1, timeout_s=1.0).channel == 1 manager.ensure_async() assert len(owners) == 1 assert [ item.frame_format for item in owners[0].kwargs["channels"] ] == ["classic_can", "can_fd"] assert manager.snapshot()["backend"] == "standalone_zlg" finally: manager.stop() assert owners[0].closed is True def _qualified_runtime_status(*, build_id: int = 16, frame_age_s: float = 0.0): now = time.time() frame_epoch = now - frame_age_s return { "bridge_version": "runtime-test-v1", "heartbeat_epoch": now, "current_request_id": None, "system": {"firmware_revision": 15}, "firmware_identity": { "firmware_build_id": build_id, "qualified": build_id == 16, }, "frame_epochs": { frame_id: frame_epoch for frame_id in ( "0x301", "0x302", "0x303", "0x304", "0x305", "0x306", "0x307" ) }, } def test_standalone_runtime_rejects_stale_protocol_and_old_firmware_build( tmp_path: Path, ) -> None: manager = StandaloneCanRuntimeManager( tmp_path / "status.json", tmp_path / "seat_bridge.py", required_version="runtime-test-v1", python_executable=tmp_path / "python.exe", zlg_bridge_script=tmp_path / "zlg_bridge.py", dll_path=tmp_path / "zlgcan.dll", device_type=41, device_index=0, channels=[ZlgChannelConfig(channel=0, frame_format="classic_can")], seat_channel=0, expected_firmware_revision=15, expected_firmware_build_id=16, ) manager.status_path.write_text( json.dumps(_qualified_runtime_status(frame_age_s=5.0)), encoding="utf-8" ) ready, reason, _ = manager._protocol_status() assert ready is False assert "过期" in reason manager.status_path.write_text( json.dumps(_qualified_runtime_status(build_id=15)), encoding="utf-8" ) ready, reason, _ = manager._protocol_status() assert ready is False assert "build" in reason def test_standalone_runtime_restores_repository_firmware_before_request( tmp_path: Path, ) -> None: class FakeProgrammer: def __init__(self): self.calls = 0 def program(self): self.calls += 1 return {"programmed": True, "image_sha256": "f" * 64} programmer = FakeProgrammer() manager = StandaloneCanRuntimeManager( tmp_path / "status.json", tmp_path / "seat_bridge.py", required_version="runtime-test-v1", python_executable=tmp_path / "python.exe", zlg_bridge_script=tmp_path / "zlg_bridge.py", dll_path=tmp_path / "zlgcan.dll", device_type=41, device_index=0, channels=[ZlgChannelConfig(channel=0, frame_format="classic_can")], seat_channel=0, firmware_programmer=programmer, firmware_auto_flash=True, expected_firmware_revision=15, expected_firmware_build_id=16, firmware_boot_wait_s=0, protocol_probe_timeout_s=0.01, startup_timeout_s=0.2, ) manager.status_path.write_text( json.dumps(_qualified_runtime_status(build_id=15)), encoding="utf-8" ) manager.ensure_async = lambda: None manager._stop_runtime_owner = lambda: None manager._attempt_start = lambda: manager.status_path.write_text( json.dumps(_qualified_runtime_status()), encoding="utf-8" ) status = manager.ensure_seat_protocol() assert status["firmware_identity"]["firmware_build_id"] == 16 assert programmer.calls == 1 assert manager.snapshot()["firmware_recovery_count"] == 1