from itertools import permutations, product import math from typing import Any, cast import unittest from dataclasses import replace from openpilot.starpilot.speed_limits.acceptance import Candidate, IdentityKind, Observation, ObservationIdentity, ObservationKind from openpilot.starpilot.speed_limits.acceptance import (AcceptedLimit, ActionKind, AdoptRequest, Authority, DriverAction, LongitudinalOwner, Mode, Policy, new_session, step) from openpilot.starpilot.speed_limits.selection import SelectionMode, SelectionPolicy, Source, select_limit def geographic(value): return ObservationIdentity(IdentityKind.GEOGRAPHIC, value=value) def observations(**speeds): values = {source: Observation(ObservationKind.ABSENT) for source in Source} values.update({Source(source): Observation(ObservationKind.VALID, Candidate(source, geographic(f"{source}-zone"), speed)) for source, speed in speeds.items()}) return values def policy(mode=SelectionMode.ORDERED, slots=(Source.VISION, Source.MAP), online=True): return SelectionPolicy(mode, slots, online) class TestSourceSelection(unittest.TestCase): def test_priority_order_and_unused_source(self): supplied = observations(dashboard=30, map=20, vision=10, online=40) for first, second in permutations((Source.DASHBOARD, Source.MAP, Source.VISION), 2): with self.subTest(first=first, second=second): selected = select_limit(supplied, policy(slots=(first, second))) self.assertIs(selected.observation, supplied[first]) self.assertEqual(selected.selected_source, first) missing = dict(supplied) missing[first] = Observation(ObservationKind.ABSENT) self.assertEqual(select_limit(missing, policy(slots=(first, second))).selected_source, second) self.assertEqual(select_limit(supplied, policy(slots=(Source.MAP, Source.MAP))).selected_source, Source.MAP) def test_extrema_vision_requires_explicit_slot(self): for mode, vision in ((SelectionMode.HIGHEST, 40), (SelectionMode.LOWEST, 5)): for slots in ((Source.MAP, Source.DASHBOARD), (None, None), (Source.MAP, Source.MAP)): with self.subTest(mode=mode, slots=slots): selected = select_limit(observations(dashboard=30, map=20, vision=vision), policy(mode, slots)) self.assertEqual(selected.selected_source, Source.DASHBOARD if mode == SelectionMode.HIGHEST else Source.MAP) self.assertNotIn(Source.VISION, selected.considered) for slots in ((Source.VISION, None), (None, Source.VISION)): selected = select_limit(observations(dashboard=30, map=20, vision=vision), policy(mode, slots)) self.assertEqual(selected.selected_source, Source.VISION) def test_extrema_ties_keep_stable_dashboard_map_vision_order(self): original = observations(dashboard=20, map=20, vision=20) for mode in (SelectionMode.HIGHEST, SelectionMode.LOWEST): for order in permutations(Source): selected = select_limit({source: original[source] for source in order}, policy(mode)) self.assertEqual(selected.selected_source, Source.DASHBOARD) original_without_dashboard = dict(original) original_without_dashboard[Source.DASHBOARD] = Observation(ObservationKind.ABSENT) self.assertEqual(select_limit(original_without_dashboard, policy(mode)).selected_source, Source.MAP) def test_online_never_preempts_valid_primary(self): for mode in SelectionMode: for online in (1.0, 50.0): selected = select_limit(observations(map=20, online=online), policy(mode)) self.assertEqual(selected.selected_source, Source.MAP) self.assertNotIn(Source.ONLINE, selected.considered) def test_online_fallback_and_disabled_provider(self): # Dashboard is valid but not configured in ordered mode; it cannot preempt fallback. supplied = observations(dashboard=30, online=20) selected = select_limit(supplied, policy()) self.assertEqual(selected.selected_source, Source.ONLINE) self.assertEqual(selected.reason, "online_fallback") for slots in ((Source.VISION, Source.MAP), (None, None)): selected = select_limit(supplied, policy(slots=slots, online=False)) self.assertIsNone(selected.selected_source) self.assertEqual(selected.observation.kind, ObservationKind.ABSENT) def test_incomplete_sources_do_not_become_absent(self): missing = (ObservationKind.ABSENT, ObservationKind.UNKNOWN, ObservationKind.STALE) for first, second, online in product(missing, repeat=3): supplied = observations() for source, kind in ((Source.VISION, first), (Source.MAP, second), (Source.ONLINE, online)): supplied[source] = Observation(kind) selected = select_limit(supplied, policy()) with self.subTest(first=first, second=second, online=online): self.assertIsNone(selected.selected_source) self.assertIsNone(selected.observation.candidate) if ObservationKind.UNKNOWN in (first, second, online): self.assertEqual(selected.observation.kind, ObservationKind.UNKNOWN) elif ObservationKind.STALE in (first, second, online): self.assertEqual(selected.observation.kind, ObservationKind.STALE) else: self.assertEqual(selected.observation.kind, ObservationKind.ABSENT) self.assertEqual(selected.unavailable, ((Source.VISION, first), (Source.MAP, second), (Source.ONLINE, online))) def test_healthy_alternative_can_replace_unavailable_source(self): for unavailable in (ObservationKind.UNKNOWN, ObservationKind.STALE): supplied = observations(map=20, online=25) supplied[Source.VISION] = Observation(unavailable) selected = select_limit(supplied, policy()) self.assertEqual(selected.selected_source, Source.MAP) self.assertIn((Source.VISION, unavailable), selected.unavailable) supplied[Source.MAP] = Observation(ObservationKind.ABSENT) self.assertEqual(select_limit(supplied, policy()).selected_source, Source.ONLINE) def test_disabled_or_unselected_unknown_sources_do_not_block_absence(self): supplied = observations() supplied[Source.VISION] = supplied[Source.ONLINE] = Observation(ObservationKind.UNKNOWN) selected = select_limit(supplied, policy(slots=(Source.DASHBOARD, Source.MAP), online=False)) self.assertEqual(selected.observation.kind, ObservationKind.ABSENT) def test_minimum_threshold_is_explicit_and_inclusive(self): supplied = observations(vision=math.nextafter(1.0, 0.0), map=1.0) selected = select_limit(supplied, policy()) self.assertEqual(selected.selected_source, Source.MAP) self.assertEqual(selected.below_minimum, (Source.VISION,)) supplied[Source.MAP] = Observation(ObservationKind.ABSENT) selected = select_limit(supplied, policy(online=False)) self.assertEqual(selected.observation.kind, ObservationKind.ABSENT) def test_malformed_candidates_fail_closed(self): candidates = [Candidate("dashboard", geographic("zone"), 20), Candidate("vision", geographic(""), 20), Candidate("vision", geographic("zone"), float("nan")), Candidate("vision", geographic("zone"), float("inf")), Candidate("vision", geographic("zone"), 0), Candidate("vision", geographic("zone"), -1), Candidate("vision", geographic("zone"), True), Candidate("vision", geographic("zone"), 10 ** 400), Candidate("vision", geographic("zone\x00"), 20)] for candidate in candidates: supplied = observations(map=20, online=30) supplied[Source.VISION] = Observation(ObservationKind.VALID, candidate) with self.subTest(candidate=candidate): selected = select_limit(supplied, policy()) self.assertEqual(selected.reason, "invalid_input") self.assertEqual(selected.observation.kind, ObservationKind.UNKNOWN) self.assertIsNone(selected.selected_source) self.assertTrue(selected.errors) def test_structural_input_errors_fail_closed(self): supplied = observations(vision=20) invalid_observations: list[Any] = [None, {}, {source.value: value for source, value in supplied.items()}, {**supplied, Source.MAP: Observation(cast(Any, "valid"), Candidate("map", geographic("zone"), 20))}, {**supplied, Source.MAP: Observation(ObservationKind.STALE, Candidate("map", geographic("zone"), 20))}, {**supplied, Source.MAP: Observation(ObservationKind.VALID)}] invalid_policies: list[Any] = [None, SelectionPolicy(cast(Any, "ordered"), (Source.VISION, Source.MAP), True), SelectionPolicy(SelectionMode.ORDERED, (Source.ONLINE, Source.MAP), True), SelectionPolicy(SelectionMode.ORDERED, cast(Any, (Source.VISION,)), True), SelectionPolicy(SelectionMode.ORDERED, (Source.VISION, Source.MAP), cast(Any, 1))] for malformed in invalid_observations: self.assertEqual(select_limit(malformed, policy()).reason, "invalid_input") for malformed in invalid_policies: self.assertEqual(select_limit(supplied, malformed).reason, "invalid_input") def test_rejected_limit_never_becomes_selected_fallback(self): for mode in (Mode.LONGITUDINAL_ONLY, Mode.COMBINED): with self.subTest(mode=mode): authority = Authority(mode, LongitudinalOwner.SYSTEM, mode == Mode.COMBINED, True, False, False) acceptance_policy = Policy(confirm_lower=True, confirm_higher=False, fallback_previous=True) source_policy = policy(slots=(Source.DASHBOARD, None), online=False) candidate = observations(dashboard=29.0576)[Source.DASHBOARD].candidate state = replace(new_session("selection-regression", AcceptedLimit(candidate, "selection-regression", 0)), received_speed_mps=29.0576) # Values and sequence reproduce the observed accepted65 / reject45 / lost-source case. for index, (speed, reject) in enumerate(((29.0576, False), (20.1168, False), (20.1168, True), (None, False))): chosen = select_limit(observations(dashboard=speed) if speed is not None else observations(), source_policy) action = DriverAction(state.session_id, 1, state.pending.decision_id, ActionKind.REJECT) if reject else None decision = step(state, chosen.observation, authority, acceptance_policy, now_ns=index * 50_000_000, action=action) self.assertFalse(decision.errors) self.assertEqual(decision.control_target_mps, 29.0576) self.assertEqual(decision.state.accepted.candidate.speed_mps, 29.0576) self.assertEqual(decision.state.pending is not None, index == 1) if index > 0: self.assertIsNone(decision.history_write) if reject: self.assertTrue(decision.action_receipt.consumed) state = decision.state returned = select_limit(observations(dashboard=20.1168), source_policy) decision = step(state, returned.observation, authority, acceptance_policy, now_ns=200_000_000) self.assertEqual(decision.basis, "rejected") self.assertEqual(decision.control_target_mps, 29.0576) def test_uncertainty_and_axis_ownership_survive_composition(self): system = Authority(Mode.LONGITUDINAL_ONLY, LongitudinalOwner.SYSTEM, False, True, False, False) acceptance_policy = Policy(confirm_higher=False, fallback_previous=True) source_policy = policy(slots=(Source.MAP, None), online=False) history = AcceptedLimit(observations(map=20)[Source.MAP].candidate, "uncertainty", 0) state = replace(new_session("uncertainty", history), received_speed_mps=20) accepted = step(state, select_limit(observations(map=20), source_policy).observation, system, acceptance_policy, now_ns=0) for kind in (ObservationKind.UNKNOWN, ObservationKind.STALE): supplied = observations() supplied[Source.MAP] = Observation(kind) selected = select_limit(supplied, source_policy) decision = step(accepted.state, selected.observation, system, acceptance_policy, now_ns=1) self.assertEqual(decision.state.accepted, accepted.state.accepted) self.assertEqual(decision.control_target_mps, 20) self.assertEqual(decision.state.observation.kind, kind) for authority in (Authority(Mode.LATERAL_ONLY, LongitudinalOwner.NONE, True, False, False, False), Authority(Mode.LATERAL_ONLY, LongitudinalOwner.STOCK, True, False, True, False), Authority(Mode.OFF, LongitudinalOwner.NONE, False, False, False, True)): selected = select_limit(observations(), source_policy) decision = step(accepted.state, selected.observation, authority, acceptance_policy, now_ns=1) self.assertEqual(decision.state.accepted, accepted.state.accepted) self.assertIsNone(decision.control_target_mps) def test_selector_preserves_identity_kind_without_inventing_geography(self): for identity in (geographic("road-1"), ObservationIdentity(IdentityKind.PRODUCER_EPISODE, value="episode-1"), ObservationIdentity(IdentityKind.SESSION_VALUE, session_id="drive")): supplied = observations() candidate = Candidate("dashboard", identity, 20) supplied[Source.DASHBOARD] = Observation(ObservationKind.VALID, candidate) selected = select_limit(supplied, policy(slots=(Source.DASHBOARD, None))) self.assertIs(selected.observation.candidate, candidate) self.assertIs(selected.observation.candidate.observation_identity, identity) self.assertEqual(selected.errors, ()) def test_acceptance_checks_session_after_structural_source_selection(self): supplied = observations() supplied[Source.DASHBOARD] = Observation( ObservationKind.VALID, Candidate("dashboard", ObservationIdentity(IdentityKind.SESSION_VALUE, session_id="previous-drive"), 20)) selected = select_limit(supplied, policy(slots=(Source.DASHBOARD, None))) self.assertEqual(selected.errors, ()) authority = Authority(Mode.LONGITUDINAL_ONLY, LongitudinalOwner.SYSTEM, False, True, False, False) decision = step(new_session("current-drive"), selected.observation, authority, Policy(False, False), now_ns=0) self.assertIn("invalid observation", decision.errors) self.assertIsNone(decision.control_target_mps) self.assertIsNone(decision.history_write) def test_dashboard_session_value_rejection_and_adopt_through_selection(self): authority = Authority(Mode.LONGITUDINAL_ONLY, LongitudinalOwner.SYSTEM, False, True, False, False) acceptance_policy = Policy(confirm_higher=False, fallback_previous=True) selection_policy = policy(slots=(Source.DASHBOARD, None), online=False) candidate = Candidate("dashboard", ObservationIdentity(IdentityKind.SESSION_VALUE, session_id="drive"), 29.0576) state = replace(new_session("drive", AcceptedLimit(candidate, "drive", 0)), received_speed_mps=29.0576) for index, speed in enumerate((29.0576, 20.1168, 20.1168, None, 20.1168)): supplied = observations() if speed is not None: supplied[Source.DASHBOARD] = Observation( ObservationKind.VALID, Candidate("dashboard", ObservationIdentity(IdentityKind.SESSION_VALUE, session_id="drive"), speed)) selected = select_limit(supplied, selection_policy) action = DriverAction("drive", 0, state.pending.decision_id, ActionKind.REJECT) if index == 2 else None decision = step(state, selected.observation, authority, acceptance_policy, now_ns=index, action=action) self.assertEqual(decision.errors, ()) self.assertEqual(decision.control_target_mps, 29.0576) state = decision.state self.assertEqual(decision.basis, "rejected") shown = state.presentation request = AdoptRequest("drive", 1, shown.presentation_id, shown.candidate) adopted = step(state, selected.observation, authority, acceptance_policy, now_ns=5, adopt=request) self.assertTrue(adopted.adoption_receipt.consumed) self.assertEqual(adopted.control_target_mps, 20.1168) self.assertEqual(adopted.state.rejected, frozenset()) self.assertEqual(adopted.adoption.action_sequence_id, request.sequence_id) self.assertEqual(adopted.history_write.candidate, shown.candidate) if __name__ == "__main__": unittest.main()