feat: integrate calibration extension and self-repair modules
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from cognitive_agent.application.calibration import protocol
|
||||
from cognitive_agent.application.calibration.service import CalibrationService
|
||||
|
||||
|
||||
def test_protocol_matches_android_golden_vectors() -> None:
|
||||
assert protocol.crc8(b"123456789") == 0x4B
|
||||
vectors = (
|
||||
(
|
||||
protocol.command(0x00, protocol.OP_HELLO, protocol.AXIS_ALL),
|
||||
"A1 00 01 FF 00 00 00 55",
|
||||
),
|
||||
(
|
||||
protocol.command(0x01, protocol.OP_ARM, protocol.AXIS_ALL, 0x4E37),
|
||||
"A1 01 02 FF 37 4E 00 0A",
|
||||
),
|
||||
(
|
||||
protocol.command(0x23, protocol.OP_CALIBRATE, protocol.AXIS_ALL),
|
||||
"A1 23 10 FF 00 00 00 33",
|
||||
),
|
||||
(
|
||||
protocol.command(0x24, protocol.OP_MOVE_PERMILLE, 0, 1000),
|
||||
"A1 24 11 00 E8 03 00 78",
|
||||
),
|
||||
(
|
||||
protocol.command(0x25, protocol.OP_STOP, protocol.AXIS_ALL),
|
||||
"A1 25 12 FF 00 00 00 6E",
|
||||
),
|
||||
)
|
||||
for actual, expected in vectors:
|
||||
assert actual == bytes.fromhex(expected)
|
||||
|
||||
|
||||
def test_protocol_parses_status_and_rejects_bad_crc() -> None:
|
||||
ack = protocol.parse(0x621, bytes.fromhex("B1 22 10 02 00 07 01 A9"))
|
||||
assert isinstance(ack, protocol.Ack)
|
||||
assert ack.accepted
|
||||
|
||||
controller = protocol.parse(0x622, bytes.fromhex("C1 01 00 17 07 FF 02 4E"))
|
||||
assert isinstance(controller, protocol.ControllerStatus)
|
||||
assert controller.can_ready
|
||||
assert controller.motor_ready
|
||||
assert controller.armed
|
||||
|
||||
frame = bytearray.fromhex("D1 00 F0 00 E0 01 17 40")
|
||||
frame[2] ^= 1
|
||||
with pytest.raises(protocol.ProtocolError):
|
||||
protocol.parse(0x630, bytes(frame))
|
||||
|
||||
|
||||
def test_simulation_uses_real_wire_boundary_and_generates_pose() -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
service = CalibrationService(Path(directory))
|
||||
service.start()
|
||||
try:
|
||||
initial = service.snapshot()
|
||||
assert initial["transport"] == {
|
||||
"mode": "hardware",
|
||||
"state": "disconnected",
|
||||
"detail": "尚未连接",
|
||||
}
|
||||
|
||||
connected = service.connect_can({"mode": "simulation"})
|
||||
assert connected["transport"]["state"] == "online"
|
||||
assert connected["controller"]["can_ready"]
|
||||
assert connected["controller"]["motor_ready"]
|
||||
assert [axis["range"] for axis in connected["axes"]] == [480, 647, 917, 373]
|
||||
|
||||
service.move(0, 800)
|
||||
time.sleep(0.2)
|
||||
moving = service.snapshot()
|
||||
assert moving["controller"]["armed"]
|
||||
assert moving["axes"][0]["moving"]
|
||||
|
||||
time.sleep(0.45)
|
||||
pose = service.snapshot()["mqtt"]
|
||||
assert pose["state"] == "simulated"
|
||||
assert pose["count"] >= 1
|
||||
assert "Seat_cushion" in pose["areas"]
|
||||
|
||||
stopped = service.stop()
|
||||
assert not stopped["controller"]["armed"]
|
||||
finally:
|
||||
service.close()
|
||||
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from cognitive_agent.application.self_extension import (
|
||||
GatewayError,
|
||||
SelfExtensionService,
|
||||
SimulationGateway,
|
||||
build_gateway_command,
|
||||
validate_slot,
|
||||
)
|
||||
|
||||
|
||||
class SelfExtensionTests(unittest.TestCase):
|
||||
def test_gateway_command_contracts_and_slot_validation(self) -> None:
|
||||
command = build_gateway_command("learn", {"slot": 17})
|
||||
self.assertEqual(command["type"], "infrared")
|
||||
self.assertEqual(command["action"], "learn")
|
||||
self.assertEqual(command["data"], {"no": 17})
|
||||
self.assertTrue(str(command["messageId"]).startswith("bridge-"))
|
||||
|
||||
self.assertEqual(validate_slot("248"), 248)
|
||||
with self.assertRaises(GatewayError):
|
||||
validate_slot(0)
|
||||
with self.assertRaises(GatewayError):
|
||||
build_gateway_command("erase", {"confirm": "wrong"})
|
||||
|
||||
def test_simulation_gateway_executes_without_physical_confirmation(self) -> None:
|
||||
service = SelfExtensionService(SimulationGateway())
|
||||
result = service.execute_gateway("emit", {"slot": 3, "repeat": 2})
|
||||
|
||||
self.assertTrue(result["success"])
|
||||
self.assertEqual(result["transport"], "simulation")
|
||||
self.assertFalse(result["physical_state_confirmed"])
|
||||
self.assertEqual(len(result["device_response"]), 2)
|
||||
|
||||
def test_plan_only_accepts_learned_catalog_capabilities(self) -> None:
|
||||
service = SelfExtensionService(SimulationGateway())
|
||||
proposed = {
|
||||
"summary": "先开玩具灯,再打开阅读灯",
|
||||
"needs_confirmation": False,
|
||||
"question": "",
|
||||
"steps": [
|
||||
{
|
||||
"kind": "peripheral",
|
||||
"capability_id": "cap-light-on",
|
||||
"command": "",
|
||||
"label": "ignored",
|
||||
},
|
||||
{
|
||||
"kind": "peripheral",
|
||||
"capability_id": "invented-capability",
|
||||
"command": "",
|
||||
"label": "ignored",
|
||||
},
|
||||
{
|
||||
"kind": "vehicle",
|
||||
"capability_id": "",
|
||||
"command": "打开全部阅读灯",
|
||||
"label": "车内灯光 · 阅读灯",
|
||||
},
|
||||
],
|
||||
}
|
||||
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
|
||||
with patch.object(
|
||||
service,
|
||||
"_request_gpt_plan",
|
||||
return_value=json.dumps(proposed, ensure_ascii=False),
|
||||
):
|
||||
result = service.plan(
|
||||
{
|
||||
"utterance": "打开玩具灯和阅读灯",
|
||||
"peripherals": [
|
||||
{
|
||||
"id": "cap-light-on",
|
||||
"status": "learned",
|
||||
"device_name": "灯光测试玩具",
|
||||
"action": "开启",
|
||||
"risk": "low",
|
||||
}
|
||||
],
|
||||
"vehicle_devices": [],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(len(result["steps"]), 2)
|
||||
self.assertEqual(result["steps"][0]["capability_id"], "cap-light-on")
|
||||
self.assertEqual(result["steps"][1]["command"], "打开全部阅读灯")
|
||||
|
||||
def test_vehicle_execution_reuses_the_same_orchestration_service(self) -> None:
|
||||
service = SelfExtensionService(SimulationGateway())
|
||||
vehicle_service = Mock()
|
||||
vehicle_service.process.return_value = {
|
||||
"status": "succeeded",
|
||||
"provider_used": "local_rules",
|
||||
"request_id": "req-extension-1",
|
||||
"decision_summary": ["打开阅读灯"],
|
||||
"validation": [],
|
||||
"execution_report": {"spec": {"results": [{"status": "succeeded"}]}},
|
||||
"deferred_requests": [],
|
||||
}
|
||||
|
||||
result = service.execute_vehicle(
|
||||
{"command": "打开全部阅读灯", "vehicle_id": "denza_n7"},
|
||||
vehicle_service,
|
||||
hardware_access_bypass=True,
|
||||
)
|
||||
|
||||
self.assertTrue(result["success"])
|
||||
self.assertEqual(result["request_id"], "req-extension-1")
|
||||
vehicle_service.process.assert_called_once()
|
||||
self.assertEqual(vehicle_service.process.call_args.args[0], "打开全部阅读灯")
|
||||
self.assertTrue(vehicle_service.process.call_args.kwargs["hardware_access_bypass"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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.signal_diagnostic import SignalDiagnosticModel
|
||||
from cognitive_agent.application.self_repair.zcan_bridge import ZcanBridgeManager
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user