Files
Cognitive_Agent/tests/unit/test_vehicle_safety_preflight.py
T

142 lines
4.9 KiB
Python

from __future__ import annotations
import unittest
from unittest.mock import patch
from cognitive_agent.application.native_can_feedback import (
VehicleSafetyPreflightError,
observe_vehicle_safety,
require_stationary_vehicle,
)
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.infrastructure.can.gateway import CanFrame
from cognitive_agent.infrastructure.can.hardware import HardwareSafetyError
class _ReceiveOnlyBus:
def __init__(self, frames: list[CanFrame]) -> None:
self.frames = list(frames)
self.send_called = False
def send(self, *args, **kwargs): # pragma: no cover - a safety tripwire
self.send_called = True
raise AssertionError("passive safety preflight must never transmit")
def receive(self, timeout_ms: int, *, include_fd: bool = False):
return self.frames.pop(0) if self.frames else None
def close(self) -> None:
return None
class _Bridge:
def __init__(self, bus: _ReceiveOnlyBus) -> None:
self.bus = bus
self.closed = False
def channel_bus(self, channel: int) -> _ReceiveOnlyBus:
if channel != 1:
raise AssertionError(f"unexpected channel {channel}")
return self.bus
def close(self) -> None:
self.closed = True
def _gear(raw: int, *, channel: int = 1) -> CanFrame:
data = bytearray(8)
data[3] = raw & 0x0F
return CanFrame(0x342, bytes(data), channel=channel)
def _speed(raw: int, *, channel: int = 1) -> CanFrame:
data = bytearray(8)
data[0] = 0x07
data[1] = raw & 0xFF
data[2] = (raw >> 8) & 0x0F
return CanFrame(0x4EB, bytes(data), channel=channel)
class VehicleSafetyPreflightTests(unittest.TestCase):
def test_fresh_ch1_p_and_zero_speed_pass_without_transmitting(self) -> None:
bus = _ReceiveOnlyBus([_gear(1), _speed(0)])
snapshot = observe_vehicle_safety(bus, timeout_ms=100)
require_stationary_vehicle(snapshot)
self.assertTrue(snapshot["stationary"])
self.assertEqual(snapshot["evidence"]["gear"]["frame_id"], "0x342")
self.assertEqual(snapshot["evidence"]["speed"]["frame_id"], "0x4EB")
self.assertFalse(bus.send_called)
def test_ch0_frames_cannot_satisfy_the_gate(self) -> None:
snapshot = observe_vehicle_safety(
_ReceiveOnlyBus([_gear(1, channel=0), _speed(0, channel=0)]),
timeout_ms=100,
)
self.assertFalse(snapshot["complete"])
self.assertEqual(snapshot["ignored_wrong_channel"], 2)
with self.assertRaisesRegex(
VehicleSafetyPreflightError, "0x342 gear evidence"
):
require_stationary_vehicle(snapshot)
def test_drive_or_nonzero_speed_fails_closed(self) -> None:
for frames, message in (
([_gear(4), _speed(0)], "not confirmed in P"),
([_gear(1), _speed(10)], "not confirmed at 0 km/h"),
):
with self.subTest(message=message):
snapshot = observe_vehicle_safety(
_ReceiveOnlyBus(frames), timeout_ms=100
)
with self.assertRaisesRegex(VehicleSafetyPreflightError, message):
require_stationary_vehicle(snapshot)
def test_policy_cannot_be_redirected_to_ch0(self) -> None:
with self.assertRaisesRegex(VehicleSafetyPreflightError, "only CH1"):
observe_vehicle_safety(
_ReceiveOnlyBus([_gear(1, channel=0), _speed(0, channel=0)]),
timeout_ms=100,
required_channel=0,
)
def test_service_refuses_before_any_command_when_vehicle_is_not_in_p(self) -> None:
service = SeatDemoService(REPOSITORY_ROOT)
action = {
"domain": "climate",
"capability": "climate.set_power",
"target": "vehicle",
"operation": "set",
"parameters": {"state": "on"},
}
config = service.native_can_runtime.build_function_config(
"climate", [action], "req-safety-preflight", "local_rules"
)
plan = service.native_can_runtime.compile(config)
bus = _ReceiveOnlyBus([_gear(4), _speed(0)])
bridge = _Bridge(bus)
with patch.object(
type(service.vehicle_can_settings),
"bridge",
return_value=bridge,
):
with self.assertRaisesRegex(HardwareSafetyError, "not confirmed in P"):
service._execute_orchestrated_reports(
{"native_climate": config},
{"native_climate": plan},
{"native_climate"},
{},
hardware_request=True,
simulated_hardware_request=False,
simulation_fault="normal",
)
self.assertFalse(bus.send_called)
self.assertTrue(bridge.closed)
if __name__ == "__main__":
unittest.main()