Files
Cognitive_Agent/tests/unit/test_driver_seat_native_can.py
T

780 lines
31 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.memory_environment = patch.dict(
os.environ,
{
"COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH": str(
REPOSITORY_ROOT / "tests" / "fixtures" / "driver_seat_memory.json"
)
},
)
self.memory_environment.start()
self.addCleanup(self.memory_environment.stop)
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": "mode", "mode": "relax"},
),
"主驾座椅按摩强度调高": (
"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.set_driver_position_native",
{"slot": 3},
),
"调用主驾座椅记忆位置2": (
"seat.set_driver_position_native",
{"slot": 2},
),
"恢复主驾座椅到二号记忆位置": (
"seat.set_driver_position_native",
{"slot": 2},
),
"召回主驾座椅2号记忆位置": (
"seat.recall_driver_memory_native",
{"slot": 2},
),
}
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_multiclause_massage_preserves_five_modes_and_three_strengths(
self,
) -> None:
modes = {
"波浪": "wave",
"脉冲": "pulse",
"伸展": "stretch",
"松弛": "slack",
"放松": "relax",
}
strengths = {
"最小": 1,
"中等": 2,
"最大": 3,
}
for mode_text, mode in modes.items():
for strength_text, strength in strengths.items():
utterance = (
f"打开主驾座椅按摩,模式切换为{mode_text}模式,"
f"强度调到{strength_text}"
)
with self.subTest(utterance=utterance):
parsed = merge_native_can_intent({}, utterance)
actions = parsed.get("native_can_actions", [])
self.assertEqual(
[item["parameters"] for item in actions],
[
{"action": "mode", "mode": mode},
{"action": "strength", "strength": strength},
],
)
parsed = merge_native_can_intent(
{},
"打开主驾座椅按摩,然后把模式切换为脉冲模式,"
"同时把强度调到最大",
)
config = self.runtime.build_function_config(
"seat",
parsed["native_can_actions"],
"req-driver-massage-multiclause",
"local_rules",
)
plan = self.runtime.compile(config)
self.assertEqual(
[
command["signals"]["debug_command"]
for command in plan["spec"]["commands"]
],
[
"seat driver massage mode pulse",
"seat driver massage strength strong",
],
)
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(
telemetry["verification_status"], "submitted_unverified"
)
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"], "succeeded")
self.assertEqual(
wrong_report["spec"]["verification_status"],
"submitted_unverified",
)
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. Submission succeeds at the orchestration
# layer while physical verification remains explicitly incomplete.
self.assertEqual(report["spec"]["status"], "succeeded")
self.assertEqual(
report["spec"]["verification_status"],
"submitted_unverified",
)
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.assertEqual(
telemetry["verification_status"], "submitted_unverified"
)
self.assertFalse(telemetry["physical_effect_confirmed"])
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()