Files
Cognitive_Agent/tests/unit/test_self_repair.py

287 lines
9.5 KiB
Python

from __future__ import annotations
import json
import time
from pathlib import Path
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_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