Files
StarPilot/openpilot/starpilot/sentry_mode/tests/test_policy.py
T
firestar5683 e52e1a6a86 Sentry
Implement Sentry scanning, notification state and worker cancellation through explicit policies and preferences.
2026-10-14 19:56:17 -05:00

141 lines
6.1 KiB
Python

"""Frozen numeric motion behavior and fail-closed parked policy boundaries."""
from unittest.mock import Mock
import pytest
from openpilot.starpilot.sentry_mode.policy import (
Inputs, MotionSample, SentryPolicy, Settings,
)
def active(sample: MotionSample | None = None) -> Inputs:
return Inputs(enabled=True, offroad=True, voltage_ok=True, device_fresh=True, ignition_off=True, sample=sample)
def motion(now: float, z: float) -> MotionSample:
return MotionSample(now, (0.0, 0.0, z))
def fresh_through(policy: SentryPolicy, start: float, end: float, z: float = 9.8):
result = None
for tenth in range(round(start * 10) + 1, round(end * 10) + 1):
now = tenth / 10
result = policy.update(now, active(motion(now, z)))
return result
def armed(policy: SentryPolicy) -> None:
assert policy.update(0.0, active(motion(0.0, 9.8))).state == "arming"
assert fresh_through(policy, 0.0, 89.9).state == "arming"
assert policy.update(90.0, active(motion(90.0, 9.8))).state == "armed"
def test_frozen_arming_warning_and_alarm_numeric_sequence():
policy = SentryPolicy(Settings(sensitivity=0.1, warning_time_seconds=0.3))
armed(policy)
assert policy.settings.warning_trigger_count == 3
assert policy.update(90.1, active(motion(90.1, 10.0))).event is None
assert policy.update(90.2, active(motion(90.2, 9.8))).event is None
assert policy.update(90.3, active(motion(90.3, 10.0))).event == "warning"
assert policy.update(90.4, active(motion(90.4, 9.8))).event is None
# The frozen detector requires strictly more than 25 changes and 30 s from
# the first change. Feed 10 Hz fresh stable samples while that time elapses.
for index in range(5, 26):
now = 90.0 + index * 0.1
assert policy.update(now, active(motion(now, 10.0 if index % 2 else 9.8))).event is None
assert policy.trigger_count == 25
assert fresh_through(policy, 92.5, 120.0, 10.0).event is None
assert policy.update(120.1, active(motion(120.1, 9.8))).event == "alarm"
assert policy.update(120.2, active(motion(120.2, 10.0))).event is None
def test_duplicate_stale_invalid_and_out_of_order_samples_do_not_accumulate():
policy = SentryPolicy(Settings(sensitivity=0.1, warning_time_seconds=0.2))
armed(policy)
assert policy.update(90.1, active(motion(90.1, 10.0))).event is None
assert policy.update(90.2, active(motion(90.1, 9.8))).state == "sensor_unavailable"
assert policy.trigger_count == 0
assert policy.update(90.3, active(motion(90.1, 9.8))).state == "sensor_unavailable"
assert policy.update(90.4, active(motion(90.4, 9.8))).state == "arming"
assert policy.update(90.5, active(motion(90.3, 10.0))).state == "sensor_unavailable"
assert policy.update(90.6, active(motion(90.6, 9.8))).state == "arming"
def test_missing_sensor_during_arming_and_after_armed_requires_full_rearm():
policy = SentryPolicy()
assert policy.update(0.0, active()).state == "sensor_unavailable"
assert policy.update(0.1, active(motion(0.1, 9.8))).state == "arming"
assert fresh_through(policy, 0.1, 90.0).state == "arming"
assert policy.update(90.1, active()).state == "sensor_unavailable"
assert policy.update(90.2, active(motion(90.2, 9.8))).state == "arming"
assert fresh_through(policy, 90.2, 180.3).state == "armed"
assert policy.update(180.4, active()).state == "sensor_unavailable"
assert policy.update(180.5, active(motion(180.5, 9.8))).state == "arming"
def test_disabled_onroad_low_voltage_and_lost_device_rearm_for_full_90_seconds():
for stopped, expected in ((Inputs(False, True, True, True, True), "disabled"),
(Inputs(True, False, True, True, True), "disabled_onroad"),
(Inputs(True, True, False, True, True), "low_voltage"),
(Inputs(True, True, True, False, True), "unavailable"),
(Inputs(True, True, True, True, False), "disabled_ignition")):
policy = SentryPolicy()
armed(policy)
assert policy.update(90.1, active(motion(90.1, 10.0))).state == "armed"
assert policy.update(90.2, stopped).state == expected
assert policy.update(90.3, active(motion(90.3, 9.8))).state == "arming"
assert fresh_through(policy, 90.3, 180.2).state == "arming"
assert fresh_through(policy, 180.2, 180.4).state == "armed"
assert policy.trigger_count == 0
def test_monotonic_reversal_and_motion_window_expiry_fail_closed():
policy = SentryPolicy(Settings(sensitivity=0.1, warning_time_seconds=0.2))
armed(policy)
policy.update(90.1, active(motion(90.1, 10.0)))
assert policy.update(60.0, active(motion(60.0, 9.8))).state == "unavailable"
assert policy.update(60.1, active(motion(60.1, 9.8))).state == "arming"
assert fresh_through(policy, 60.1, 150.0).state == "arming"
assert fresh_through(policy, 150.0, 150.2).state == "armed"
assert policy.update(150.3, active(motion(150.3, 10.0))).event is None
assert fresh_through(policy, 150.3, 210.2, 10.0).event is None
assert policy.update(210.3, active(motion(210.3, 9.8))).event is None
assert policy.trigger_count == 1
def test_two_samples_90_seconds_apart_never_arm():
policy = SentryPolicy()
assert policy.update(0.0, active(motion(0.0, 9.8))).state == "arming"
assert policy.update(90.0, active(motion(90.0, 9.8))).state == "unavailable"
assert policy.update(90.1, active(motion(90.1, 9.8))).state == "arming"
assert fresh_through(policy, 90.1, 180.2).state == "armed"
@pytest.mark.parametrize("settings", [
{"sensitivity": 0.004}, {"sensitivity": float("nan")},
{"sensitivity": True}, {"sensitivity": "0.04"},
{"warning_time_seconds": 10.1}, {"warning_time_seconds": float("inf")},
{"warning_time_seconds": False},
])
def test_settings_reject_invalid_values(settings):
with pytest.raises(ValueError):
Settings(**settings)
@pytest.mark.parametrize("sample", [
(float("nan"), (0.0, 0.0, 9.8)),
(1.0, (0.0, float("inf"), 9.8)),
(1.0, (1e308, 0.0, 9.8)),
(1.0, (True, 0.0, 9.8)),
(1.0, (0.0, 9.8)),
])
def test_sample_requires_finite_time_and_three_axes(sample):
with pytest.raises(ValueError):
MotionSample(*sample)
def test_authority_inputs_reject_truthy_strings():
with pytest.raises(ValueError):
Mock(wraps=Inputs)("off", True, True, True, True)