mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-09 05:53:42 +08:00
d8708b65c7
Implement conditional driving modes with explicit scene, button and traffic state ownership. Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com> Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com> Co-authored-by: whoisdomi <dcantu83@gmail.com>
385 lines
24 KiB
Python
385 lines
24 KiB
Python
"""Behavioral traces from frozen 678af783 CEM/CCM and manual-state contracts."""
|
|
|
|
from dataclasses import replace
|
|
from typing import cast
|
|
import unittest
|
|
|
|
from openpilot.starpilot.conditional_mode import (
|
|
Authority,
|
|
ConditionalModePolicy,
|
|
LeadEvidence,
|
|
ManualIntent,
|
|
ModeChoice,
|
|
ModeSettings,
|
|
Reason,
|
|
SceneEvidence,
|
|
next_manual_status,
|
|
restore_manual_status,
|
|
)
|
|
|
|
|
|
AUTHORITY = Authority(True, True, False, True, True, True)
|
|
NO_LEAD = LeadEvidence(False, False, False)
|
|
|
|
|
|
def scene(now: float, speed: float = 20.0, **changes) -> SceneEvidence:
|
|
initial = SceneEvidence(
|
|
observed_mono_s=now,
|
|
speed_mps=speed,
|
|
set_speed_mps=speed + 3.0,
|
|
lead=NO_LEAD,
|
|
following_lead=False,
|
|
left_blinker=False,
|
|
right_blinker=False,
|
|
lane_available=True,
|
|
curve_detected=False,
|
|
slow_lead_detected=False,
|
|
stop_light_detected=False,
|
|
slc_experimental=False,
|
|
standstill=False,
|
|
traffic_mode=False,
|
|
stop_sign_confirmed=False,
|
|
forcing_stop=False,
|
|
adjacent_lead_ambiguous=False,
|
|
low_speed_stop_scene=False,
|
|
launch_candidate=False,
|
|
launch_forced_exit=False,
|
|
launch_lead=False,
|
|
)
|
|
return replace(initial, **changes)
|
|
|
|
|
|
class ConditionalModePolicyTest(unittest.TestCase):
|
|
def test_committed_stop_remains_experimental_without_detector_geometry(self):
|
|
policy = ConditionalModePolicy()
|
|
for tick in range(40):
|
|
now = 10. + tick * .05
|
|
evidence = scene(now, speed=0. if tick >= 20 else 8., standstill=tick >= 20,
|
|
stop_light_detected=None, standstill_stop_hold=None, forcing_stop=True)
|
|
result = policy.step(now, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, evidence)
|
|
self.assertEqual(result.reason, Reason.CEM_STOP)
|
|
self.assertTrue(result.effective_longitudinal)
|
|
override = policy.step(12., ModeChoice.CEM, ManualIntent.FORCE_CHILL, AUTHORITY,
|
|
scene(12., forcing_stop=True, stop_light_detected=None))
|
|
self.assertEqual(override.reason, Reason.MANUAL_CHILL)
|
|
self.assertFalse(override.effective_longitudinal)
|
|
unavailable = policy.step(12.05, ModeChoice.CEM, ManualIntent.NONE, replace(AUTHORITY, fresh=False),
|
|
scene(12.05, forcing_stop=True))
|
|
self.assertFalse(unavailable.effective_longitudinal)
|
|
|
|
def test_stock_is_inert_and_lateral_only_is_not_long_authority(self):
|
|
policy = ConditionalModePolicy()
|
|
lateral_only = replace(AUTHORITY, long_active=False)
|
|
stock = policy.step(10.0, ModeChoice.STOCK, ManualIntent.NONE, lateral_only, scene(10.0), stock_experimental=True)
|
|
self.assertTrue(stock.requested_experimental)
|
|
self.assertFalse(stock.effective_longitudinal)
|
|
self.assertEqual(stock.reason, Reason.STOCK)
|
|
self.assertTrue(stock.warm)
|
|
self.assertFalse(policy.step(10.1, ModeChoice.STOCK, ManualIntent.NONE, AUTHORITY, scene(10.1)).requested_experimental)
|
|
no_capability = policy.step(10.2, ModeChoice.STOCK, ManualIntent.NONE, replace(AUTHORITY, system_long_capable=False), scene(10.2), stock_experimental=True)
|
|
self.assertTrue(no_capability.requested_experimental)
|
|
self.assertFalse(no_capability.effective_longitudinal)
|
|
|
|
def test_frozen_manual_wheel_cycle_and_persisted_normalization(self):
|
|
# Frozen experimental_state.py numeric CE/CC statuses: CEM 1=Chill, 2=EXP;
|
|
# CCM 1=EXP, 2=Chill. Both clear the override on the next press.
|
|
self.assertEqual(next_manual_status(ModeChoice.CEM, 0, True), 1)
|
|
self.assertEqual(next_manual_status(ModeChoice.CEM, 0, False), 2)
|
|
self.assertEqual(next_manual_status(ModeChoice.CEM, 2, False), 0)
|
|
self.assertEqual(next_manual_status(ModeChoice.CCM, 0, True), 2)
|
|
self.assertEqual(next_manual_status(ModeChoice.CCM, 0, False), 1)
|
|
self.assertEqual(next_manual_status(ModeChoice.CCM, 1, True), 0)
|
|
self.assertEqual(restore_manual_status(ModeChoice.CEM, 0, 2, True), ManualIntent.FORCE_EXPERIMENTAL)
|
|
self.assertEqual(restore_manual_status(ModeChoice.CEM, 1, 2, True), ManualIntent.FORCE_CHILL)
|
|
self.assertEqual(restore_manual_status(ModeChoice.CEM, 0, 2, False), ManualIntent.NONE)
|
|
self.assertEqual(restore_manual_status(ModeChoice.CCM, 0, 1, True), ManualIntent.FORCE_EXPERIMENTAL)
|
|
self.assertEqual(restore_manual_status(ModeChoice.CCM, 3, 99, True), ManualIntent.NONE)
|
|
|
|
def test_manual_intent_survives_scene_dropout_but_not_authority_dropout(self):
|
|
policy = ConditionalModePolicy()
|
|
stale_scene = scene(8.0)
|
|
forced = policy.step(10.0, ModeChoice.CEM, ManualIntent.FORCE_EXPERIMENTAL, AUTHORITY, stale_scene)
|
|
self.assertTrue(forced.requested_experimental)
|
|
self.assertTrue(forced.effective_longitudinal)
|
|
self.assertEqual((forced.reason, forced.status_code), (Reason.MANUAL_EXPERIMENTAL, 2))
|
|
lateral_only = policy.step(10.1, ModeChoice.CEM, ManualIntent.FORCE_EXPERIMENTAL, replace(AUTHORITY, long_active=False), stale_scene)
|
|
self.assertTrue(lateral_only.requested_experimental)
|
|
self.assertFalse(lateral_only.effective_longitudinal)
|
|
for unavailable in (replace(AUTHORITY, fresh=False), replace(AUTHORITY, system_long_capable=False), replace(AUTHORITY, safe_mode=True)):
|
|
denied = policy.step(10.2, ModeChoice.CEM, ManualIntent.FORCE_EXPERIMENTAL, unavailable, stale_scene)
|
|
self.assertFalse(denied.requested_experimental)
|
|
self.assertEqual(denied.reason, Reason.UNAVAILABLE)
|
|
|
|
def test_cem_frozen_speed_precedence_and_hysteresis_trace(self):
|
|
policy = ConditionalModePolicy()
|
|
settings = replace(ModeSettings(), cem_speed_mps=15.0, cem_curves=True)
|
|
first = policy.step(10.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.0, speed=10.0, curve_detected=True), settings)
|
|
self.assertEqual((first.requested_experimental, first.reason, first.status_code), (True, Reason.CEM_SPEED, 6))
|
|
held = policy.step(10.1, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.1, speed=20.0), settings)
|
|
self.assertEqual((held.reason, held.status_code), (Reason.CEM_HOLD, 6))
|
|
self.assertTrue(held.requested_experimental)
|
|
policy.step(10.3, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.3, speed=20.0), settings)
|
|
self.assertTrue(policy.step(10.49, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.49, speed=20.0), settings).requested_experimental)
|
|
self.assertFalse(policy.step(10.51, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.51, speed=20.0), settings).requested_experimental)
|
|
|
|
def test_cem_frozen_trigger_order_and_unknown_not_false(self):
|
|
policy = ConditionalModePolicy()
|
|
settings = replace(ModeSettings(), cem_open_road=True, cem_signal_mps=15.0, cem_curves=True, cem_curves_with_lead=True)
|
|
open_road = policy.step(1.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(1.0, speed=20.0, set_speed_mps=20.0, curve_detected=True), settings)
|
|
self.assertEqual(open_road.reason, Reason.CEM_OPEN_ROAD)
|
|
signal = policy.step(
|
|
1.1, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(1.1, speed=10.0, left_blinker=True, lane_available=False, curve_detected=True), settings
|
|
)
|
|
self.assertEqual(signal.reason, Reason.CEM_SIGNAL)
|
|
unknown_lane = policy.step(2.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(2.0, speed=10.0, left_blinker=True, lane_available=None), settings)
|
|
self.assertEqual(unknown_lane.reason, Reason.NO_TRIGGER)
|
|
self.assertFalse(unknown_lane.requested_experimental)
|
|
|
|
def test_cem_stale_scene_clears_automatic_hold_and_reentry(self):
|
|
policy = ConditionalModePolicy()
|
|
settings = replace(ModeSettings(), cem_speed_mps=15.0)
|
|
self.assertTrue(policy.step(1.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(1.0, speed=10.0), settings).requested_experimental)
|
|
stale = policy.step(1.1, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(0.5, speed=10.0), settings)
|
|
self.assertEqual((stale.reason, stale.requested_experimental), (Reason.SCENE_UNAVAILABLE, False))
|
|
self.assertEqual(policy.step(1.2, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(1.2, speed=20.0), settings).reason, Reason.NO_TRIGGER)
|
|
|
|
def test_cem_scene_reasons_preserve_frozen_priority_and_status(self):
|
|
policy = ConditionalModePolicy()
|
|
settings = replace(ModeSettings(), cem_curves=True)
|
|
selected = policy.step(
|
|
4.0,
|
|
ModeChoice.CEM,
|
|
ManualIntent.NONE,
|
|
AUTHORITY,
|
|
scene(4.0, curve_detected=True, slow_lead_detected=True, stop_light_detected=True, slc_experimental=True),
|
|
settings,
|
|
)
|
|
self.assertEqual((selected.reason, selected.status_code), (Reason.CEM_CURVE, 3))
|
|
policy.reset()
|
|
selected = policy.step(4.1, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(4.1, stop_light_detected=True, slc_experimental=True), settings)
|
|
self.assertEqual((selected.reason, selected.status_code), (Reason.CEM_STOP, 8))
|
|
policy.reset()
|
|
selected = policy.step(4.2, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(4.2, slc_experimental=True), settings)
|
|
self.assertEqual((selected.reason, selected.status_code), (Reason.CEM_SPEED_LIMIT, 7))
|
|
|
|
def test_cem_slow_lead_hold_requires_credible_current_lead(self):
|
|
lead = LeadEvidence(True, True, False, 45.0, 15.0, 0.0, 0.9)
|
|
for loses_lead in (False, True):
|
|
with self.subTest(loses_lead=loses_lead):
|
|
policy = ConditionalModePolicy()
|
|
first = policy.step(10.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.0, lead=lead, following_lead=True, slow_lead_detected=True))
|
|
self.assertEqual((first.requested_experimental, first.status_code), (True, 4))
|
|
for tick in range(1, 31):
|
|
now = 10.0 + tick * 0.05
|
|
current_lead = NO_LEAD if loses_lead and tick >= 8 else lead
|
|
result = policy.step(now, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(now, lead=current_lead, following_lead=not loses_lead))
|
|
self.assertEqual(result.requested_experimental, tick < (10 if loses_lead else 30))
|
|
# Reappearing geometry without a new slow-lead trigger cannot revive it.
|
|
self.assertFalse(policy.step(11.55, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(11.55, lead=lead, following_lead=True)).requested_experimental)
|
|
|
|
def test_cem_open_road_handoff_is_bounded_and_rechecks_closing_lead(self):
|
|
settings = replace(ModeSettings(), cem_open_road=True)
|
|
for closes_fast in (False, True):
|
|
with self.subTest(closes_fast=closes_fast):
|
|
policy = ConditionalModePolicy()
|
|
first = policy.step(10.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.0, set_speed_mps=20.0), settings)
|
|
self.assertEqual(first.reason, Reason.CEM_OPEN_ROAD)
|
|
for tick in range(1, 18):
|
|
now = 10.0 + tick * 0.05
|
|
lead = LeadEvidence(True, True, True, 60.0, 15.0 if closes_fast and tick >= 11 else 20.0, 0.0)
|
|
result = policy.step(now, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(now, set_speed_mps=20.0, lead=lead, following_lead=True), settings)
|
|
self.assertEqual(result.requested_experimental, tick < (11 if closes_fast else 16))
|
|
|
|
def test_cem_stop_release_suppresses_reentry_but_retains_new_stop_evidence(self):
|
|
settings = replace(ModeSettings(), cem_speed_mps=15.0, cem_open_road=True)
|
|
for release_at_standstill in (False, True):
|
|
with self.subTest(release_at_standstill=release_at_standstill):
|
|
policy = ConditionalModePolicy()
|
|
held = policy.step(10.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.0, speed=0.0, standstill=True, standstill_stop_hold=True), settings)
|
|
self.assertEqual((held.reason, held.status_code, held.requested_experimental), (Reason.CEM_STOP, 8, True))
|
|
if release_at_standstill:
|
|
released = policy.step(
|
|
10.05, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.05, speed=0.0, standstill=True, standstill_stop_hold=False), settings
|
|
)
|
|
self.assertFalse(released.requested_experimental)
|
|
for tick in range(41):
|
|
now = 10.1 + tick * 0.05
|
|
result = policy.step(now, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(now, speed=10.0, set_speed_mps=10.0, slow_lead_detected=True), settings)
|
|
self.assertEqual(result.requested_experimental, tick == 40)
|
|
# A newly qualified stop scene is never masked by launch suppression.
|
|
policy.step(12.15, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(12.15, speed=0.0, standstill=True, standstill_stop_hold=True), settings)
|
|
stop = policy.step(12.2, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(12.2, speed=10.0, stop_light_detected=True), settings)
|
|
self.assertEqual((stop.reason, stop.status_code), (Reason.CEM_STOP, 8))
|
|
|
|
def test_cem_unknown_stop_and_scene_loss_clear_lifecycle_state(self):
|
|
policy = ConditionalModePolicy()
|
|
settings = replace(ModeSettings(), cem_speed_mps=15.0)
|
|
policy.step(10.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.0, speed=0.0, standstill=True, standstill_stop_hold=True), settings)
|
|
unknown = policy.step(10.05, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.05, speed=0.0, standstill=True), settings)
|
|
self.assertEqual((unknown.reason, unknown.requested_experimental), (Reason.SCENE_UNAVAILABLE, False))
|
|
# Missing decisive stop evidence clears remembered latches, not an invented release.
|
|
resumed = policy.step(10.1, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.1, speed=10.0), settings)
|
|
self.assertEqual(resumed.reason, Reason.CEM_SPEED)
|
|
policy.step(10.15, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(9.0, speed=10.0), settings)
|
|
cleared = policy.step(10.2, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.2, speed=20.0), settings)
|
|
self.assertFalse(cleared.requested_experimental)
|
|
|
|
def test_cem_manual_override_preserves_observed_stop_release_quiet_period(self):
|
|
settings = replace(ModeSettings(), cem_speed_mps=15.0)
|
|
for manual_on_release in (False, True):
|
|
with self.subTest(manual_on_release=manual_on_release):
|
|
policy = ConditionalModePolicy()
|
|
policy.step(10.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.0, speed=0.0, standstill=True, standstill_stop_hold=True), settings)
|
|
for tick in range(1, 43):
|
|
now = 10.0 + tick * 0.05
|
|
intent = ManualIntent.FORCE_CHILL if tick == (1 if manual_on_release else 3) else ManualIntent.NONE
|
|
result = policy.step(now, ModeChoice.CEM, intent, AUTHORITY, scene(now, speed=10.0), settings)
|
|
self.assertEqual(result.requested_experimental, tick >= 41)
|
|
# A different mode/session must not inherit that CEM suppression.
|
|
policy.step(12.15, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(12.15), settings)
|
|
resumed = policy.step(12.2, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(12.2, speed=10.0), settings)
|
|
self.assertEqual(resumed.reason, Reason.CEM_SPEED)
|
|
|
|
def test_ccm_frozen_speed_entry_dwell_exit_and_veto(self):
|
|
policy = ConditionalModePolicy()
|
|
|
|
def fast(now):
|
|
return scene(now, speed=55 * 0.44704, set_speed_mps=60 * 0.44704)
|
|
|
|
first = policy.step(10.0, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, fast(10.0))
|
|
self.assertTrue(first.requested_experimental)
|
|
policy.step(10.2, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, fast(10.2))
|
|
policy.step(10.4, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, fast(10.4))
|
|
entered = policy.step(10.5, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, fast(10.5))
|
|
self.assertEqual((entered.reason, entered.status_code, entered.requested_experimental), (Reason.CCM_SPEED, 6, False))
|
|
held = policy.step(10.6, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(10.6, speed=20.0))
|
|
self.assertEqual((held.reason, held.requested_experimental), (Reason.CCM_HOLD, False))
|
|
for now in (10.8, 11.0, 11.2, 11.4):
|
|
policy.step(now, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(now, speed=20.0))
|
|
self.assertFalse(policy.step(11.6, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(11.6, speed=20.0)).requested_experimental)
|
|
self.assertTrue(policy.step(11.71, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(11.71, speed=20.0)).requested_experimental)
|
|
self.assertEqual(policy.step(12.0, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(12.0, speed=25.0, left_blinker=True)).reason, Reason.CCM_VETO)
|
|
|
|
def test_ccm_unknown_veto_cannot_enter_chill(self):
|
|
policy = ConditionalModePolicy()
|
|
fast = scene(10.0, speed=25.0, set_speed_mps=30.0, adjacent_lead_ambiguous=None)
|
|
first = policy.step(10.0, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, fast)
|
|
later = policy.step(10.5, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, replace(fast, observed_mono_s=10.5))
|
|
self.assertEqual((first.reason, later.reason), (Reason.SCENE_UNAVAILABLE, Reason.SCENE_UNAVAILABLE))
|
|
self.assertTrue(later.requested_experimental)
|
|
|
|
def test_ccm_stable_radar_lead_confirmation_and_hard_veto(self):
|
|
policy = ConditionalModePolicy()
|
|
lead = LeadEvidence(True, True, True, distance_m=60.0, speed_mps=21.0, accel_mps2=0.0)
|
|
|
|
def followed(now):
|
|
return scene(now, speed=22.0, lead=lead, following_lead=True)
|
|
|
|
self.assertTrue(policy.step(20.0, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, followed(20.0)).requested_experimental)
|
|
for now in (20.2, 20.4, 20.6):
|
|
policy.step(now, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, followed(now))
|
|
policy.step(20.8, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, followed(20.8))
|
|
self.assertTrue(policy.step(20.9, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, followed(20.9)).requested_experimental)
|
|
entered = policy.step(21.01, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, followed(21.01))
|
|
self.assertEqual((entered.reason, entered.status_code, entered.requested_experimental), (Reason.CCM_LEAD, 4, False))
|
|
veto = policy.step(21.02, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, replace(followed(21.02), slc_experimental=True))
|
|
self.assertEqual((veto.reason, veto.requested_experimental), (Reason.CCM_VETO, True))
|
|
|
|
def test_ccm_qualified_launch_and_missing_launch_proof(self):
|
|
policy = ConditionalModePolicy()
|
|
settings = replace(ModeSettings(), ccm_launch=True)
|
|
qualified = scene(30.0, speed=0.0, standstill=True, launch_candidate=True, low_speed_stop_scene=True)
|
|
launch = policy.step(30.0, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, qualified, settings)
|
|
self.assertEqual((launch.reason, launch.requested_experimental), (Reason.CCM_LAUNCH, False))
|
|
missing = policy.step(30.1, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, replace(qualified, observed_mono_s=30.1, launch_candidate=None), settings)
|
|
self.assertEqual((missing.reason, missing.requested_experimental), (Reason.CCM_VETO, True))
|
|
|
|
def test_ccm_launch_exit_bypasses_dwell_and_keeps_frozen_lead_status(self):
|
|
settings = replace(ModeSettings(), ccm_launch=True)
|
|
for lead_launch in (False, True):
|
|
with self.subTest(lead_launch=lead_launch):
|
|
policy = ConditionalModePolicy()
|
|
launch = policy.step(
|
|
10.0, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(10.0, speed=0.5, launch_candidate=True, launch_lead=lead_launch), settings
|
|
)
|
|
self.assertFalse(launch.requested_experimental)
|
|
self.assertEqual(launch.status_code, 4 if lead_launch else 6)
|
|
exit_result = policy.step(
|
|
10.05, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(10.05, speed=7.0, launch_candidate=False, launch_forced_exit=True), settings
|
|
)
|
|
self.assertTrue(exit_result.requested_experimental)
|
|
self.assertEqual(exit_result.status_code, 0)
|
|
|
|
def test_ccm_launch_expiry_cannot_extend_chill_with_unknown_exit_evidence(self):
|
|
policy = ConditionalModePolicy()
|
|
settings = replace(ModeSettings(), ccm_launch=True)
|
|
policy.step(10.0, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(10.0, speed=0.5, launch_candidate=True), settings)
|
|
missing = policy.step(
|
|
10.05, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(10.05, speed=0.5, launch_candidate=None, launch_forced_exit=None), settings
|
|
)
|
|
self.assertEqual((missing.reason, missing.requested_experimental), (Reason.SCENE_UNAVAILABLE, True))
|
|
|
|
def test_ccm_launch_exit_allows_independent_qualified_speed_candidate(self):
|
|
policy = ConditionalModePolicy()
|
|
settings = replace(ModeSettings(), ccm_launch=True)
|
|
policy.step(10.0, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(10.0, speed=0.5, launch_candidate=True), settings)
|
|
speed = policy.step(
|
|
10.05,
|
|
ModeChoice.CCM,
|
|
ManualIntent.NONE,
|
|
AUTHORITY,
|
|
scene(10.05, speed=25.0, set_speed_mps=30.0, launch_candidate=False, launch_forced_exit=True),
|
|
settings,
|
|
)
|
|
self.assertEqual((speed.reason, speed.requested_experimental, speed.status_code), (Reason.CCM_SPEED, False, 6))
|
|
|
|
def test_mode_switch_discards_previous_timers(self):
|
|
policy = ConditionalModePolicy()
|
|
settings = replace(ModeSettings(), cem_speed_mps=15.0)
|
|
self.assertTrue(policy.step(1.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(1.0, speed=10.0), settings).requested_experimental)
|
|
changed = policy.step(1.1, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(1.1, speed=20.0), settings)
|
|
self.assertEqual((changed.reason, changed.requested_experimental), (Reason.NO_TRIGGER, True))
|
|
back = policy.step(1.2, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(1.2, speed=20.0), settings)
|
|
self.assertEqual((back.reason, back.requested_experimental), (Reason.NO_TRIGGER, False))
|
|
|
|
def test_manual_override_clears_pending_confirmation_and_old_hold(self):
|
|
policy = ConditionalModePolicy()
|
|
fast = scene(10.0, speed=25.0, set_speed_mps=30.0)
|
|
self.assertTrue(policy.step(10.0, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, fast).requested_experimental)
|
|
manual = policy.step(10.1, ModeChoice.CCM, ManualIntent.FORCE_CHILL, AUTHORITY, replace(fast, observed_mono_s=10.1))
|
|
self.assertEqual((manual.reason, manual.requested_experimental), (Reason.MANUAL_CHILL, False))
|
|
resumed = policy.step(10.2, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, replace(fast, observed_mono_s=10.2))
|
|
self.assertEqual((resumed.reason, resumed.requested_experimental), (Reason.NO_TRIGGER, True))
|
|
policy.step(10.4, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, replace(fast, observed_mono_s=10.4))
|
|
self.assertFalse(policy.step(10.56, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, replace(fast, observed_mono_s=10.56)).requested_experimental)
|
|
|
|
policy = ConditionalModePolicy()
|
|
settings = replace(ModeSettings(), cem_speed_mps=15.0)
|
|
self.assertTrue(policy.step(20.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(20.0, speed=10.0), settings).requested_experimental)
|
|
policy.step(20.1, ModeChoice.CEM, ManualIntent.FORCE_CHILL, AUTHORITY, scene(20.1, speed=20.0), settings)
|
|
resumed = policy.step(20.2, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(20.2, speed=20.0), settings)
|
|
self.assertEqual((resumed.reason, resumed.requested_experimental), (Reason.NO_TRIGGER, False))
|
|
|
|
def test_raw_mode_string_is_rejected_not_misrouted(self):
|
|
policy = ConditionalModePolicy()
|
|
# Deliberately bypass the type annotation to exercise the runtime boundary.
|
|
result = policy.step(
|
|
10.0, cast(ModeChoice, 'conditional_experimental'), ManualIntent.NONE, AUTHORITY, scene(10.0, speed=10.0), replace(ModeSettings(), cem_speed_mps=15.0)
|
|
)
|
|
self.assertEqual((result.reason, result.requested_experimental), (Reason.UNAVAILABLE, False))
|
|
|
|
def test_invalid_values_and_clock_rollback_fail_closed(self):
|
|
policy = ConditionalModePolicy()
|
|
settings = replace(ModeSettings(), ccm_speed_mps=10**350)
|
|
result = policy.step(10.0, ModeChoice.CCM, ManualIntent.NONE, AUTHORITY, scene(10.0), settings)
|
|
self.assertEqual(result.reason, Reason.UNAVAILABLE)
|
|
self.assertFalse(result.requested_experimental)
|
|
no_speed = policy.step(10.1, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(10.1, speed=float('nan')))
|
|
self.assertEqual(no_speed.reason, Reason.SCENE_UNAVAILABLE)
|
|
rollback = policy.step(9.0, ModeChoice.CEM, ManualIntent.NONE, AUTHORITY, scene(9.0))
|
|
self.assertEqual(rollback.reason, Reason.UNAVAILABLE)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|