684 lines
28 KiB
Python
684 lines
28 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from cognitive_agent.application.capability_registry import CapabilityRegistry
|
|
from cognitive_agent.application.native_can_intent import merge_native_can_intent
|
|
from cognitive_agent.application.native_can_runtime import (
|
|
NativeCanCompileError,
|
|
NativeCanExecutor,
|
|
NativeCanRuntime,
|
|
)
|
|
from cognitive_agent.infrastructure.can.gateway import CanFrame
|
|
|
|
|
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
class _FakeBus:
|
|
def __init__(
|
|
self,
|
|
received: list[CanFrame] | None = None,
|
|
*,
|
|
fail_on_calls: set[int] | None = None,
|
|
) -> None:
|
|
self.received = list(received or [])
|
|
self.fail_on_calls = set(fail_on_calls or set())
|
|
self.send_calls = 0
|
|
self.sent: list[CanFrame] = []
|
|
self.closed = False
|
|
|
|
def send(
|
|
self,
|
|
arbitration_id: int,
|
|
data: bytes,
|
|
*,
|
|
is_fd: bool = False,
|
|
bitrate_switch: bool = False,
|
|
) -> CanFrame:
|
|
self.send_calls += 1
|
|
if self.send_calls in self.fail_on_calls:
|
|
raise RuntimeError(f"injected send fault {self.send_calls}")
|
|
frame = CanFrame(
|
|
arbitration_id,
|
|
bytes(data),
|
|
channel=1,
|
|
is_fd=is_fd,
|
|
bitrate_switch=bitrate_switch,
|
|
)
|
|
self.sent.append(frame)
|
|
return frame
|
|
|
|
def receive(
|
|
self, timeout_ms: int, *, include_fd: bool = False
|
|
) -> CanFrame | None:
|
|
return self.received.pop(0) if self.received else None
|
|
|
|
def close(self) -> None:
|
|
self.closed = True
|
|
|
|
|
|
def _native_action(
|
|
capability: str, parameters: dict, target: str = "driver"
|
|
) -> dict:
|
|
return {
|
|
"domain": "seat",
|
|
"capability": capability,
|
|
"target": target,
|
|
"operation": "set",
|
|
"parameters": parameters,
|
|
}
|
|
|
|
|
|
class DriverSeatNativeCanTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.runtime = NativeCanRuntime(REPOSITORY_ROOT)
|
|
|
|
def _compile(
|
|
self, capability: str, parameters: dict, target: str = "driver"
|
|
) -> tuple[dict, dict]:
|
|
config = self.runtime.build_function_config(
|
|
"seat",
|
|
[_native_action(capability, parameters, target)],
|
|
"req-driver-seat",
|
|
"local_rules",
|
|
)
|
|
return config, self.runtime.compile(config)
|
|
|
|
@staticmethod
|
|
def _parsed_action(utterance: str) -> dict | None:
|
|
parsed = merge_native_can_intent({}, utterance)
|
|
actions = parsed.get("native_can_actions", [])
|
|
return actions[0] if len(actions) == 1 else None
|
|
|
|
def test_catalog_exposes_only_driver_and_front_left_native_targets(self) -> None:
|
|
snapshot = CapabilityRegistry(REPOSITORY_ROOT, {"seat"}).snapshot()
|
|
native_seat = next(
|
|
item
|
|
for item in snapshot["domains"]
|
|
if item["domain"] == "seat" and item["availability"] == "native_can"
|
|
)
|
|
capabilities = {item["name"]: item for item in native_seat["capabilities"]}
|
|
self.assertEqual(
|
|
set(capabilities),
|
|
{
|
|
"seat.set_driver_ventilation_native",
|
|
"seat.set_driver_heating_native",
|
|
"seat.set_driver_massage_native",
|
|
"seat.set_driver_axis_native",
|
|
"seat.set_driver_position_native",
|
|
"seat.recall_driver_memory_native",
|
|
},
|
|
)
|
|
for capability in capabilities.values():
|
|
self.assertEqual(set(capability["target_scopes"]), {"driver", "front_left"})
|
|
|
|
def test_exact_commands_preserve_verified_driver_seat_parameters(self) -> None:
|
|
cases = {
|
|
"seat driver vent low": (
|
|
"seat.set_driver_ventilation_native",
|
|
{"state": "on", "level": 2},
|
|
"driver",
|
|
),
|
|
"seat front-left heat high": (
|
|
"seat.set_driver_heating_native",
|
|
{"state": "on", "level": 1},
|
|
"front_left",
|
|
),
|
|
"seat driver massage off": (
|
|
"seat.set_driver_massage_native",
|
|
{"action": "off"},
|
|
"driver",
|
|
),
|
|
"seat driver massage strength strong": (
|
|
"seat.set_driver_massage_native",
|
|
{"action": "strength", "strength": 3},
|
|
"driver",
|
|
),
|
|
"seat driver massage mode relax": (
|
|
"seat.set_driver_massage_native",
|
|
{"action": "mode", "mode": "relax"},
|
|
"driver",
|
|
),
|
|
"seat driver backrest backward 800": (
|
|
"seat.set_driver_axis_native",
|
|
{"axis": "backrest", "direction": "backward", "duration_ms": 800},
|
|
"driver",
|
|
),
|
|
"seat driver height stop": (
|
|
"seat.set_driver_axis_native",
|
|
{"axis": "height", "direction": "stop", "duration_ms": 500},
|
|
"driver",
|
|
),
|
|
"seat driver position 3": (
|
|
"seat.set_driver_position_native",
|
|
{"slot": 3},
|
|
"driver",
|
|
),
|
|
"seat driver memory recall 2": (
|
|
"seat.recall_driver_memory_native",
|
|
{"slot": 2},
|
|
"driver",
|
|
),
|
|
}
|
|
for utterance, expected in cases.items():
|
|
with self.subTest(utterance=utterance):
|
|
action = self._parsed_action(utterance)
|
|
self.assertIsNotNone(action)
|
|
assert action is not None
|
|
self.assertEqual(
|
|
(action["capability"], action["parameters"], action["target"]),
|
|
expected,
|
|
)
|
|
|
|
def test_chinese_commands_require_explicit_driver_scope(self) -> None:
|
|
cases = {
|
|
"打开主驾座椅通风": (
|
|
"seat.set_driver_ventilation_native",
|
|
{"state": "on", "level": 2},
|
|
),
|
|
"主驾座椅加热高档": (
|
|
"seat.set_driver_heating_native",
|
|
{"state": "on", "level": 1},
|
|
),
|
|
"把驾驶位按摩调成波浪模式": (
|
|
"seat.set_driver_massage_native",
|
|
{"action": "mode", "mode": "wave"},
|
|
),
|
|
"主驾座椅按摩强度调高": (
|
|
"seat.set_driver_massage_native",
|
|
{"action": "strength", "strength": 3},
|
|
),
|
|
"主驾座椅向前移动500毫秒": (
|
|
"seat.set_driver_axis_native",
|
|
{"axis": "horizontal", "direction": "forward", "duration_ms": 500},
|
|
),
|
|
"主驾座椅靠背后仰800毫秒": (
|
|
"seat.set_driver_axis_native",
|
|
{"axis": "backrest", "direction": "backward", "duration_ms": 800},
|
|
),
|
|
"把主驾座椅坐盆降低": (
|
|
"seat.set_driver_axis_native",
|
|
{"axis": "basin", "direction": "down", "duration_ms": 500},
|
|
),
|
|
"主驾座椅升高": (
|
|
"seat.set_driver_axis_native",
|
|
{"axis": "height", "direction": "up", "duration_ms": 500},
|
|
),
|
|
"主驾座椅调整到记忆位置3": (
|
|
"seat.recall_driver_memory_native",
|
|
{"slot": 3},
|
|
),
|
|
}
|
|
for utterance, expected in cases.items():
|
|
with self.subTest(utterance=utterance):
|
|
action = self._parsed_action(utterance)
|
|
self.assertIsNotNone(action)
|
|
assert action is not None
|
|
self.assertEqual((action["capability"], action["parameters"]), expected)
|
|
self.assertEqual(action["target"], "driver")
|
|
|
|
def test_parser_rejects_passenger_right_front_and_unsafe_guesses(self) -> None:
|
|
utterances = (
|
|
"seat passenger vent low",
|
|
"seat front-right heat high",
|
|
"打开副驾座椅通风",
|
|
"打开右前座椅加热",
|
|
"打开主驾和副驾座椅通风",
|
|
"seat driver vent low, seat passenger vent low",
|
|
"打开主驾座椅按摩",
|
|
"主驾座椅向前移动50毫秒",
|
|
"seat driver height forward 500",
|
|
)
|
|
for utterance in utterances:
|
|
with self.subTest(utterance=utterance):
|
|
parsed = merge_native_can_intent({}, utterance)
|
|
self.assertFalse(parsed.get("native_can_actions"))
|
|
|
|
def test_deterministic_driver_parse_supersedes_deferred_model_copy(self) -> None:
|
|
parsed = {
|
|
"actions": [],
|
|
"domain_actions": [],
|
|
"native_can_actions": [],
|
|
"deferred_requests": [
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_driver_ventilation_native",
|
|
"target_scope": "driver",
|
|
"parameters": {"state": "on", "level": 2},
|
|
"execution_backend": "native_can",
|
|
}
|
|
],
|
|
"model_requests": [
|
|
{
|
|
"domain": "seat",
|
|
"capability": "seat.set_driver_ventilation_native",
|
|
"target_scope": "driver",
|
|
"operation": "set",
|
|
"parameters": [
|
|
{"name": "state", "value": "on"},
|
|
{"name": "level", "value": 2},
|
|
],
|
|
"reason": "model",
|
|
}
|
|
],
|
|
}
|
|
merged = merge_native_can_intent(parsed, "打开主驾座椅通风")
|
|
self.assertEqual(len(merged["native_can_actions"]), 1)
|
|
self.assertEqual(
|
|
merged["native_can_actions"][0]["capability"],
|
|
"seat.set_driver_ventilation_native",
|
|
)
|
|
|
|
def test_thermal_frames_match_cpp_whitelist_and_feedback_contract(self) -> None:
|
|
cases = (
|
|
(
|
|
"seat.set_driver_ventilation_native",
|
|
{"state": "on", "level": 1},
|
|
"00 01 00 00 00 00 00 00",
|
|
"driver_seat_ventilation:3",
|
|
),
|
|
(
|
|
"seat.set_driver_ventilation_native",
|
|
{"state": "off"},
|
|
"00 03 00 00 00 00 00 00",
|
|
"driver_seat_ventilation:1",
|
|
),
|
|
(
|
|
"seat.set_driver_heating_native",
|
|
{"state": "on", "level": 2},
|
|
"00 20 00 00 00 00 00 00",
|
|
"driver_seat_heating:2",
|
|
),
|
|
(
|
|
"seat.set_driver_heating_native",
|
|
{"state": "off"},
|
|
"00 30 00 00 00 00 00 00",
|
|
"driver_seat_heating:1",
|
|
),
|
|
)
|
|
for capability, parameters, payload, expected in cases:
|
|
with self.subTest(capability=capability, parameters=parameters):
|
|
_, plan = self._compile(capability, parameters)
|
|
command = plan["spec"]["commands"][0]
|
|
self.assertEqual(command["signals"]["frame_id"], "0x431")
|
|
self.assertEqual(command["signals"]["data_hex"], payload)
|
|
self.assertTrue(command["signals"]["is_fd"])
|
|
self.assertTrue(command["signals"]["bitrate_switch"])
|
|
self.assertEqual(command["repeat"], {"count": 3, "period_ms": 100})
|
|
self.assertEqual(command["signals"]["feedback_ids"], "0x4FA")
|
|
self.assertEqual(command["signals"]["expected_feedback"], expected)
|
|
|
|
def test_massage_frames_match_cpp_whitelist(self) -> None:
|
|
cases = (
|
|
({"action": "off"}, "12 00 00 01 00 00 00 00", "driver_seat_massage_mode:1"),
|
|
(
|
|
{"action": "strength", "strength": 3},
|
|
"12 03 00 00 00 00 00 00",
|
|
"driver_seat_massage_strength:11",
|
|
),
|
|
(
|
|
{"action": "mode", "mode": "relax"},
|
|
"12 00 00 06 00 00 00 00",
|
|
"driver_seat_massage_mode:6",
|
|
),
|
|
)
|
|
for parameters, payload, expected in cases:
|
|
with self.subTest(parameters=parameters):
|
|
_, plan = self._compile("seat.set_driver_massage_native", parameters)
|
|
command = plan["spec"]["commands"][0]
|
|
self.assertEqual(command["signals"]["frame_id"], "0x4C1")
|
|
self.assertEqual(command["signals"]["data_hex"], payload)
|
|
self.assertEqual(command["repeat"], {"count": 3, "period_ms": 100})
|
|
self.assertEqual(command["signals"]["feedback_ids"], "0x4FA")
|
|
self.assertEqual(command["signals"]["expected_feedback"], expected)
|
|
|
|
def test_all_four_axes_compile_bounded_motion_and_forced_stop(self) -> None:
|
|
cases = (
|
|
("horizontal", "forward", "05 01 00 00 00 00 00 00"),
|
|
("height", "down", "05 08 00 00 00 00 00 00"),
|
|
("backrest", "forward", "05 10 00 00 00 00 00 00"),
|
|
("basin", "down", "05 80 00 00 00 00 00 00"),
|
|
)
|
|
for axis, direction, payload in cases:
|
|
with self.subTest(axis=axis):
|
|
_, plan = self._compile(
|
|
"seat.set_driver_axis_native",
|
|
{"axis": axis, "direction": direction, "duration_ms": 500},
|
|
)
|
|
command = plan["spec"]["commands"][0]
|
|
self.assertEqual(command["signals"]["frame_id"], "0x4C1")
|
|
self.assertEqual(command["signals"]["data_hex"], payload)
|
|
self.assertEqual(command["repeat"], {"count": 5, "period_ms": 100})
|
|
self.assertEqual(command["signals"]["feedback_ids"], "0x494")
|
|
self.assertEqual(command["signals"]["special_handler"], "driver_seat_axis")
|
|
safety = json.loads(command["signals"]["special_parameters"])
|
|
self.assertEqual(safety["stop_data_hex"], "05 00 00 00 00 00 00 00")
|
|
self.assertEqual(safety["stop_repeat_count"], 3)
|
|
self.assertEqual(safety["stop_period_ms"], 100)
|
|
public = self.runtime.command_frame_public(command)
|
|
self.assertTrue(public["safety"]["forced_stop"])
|
|
|
|
def test_explicit_axis_stop_is_three_fd_stop_frames(self) -> None:
|
|
_, plan = self._compile(
|
|
"seat.set_driver_axis_native",
|
|
{"axis": "height", "direction": "stop"},
|
|
)
|
|
command = plan["spec"]["commands"][0]
|
|
self.assertEqual(command["signals"]["data_hex"], "05 00 00 00 00 00 00 00")
|
|
self.assertEqual(command["repeat"], {"count": 3, "period_ms": 100})
|
|
self.assertNotIn("special_handler", command["signals"])
|
|
|
|
def test_memory_aliases_compile_the_same_learned_twenty_frame_sequence(self) -> None:
|
|
commands = []
|
|
for capability in (
|
|
"seat.set_driver_position_native",
|
|
"seat.recall_driver_memory_native",
|
|
):
|
|
with self.subTest(capability=capability):
|
|
_, plan = self._compile(capability, {"slot": 3})
|
|
command = plan["spec"]["commands"][0]
|
|
commands.append(command)
|
|
self.assertEqual(command["signals"]["frame_id"], "0x4C1")
|
|
self.assertEqual(
|
|
command["signals"]["special_handler"],
|
|
"driver_seat_memory_recall",
|
|
)
|
|
special = json.loads(command["signals"]["special_parameters"])
|
|
self.assertEqual(len(special["frame_sequence"]), 20)
|
|
self.assertEqual(
|
|
special["frame_sequence"][0::2],
|
|
["16 9A 66 3C 76 04 06 00"] * 10,
|
|
)
|
|
self.assertEqual(
|
|
special["frame_sequence"][1::2],
|
|
["15 02 C6 3E 2A C8 FF FF"] * 10,
|
|
)
|
|
self.assertEqual(special["period_ms"], 50)
|
|
public = self.runtime.command_frame_public(command)
|
|
self.assertEqual(public["sequence"]["frame_count"], 20)
|
|
self.assertTrue(
|
|
public["sequence"]["requires_fresh_initialized_0x494"]
|
|
)
|
|
self.assertEqual(
|
|
commands[0]["signals"]["special_parameters"],
|
|
commands[1]["signals"]["special_parameters"],
|
|
)
|
|
|
|
def test_memory_executor_requires_initialized_status_and_alternates_frames(self) -> None:
|
|
config, plan = self._compile(
|
|
"seat.recall_driver_memory_native", {"slot": 2}
|
|
)
|
|
initialized = CanFrame(
|
|
0x494,
|
|
bytes((0, 0, 0, 0, 0, 0, 0, 0x3C)),
|
|
channel=1,
|
|
is_fd=True,
|
|
bitrate_switch=True,
|
|
)
|
|
bus = _FakeBus([initialized, initialized])
|
|
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
|
|
report = NativeCanExecutor(
|
|
lambda: bus, feedback_timeout_ms=1, feedback_required=False
|
|
).execute(self.runtime, plan, config)
|
|
self.assertEqual(report["spec"]["status"], "succeeded")
|
|
self.assertEqual(len(bus.sent), 20)
|
|
self.assertTrue(all(frame.is_fd and frame.bitrate_switch for frame in bus.sent))
|
|
self.assertEqual(
|
|
[frame.data[0] for frame in bus.sent], [0x16, 0x15] * 10
|
|
)
|
|
self.assertEqual(
|
|
[frame.data for frame in bus.sent[0::2]],
|
|
[bytes.fromhex("16 9A 66 3C 76 04 06 00")] * 10,
|
|
)
|
|
self.assertEqual(
|
|
[frame.data for frame in bus.sent[1::2]],
|
|
[bytes.fromhex("15 02 A6 AE 00 C8 FF FF")] * 10,
|
|
)
|
|
telemetry = report["spec"]["results"][0]["telemetry"]
|
|
self.assertFalse(telemetry["physical_effect_confirmed"])
|
|
self.assertEqual(len(telemetry["sent_frames"]), 20)
|
|
|
|
uninitialized_bus = _FakeBus(
|
|
[CanFrame(0x494, bytes(8), channel=1, is_fd=True)]
|
|
)
|
|
rejected = NativeCanExecutor(
|
|
lambda: uninitialized_bus,
|
|
feedback_timeout_ms=1,
|
|
feedback_required=False,
|
|
).execute(self.runtime, plan, config)
|
|
self.assertEqual(rejected["spec"]["status"], "failed")
|
|
self.assertFalse(uninitialized_bus.sent)
|
|
|
|
def test_memory_sequence_stops_immediately_on_send_failure(self) -> None:
|
|
config, plan = self._compile(
|
|
"seat.set_driver_position_native", {"slot": 1}
|
|
)
|
|
initialized = CanFrame(
|
|
0x494, bytes((0, 0, 0, 0, 0, 0, 0, 0x3C)), channel=1
|
|
)
|
|
bus = _FakeBus([initialized], fail_on_calls={4})
|
|
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
|
|
report = NativeCanExecutor(
|
|
lambda: bus, feedback_timeout_ms=1, feedback_required=False
|
|
).execute(self.runtime, plan, config)
|
|
self.assertEqual(report["spec"]["status"], "failed")
|
|
self.assertEqual(bus.send_calls, 4)
|
|
self.assertEqual(len(bus.sent), 3)
|
|
|
|
def test_memory_compile_rejects_missing_or_malformed_learned_slots(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "driver_memory.json"
|
|
path.write_text(
|
|
json.dumps(
|
|
{
|
|
"version": 1,
|
|
"positions": {
|
|
"1": {
|
|
"payload_15": "15 00",
|
|
"payload_16": "16 00 00 00 00 00 00 00",
|
|
}
|
|
},
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
with patch.dict(
|
|
os.environ,
|
|
{"COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH": str(path)},
|
|
):
|
|
config = self.runtime.build_function_config(
|
|
"seat",
|
|
[_native_action("seat.recall_driver_memory_native", {"slot": 1})],
|
|
"req-bad-memory",
|
|
"local_rules",
|
|
)
|
|
with self.assertRaisesRegex(NativeCanCompileError, "8-byte"):
|
|
self.runtime.compile(config)
|
|
|
|
missing = self.runtime.build_function_config(
|
|
"seat",
|
|
[_native_action("seat.recall_driver_memory_native", {"slot": 2})],
|
|
"req-missing-memory",
|
|
"local_rules",
|
|
)
|
|
with self.assertRaisesRegex(NativeCanCompileError, "has not been learned"):
|
|
self.runtime.compile(missing)
|
|
|
|
def test_compiler_fails_closed_for_wrong_seat_or_invalid_parameters(self) -> None:
|
|
for target in ("passenger", "front_right"):
|
|
action = _native_action(
|
|
"seat.set_driver_ventilation_native",
|
|
{"state": "on", "level": 2},
|
|
target,
|
|
)
|
|
with self.subTest(target=target):
|
|
self.assertFalse(self.runtime.supports(action))
|
|
with self.assertRaises(NativeCanCompileError):
|
|
self.runtime.build_function_config(
|
|
"seat", [action], "req-reject", "local_rules"
|
|
)
|
|
self.assertNotIn("seat.set_ventilation", self.runtime.supported_capabilities)
|
|
invalid = (
|
|
(
|
|
"seat.set_driver_ventilation_native",
|
|
{"state": "on", "level": 3},
|
|
),
|
|
(
|
|
"seat.set_driver_massage_native",
|
|
{"action": "mode", "mode": "unknown"},
|
|
),
|
|
(
|
|
"seat.set_driver_axis_native",
|
|
{"axis": "height", "direction": "forward", "duration_ms": 500},
|
|
),
|
|
(
|
|
"seat.set_driver_axis_native",
|
|
{"axis": "horizontal", "direction": "forward", "duration_ms": 2001},
|
|
),
|
|
)
|
|
for capability, parameters in invalid:
|
|
with self.subTest(capability=capability, parameters=parameters):
|
|
config = self.runtime.build_function_config(
|
|
"seat",
|
|
[_native_action(capability, parameters)],
|
|
"req-invalid",
|
|
"local_rules",
|
|
)
|
|
with self.assertRaises(NativeCanCompileError):
|
|
self.runtime.compile(config)
|
|
|
|
def test_thermal_feedback_is_semantically_matched_and_decoded(self) -> None:
|
|
config, plan = self._compile(
|
|
"seat.set_driver_ventilation_native",
|
|
{"state": "on", "level": 1},
|
|
)
|
|
feedback = CanFrame(
|
|
0x4FA,
|
|
bytes((0x0B, 0, 0, 0, 0, 0x81, 0, 0)),
|
|
channel=1,
|
|
is_fd=True,
|
|
bitrate_switch=True,
|
|
)
|
|
bus = _FakeBus([feedback])
|
|
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
|
|
report = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1).execute(
|
|
self.runtime, plan, config
|
|
)
|
|
result = report["spec"]["results"][0]
|
|
self.assertEqual(report["spec"]["status"], "succeeded")
|
|
self.assertTrue(result["telemetry"]["feedback_match"])
|
|
self.assertEqual(result["telemetry"]["feedback_state"]["ventilation"], "high")
|
|
self.assertEqual(result["telemetry"]["feedback_state"]["heating"], "off")
|
|
self.assertEqual(result["telemetry"]["feedback_state"]["massage_mode"], "off")
|
|
self.assertTrue(bus.closed)
|
|
|
|
wrong_bus = _FakeBus(
|
|
[CanFrame(0x4FA, bytes((0x0A, 0, 0, 0, 0, 0x81, 0, 0)), channel=1)]
|
|
)
|
|
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
|
|
wrong_report = NativeCanExecutor(
|
|
lambda: wrong_bus, feedback_timeout_ms=1
|
|
).execute(self.runtime, plan, config)
|
|
self.assertEqual(wrong_report["spec"]["status"], "failed")
|
|
self.assertFalse(
|
|
wrong_report["spec"]["results"][0]["telemetry"]["feedback_match"]
|
|
)
|
|
|
|
def test_massage_feedback_matches_mode_and_strength_fields(self) -> None:
|
|
cases = (
|
|
(
|
|
{"action": "mode", "mode": "relax"},
|
|
bytes((1, 0, 0, 0, 0, 0x86, 0, 0)),
|
|
"relax",
|
|
"off",
|
|
),
|
|
(
|
|
{"action": "strength", "strength": 3},
|
|
bytes((1, 0, 0, 0, 0, 0xB1, 0, 0)),
|
|
"off",
|
|
"strong",
|
|
),
|
|
)
|
|
for parameters, payload, mode, strength in cases:
|
|
with self.subTest(parameters=parameters):
|
|
config, plan = self._compile(
|
|
"seat.set_driver_massage_native", parameters
|
|
)
|
|
bus = _FakeBus([CanFrame(0x4FA, payload, channel=1)])
|
|
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
|
|
report = NativeCanExecutor(
|
|
lambda: bus, feedback_timeout_ms=1
|
|
).execute(self.runtime, plan, config)
|
|
telemetry = report["spec"]["results"][0]["telemetry"]
|
|
self.assertEqual(report["spec"]["status"], "succeeded")
|
|
self.assertTrue(telemetry["feedback_match"])
|
|
self.assertEqual(telemetry["feedback_state"]["massage_mode"], mode)
|
|
self.assertEqual(
|
|
telemetry["feedback_state"]["massage_strength"], strength
|
|
)
|
|
|
|
def test_axis_executor_sends_stop_and_reports_0x494_limitation(self) -> None:
|
|
config, plan = self._compile(
|
|
"seat.set_driver_axis_native",
|
|
{"axis": "horizontal", "direction": "forward", "duration_ms": 500},
|
|
)
|
|
feedback = CanFrame(
|
|
0x494,
|
|
bytes((0, 0, 0, 0, 0, 0, 0, 0x3C)),
|
|
channel=1,
|
|
is_fd=True,
|
|
bitrate_switch=True,
|
|
)
|
|
bus = _FakeBus([feedback])
|
|
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
|
|
report = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1).execute(
|
|
self.runtime, plan, config
|
|
)
|
|
# 0x494 proves only initialization and cannot confirm that the timed
|
|
# motion physically occurred, so strict hardware feedback fails closed.
|
|
self.assertEqual(report["spec"]["status"], "failed")
|
|
self.assertEqual(len(bus.sent), 8)
|
|
self.assertTrue(all(frame.is_fd and frame.bitrate_switch for frame in bus.sent))
|
|
self.assertEqual(
|
|
[frame.data for frame in bus.sent[-3:]],
|
|
[bytes((0x05, 0, 0, 0, 0, 0, 0, 0))] * 3,
|
|
)
|
|
telemetry = report["spec"]["results"][0]["telemetry"]
|
|
self.assertIsNone(telemetry["feedback_match"])
|
|
self.assertTrue(telemetry["feedback_required"])
|
|
self.assertTrue(telemetry["feedback_observed"])
|
|
self.assertEqual(
|
|
telemetry["feedback_state"]["execution_confirmation"],
|
|
"status_frame_only",
|
|
)
|
|
self.assertTrue(all(telemetry["feedback_state"]["initialized"].values()))
|
|
|
|
def test_axis_executor_attempts_every_stop_frame_after_motion_fault(self) -> None:
|
|
config, plan = self._compile(
|
|
"seat.set_driver_axis_native",
|
|
{"axis": "basin", "direction": "up", "duration_ms": 500},
|
|
)
|
|
bus = _FakeBus(fail_on_calls={2})
|
|
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
|
|
report = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1).execute(
|
|
self.runtime, plan, config
|
|
)
|
|
self.assertEqual(report["spec"]["status"], "failed")
|
|
self.assertEqual(bus.send_calls, 5)
|
|
self.assertEqual(
|
|
[frame.data for frame in bus.sent[-3:]],
|
|
[bytes((0x05, 0, 0, 0, 0, 0, 0, 0))] * 3,
|
|
)
|
|
self.assertIn("motion send failed", report["spec"]["results"][0]["message"])
|
|
self.assertTrue(bus.closed)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|