fix(slc): prevent confirmation prompts when longitudinal control is disengaged

This commit is contained in:
firestarsdog
2026-10-30 23:30:22 -04:00
parent c3d5939ffe
commit 2fd0741f56
11 changed files with 221 additions and 30 deletions
+2
View File
@@ -440,6 +440,8 @@ def starpilot_main():
if conditional_host is not None:
subscribed_services.append('starpilotRadarState')
optional_slc = ['slcDashboardObservation', 'slcVisionObservation', 'mapdOut', 'starpilotCarState'] if slc_available else []
if slc_available and feature_enabled(params, CP, 'aol', os.environ):
optional_slc.append('aolAxisState')
subscribed_services.extend(optional_slc)
subscribed_services.append('starpilotNavigation')
optional_conditional = ['starpilotNavigation'] + (['starpilotRadarState'] if conditional_host is not None else []) + optional_slc
+10 -4
View File
@@ -54,8 +54,12 @@ The source and arrow share a centered row; all sign contents use the same geomet
on both screens. Small arrows are baked at double resolution and text is aligned
to final pixels for clear native rendering.
Steering-wheel + accepts and − declines. Tapping the sign opens the same
Accept / Reject choices on either native screen. A pending manual choice expires
after ten monotonic seconds, including time without active longitudinal control.
Accept / Reject choices on either native screen. Manual confirmations appear only
while system longitudinal control is engaged, including a temporary accelerator
override. Disengaged and AOL lateral-only modes keep displaying limits without
prompting. Losing longitudinal engagement cancels the pending choice without
declining it; re-engaging starts a fresh choice if needed. A pending manual choice
expires after ten monotonic seconds while engaged.
Decline and timeout leave set speed unchanged. An action at the exact deadline
can still complete its presented decision; a later action cannot accept it.
@@ -83,8 +87,10 @@ This is approximate timing, not a promise to reach the target speed at the sign.
The first offer latches one boundary encounter. Slowing or stopping does not
retract it or restart the existing ten-second confirmation deadline. Source loss
removes authority and hides the pending prompt; fresh recovery of the same
boundary uses the original deadline and a current presentation. Decline, expiry,
manual override and completed application survive the boundary becoming current.
boundary uses the original deadline and a current presentation. Losing longitudinal
engagement cancels an unanswered choice; re-engaging starts a fresh deadline.
Decline, expiry, manual override and completed application survive the boundary
becoming current.
An unchanged approach limit cannot be offered again solely because the preview
is lost or disabled. Current-source evidence, command freshness, expected SET,
driver-button checks and Toyota's native capacity/release checks still apply.
@@ -424,6 +424,9 @@ def step(state: State, observation: Observation, authority: Authority, policy: P
return finish("accepted", target=previous_target())
confirmation = policy.confirm_higher if previous_speed is None or candidate.speed_mps > previous_speed else (
policy.confirm_lower if candidate.speed_mps < previous_speed else policy.confirm_lower or policy.confirm_higher)
if confirmation and not authority.can_control:
state = replace(state, pending=None, timer_running=False)
return finish("inactive")
if not new_limit and old.pending is not None and old.pending.automatic == (not confirmation):
pending = replace(old.pending, candidate=candidate)
if old.timer_running:
@@ -452,9 +455,6 @@ def step(state: State, observation: Observation, authority: Authority, policy: P
elif old.presentation is None or old.presentation.candidate != candidate:
receipt = replace(receipt, status="different_presentation")
else:
# The driver is always asked, even with only lateral or stock ACC active; the
# choice is recorded as accepted history and only commands a target when
# this system owns longitudinal control.
receipt = replace(receipt, consumed=True, status=action.kind.value)
if action.kind is ActionKind.ACCEPT:
state, history = _accept(state, candidate, now_ns)
+8 -2
View File
@@ -12,7 +12,7 @@ class Encounter:
from_mps: float
speed_mps: float
way_id: int
started_ns: int
started_ns: int | None
crossed: bool = False
declined: bool = False
withdrawn: bool = False
@@ -40,10 +40,16 @@ class Advance:
self.encounter.accepted_ns = None
def propose(self, observations, policy, upcoming: Upcoming | None, *, current_way_id: int,
seconds: int, speed_mps: float | None, standstill: bool, can_offer: bool, now_ns: int):
seconds: int, speed_mps: float | None, standstill: bool, can_offer: bool,
confirmation_active: bool, now_ns: int):
self.boundary_id, self.started_ns = '', None
current = observations[sel.Source.MAP].candidate
encounter = self.encounter
if encounter is not None and not encounter.declined and encounter.accepted_ns is None:
if not confirmation_active:
encounter.started_ns = None
elif encounter.started_ns is None:
encounter.started_ns = now_ns
if current is None:
if encounter is not None and not encounter.crossed:
return {source: acc.Observation(acc.ObservationKind.UNKNOWN)
+6 -1
View File
@@ -7,6 +7,7 @@ from dataclasses import dataclass, replace
import openpilot.cereal.messaging as messaging
from openpilot.cereal.services import SERVICE_LIST
from openpilot.selfdrive.car.cruise import V_CRUISE_MIN, V_CRUISE_MAX
from openpilot.starpilot.aol.runtime import current_axis
from openpilot.starpilot.speed_limits import acceptance as acc
from openpilot.starpilot.speed_limits import action_arbitration as arb
from openpilot.starpilot.speed_limits import composition as comp
@@ -384,6 +385,9 @@ class Runtime:
selfdrive.enabled and car_fresh and selfdrive_fresh)
long_enabled = bool(car_fresh and controls_fresh and selfdrive_fresh and car_control_fresh and
selfdrive.enabled and sm['carControl'].enabled)
if 'aolAxisState' in sm.logMonoTime:
axis = current_axis(sm, now_ns=now_ns)
long_enabled = long_enabled and bool(axis is not None and axis.nativeAcknowledged and axis.desiredLongitudinal)
lateral = bool(car_control_fresh and sm['carControl'].latActive and selfdrive_fresh)
authority = _authority(CP, sm, host_long, lateral, long_enabled=long_enabled)
authority = replace(authority, accelerator_override=bool(CP.openpilotLongitudinalControl and long_enabled and car_state.gasPressed))
@@ -415,7 +419,8 @@ class Runtime:
current_way_id=self.map_source.way_id, seconds=self.settings.advance_seconds,
speed_mps=float(car_state.vEgo) if ego_valid else None,
standstill=bool(getattr(car_state, 'standstill', False)) or str(getattr(car_state, 'gearShifter', '')) == 'reverse',
can_offer=self.settings.enabled and authority.can_control and not host.force_decel, now_ns=now_ns)
can_offer=self.settings.enabled and authority.can_control and not host.force_decel,
confirmation_active=authority.can_control, now_ns=now_ns)
return replace(frame, adoption_observations=proposals, confirmation_started_ns=self.advance.started_ns)
def _action(self, request: Action | None, now_ns: int) -> tuple[acc.DriverAction | None, acc.AdoptRequest | None, int | None]:
@@ -42,12 +42,54 @@ class TestAcceptance(unittest.TestCase):
self.assertIsNone(result.history_write)
self.assertIsNone(result.control_target_mps)
def test_manual_confirmation_requires_longitudinal_engagement_for_every_source(self):
lateral_only = Authority(Mode.LATERAL_ONLY, LongitudinalOwner.NONE, True, False, False, False)
for source, authority in itertools.product(('dashboard', 'map', 'vision', 'online'), (DISENGAGED, lateral_only, PAUSED)):
with self.subTest(source=source, authority=authority):
candidate = replace(LOW, source=source)
inactive = step(saved(), valid(candidate), authority, Policy(), now_ns=0)
self.assertIsNone(inactive.state.pending)
self.assertFalse(inactive.state.timer_running)
self.assertEqual(inactive.display_candidate, candidate)
self.assertEqual(inactive.state.accepted, saved().accepted)
self.assertIsNone(inactive.history_write)
self.assertIsNone(inactive.control_target_mps)
engaged = step(inactive.state, valid(candidate), ACTIVE, Policy(), now_ns=20 * NS)
self.assertIsNotNone(engaged.state.pending)
self.assertEqual(engaged.state.pending.active_elapsed_ns, 0)
def test_disengagement_cancels_confirmation_without_declining_or_consuming_an_old_action(self):
pending = self.pending()
action = DriverAction('drive', 1, pending.state.pending.decision_id, ActionKind.ACCEPT)
inactive = step(pending.state, valid(LOW), DISENGAGED, Policy(), now_ns=5 * NS, action=action)
self.assertIsNone(inactive.state.pending)
self.assertFalse(inactive.state.timer_running)
self.assertFalse(inactive.state.rejected)
self.assertFalse(inactive.action_receipt.consumed)
self.assertEqual(inactive.state.accepted, pending.state.accepted)
resumed = step(inactive.state, valid(LOW), ACTIVE, Policy(), now_ns=20 * NS,
action=replace(action, sequence_id=2))
self.assertIsNotNone(resumed.state.pending)
self.assertNotEqual(resumed.state.pending.decision_id, pending.state.pending.decision_id)
self.assertEqual(resumed.state.pending.active_elapsed_ns, 0)
self.assertFalse(resumed.action_receipt.consumed)
self.assertEqual(resumed.state.accepted, pending.state.accepted)
def test_accelerator_override_keeps_an_engaged_confirmation(self):
pending = self.pending()
overriding = replace(ACTIVE, longitudinal_active=False, accelerator_override=True)
held = step(pending.state, valid(LOW), overriding, Policy(), now_ns=NS)
self.assertEqual(held.state.pending.decision_id, pending.state.pending.decision_id)
accepted = self.act(held.state, ActionKind.ACCEPT, authority=overriding, now_ns=2 * NS)
self.assertTrue(accepted.action_receipt.consumed)
self.assertEqual(accepted.control_target_mps, LOW.speed_mps)
def test_sources_share_confirmation_across_disengage_and_source_loss(self):
for source, confirmation in itertools.product(('dashboard', 'map', 'vision', 'online'), (False, True)):
policy = Policy(confirmation, confirmation)
candidate = replace(LOW, source=source)
first = step(new_session('drive'), valid(candidate), DISENGAGED, policy, now_ns=0)
self.assertEqual(first.state.pending is not None, confirmation)
self.assertIsNone(first.state.pending)
self.assertEqual(first.history_write is None, confirmation)
self.assertIsNone(first.control_target_mps)
ready = step(first.state, valid(candidate), ACTIVE, policy, now_ns=NS)
@@ -190,13 +232,18 @@ class TestAcceptance(unittest.TestCase):
self.assertIsNone(expired.history_write)
self.assertEqual(expired.control_target_mps, HIGH.speed_mps)
def test_confirmation_expires_after_ten_seconds_even_without_active_longitudinal(self):
def test_disengagement_cancels_confirmation_instead_of_timing_out(self):
start = self.pending()
pause = step(start.state, valid(LOW), PAUSED, Policy(), now_ns=5 * NS)
self.assertEqual(pause.state.pending.active_elapsed_ns, 5 * NS)
expired = step(pause.state, valid(LOW), PAUSED, Policy(), now_ns=CONFIRMATION_NS)
self.assertIsNone(pause.state.pending)
inactive = step(pause.state, valid(LOW), PAUSED, Policy(), now_ns=CONFIRMATION_NS)
self.assertFalse(inactive.state.rejected)
self.assertIsNone(inactive.control_target_mps)
resumed = self.pending(state=inactive.state, now_ns=20 * NS)
self.assertNotEqual(resumed.state.pending.decision_id, start.state.pending.decision_id)
self.assertEqual(resumed.state.pending.active_elapsed_ns, 0)
expired = step(resumed.state, valid(LOW), ACTIVE, Policy(), now_ns=30 * NS)
self.assertEqual(expired.basis, 'timeout_reject')
self.assertIsNone(expired.control_target_mps)
self.assertEqual(expired.state.accepted.candidate, HIGH)
def test_no_implicit_ttl_or_maximum_active_gap(self):
@@ -261,12 +308,13 @@ class TestAcceptance(unittest.TestCase):
self.assertEqual(ready.control_target_mps, LOW.speed_mps)
def test_zero_active_axes_does_not_imply_fully_disengaged(self):
result = self.pending(authority=PAUSED)
result = step(saved(), valid(LOW), PAUSED, Policy(), now_ns=0)
self.assertIsNone(result.state.pending)
self.assertEqual(result.state.accepted.candidate, HIGH)
self.assertIsNone(result.history_write)
self.assertIsNone(result.control_target_mps)
def test_modes_stock_owner_and_inactive_long_never_gain_control_but_driver_is_still_asked(self):
def test_modes_stock_owner_and_inactive_long_cannot_confirm_or_gain_control(self):
contexts = [
Authority(Mode.OFF, LongitudinalOwner.NONE, False, False, False, False),
Authority(Mode.LATERAL_ONLY, LongitudinalOwner.SYSTEM, True, False, False, False),
@@ -277,15 +325,16 @@ class TestAcceptance(unittest.TestCase):
]
for authority in contexts:
with self.subTest(authority=authority):
pending = self.pending(authority=authority)
pending = self.pending()
inactive = step(pending.state, valid(LOW), authority, Policy(), now_ns=NS)
acted = self.act(pending.state, ActionKind.ACCEPT, authority=authority)
lost = step(acted.state, ABSENT, authority, Policy(), now_ns=2 * NS)
for decision in (pending, acted, lost):
for decision in (inactive, acted, lost):
self.assertIsNone(decision.control_target_mps)
self.assertIsNone(pending.history_write)
# The driver's choice is recorded even though nothing here may act on it.
self.assertTrue(acted.action_receipt.consumed)
self.assertEqual(acted.state.accepted.candidate, LOW)
self.assertIsNone(decision.state.pending)
self.assertIsNone(decision.history_write)
self.assertFalse(acted.action_receipt.consumed)
self.assertEqual(acted.state.accepted.candidate, HIGH)
self.assertEqual(acted.state.last_action_sequence, 0)
def test_combined_and_long_only_have_equal_longitudinal_decisions(self):
@@ -327,7 +376,7 @@ class TestAcceptance(unittest.TestCase):
def test_repeated_and_out_of_order_actions_are_seen_but_not_consumed(self):
pending = self.pending()
first = self.act(pending.state, ActionKind.ACCEPT, sequence=10, authority=PAUSED)
first = self.act(pending.state, ActionKind.ACCEPT, sequence=10)
self.assertTrue(first.action_receipt.consumed)
seen = replace(pending.state, last_action_sequence=10)
for sequence in (10, 9):
@@ -662,11 +711,13 @@ class TestAcceptance(unittest.TestCase):
shown = self.pending()
request = replace(self.adopt_request(shown.state, sequence=5), presentation_id=999)
paused = step(shown.state, valid(LOW), PAUSED, Policy(), now_ns=NS, adopt=request)
repeated_action = DriverAction("drive", 5, paused.state.pending.decision_id, ActionKind.ACCEPT)
self.assertIsNone(paused.state.pending)
repeated_action = DriverAction("drive", 5, shown.state.pending.decision_id, ActionKind.ACCEPT)
repeated = step(paused.state, valid(LOW), ACTIVE, Policy(), now_ns=2 * NS, action=repeated_action)
self.assertEqual(repeated.action_receipt.status, "already_seen")
self.assertFalse(repeated.action_receipt.consumed)
fresh = step(repeated.state, valid(LOW), ACTIVE, Policy(), now_ns=3 * NS, action=replace(repeated_action, sequence_id=6))
fresh = step(repeated.state, valid(LOW), ACTIVE, Policy(), now_ns=3 * NS,
action=replace(repeated_action, sequence_id=6, decision_id=repeated.state.pending.decision_id))
self.assertTrue(fresh.action_receipt.consumed)
old_adopt = step(fresh.state, valid(LOW), ACTIVE, Policy(), now_ns=4 * NS, adopt=request)
self.assertFalse(old_adopt.adoption_receipt.consumed)
@@ -138,6 +138,7 @@ def test_confirmation_latches_on_slowdown_and_stop_then_times_out_once():
drive = Drive(confirm=True)
first = drive.tick().message.slcState
start = drive.runtime.advance.encounter.started_ns
assert start is not None
slower = drive.tick(speed=1., distance=59.).message.slcState
stopped = drive.tick(speed=0., stopped=True, distance=59.).message.slcState
assert first.decisionId == slower.decisionId == stopped.decisionId
@@ -152,6 +153,7 @@ def test_exact_deadline_action_wins_and_crossing_does_not_repeat(kind):
drive = Drive(confirm=True)
state = drive.tick().message.slcState
start = drive.runtime.advance.encounter.started_ns
assert start is not None
action = drive.action(state, kind)
output = drive.tick(at=start+acc.CONFIRMATION_NS, action=action)
assert output.message.slcState.actionStatus == kind
@@ -166,6 +168,7 @@ def test_recovery_renews_presentation_without_restarting_confirmation():
drive = Drive(confirm=True)
first = drive.tick().message.slcState
start = drive.runtime.advance.encounter.started_ns
assert start is not None
lost = drive.tick(alive=False)
assert not lost.message.slcState.hasPending and lost.command is None
recovered = drive.tick(at=start+9_000_000_000, producer=42).message.slcState
@@ -174,6 +177,30 @@ def test_recovery_renews_presentation_without_restarting_confirmation():
assert not expired.message.slcState.hasPending and expired.command is None
def test_disengagement_cancels_early_confirmation_and_reengagement_gets_a_fresh_deadline():
drive = Drive(confirm=True)
first = drive.tick().message.slcState
start = drive.now
drive.sm['carControl'].enabled = drive.sm['carControl'].longActive = False
drive.sm['selfdriveState'].enabled = False
drive.sm['controlsState'].longControlState = 'off'
inactive = drive.tick(at=start+2_000_000_000)
assert not inactive.message.slcState.hasPending and inactive.command is None
assert not drive.runtime.advance.encounter.declined
drive.sm['carControl'].enabled = drive.sm['carControl'].longActive = True
drive.sm['selfdriveState'].enabled = True
drive.sm['controlsState'].longControlState = 'pid'
resumed = drive.tick(at=start+20_000_000_000).message.slcState
assert resumed.hasPending and resumed.decisionId != first.decisionId
assert resumed.pendingSinceMonoTime == drive.now
restarted = drive.now
stale_action = drive.tick(action=drive.action(first, 'accept'))
assert stale_action.message.slcState.hasPending and stale_action.command is None
assert drive.tick(at=restarted+9_000_000_000).message.slcState.hasPending
assert not drive.tick(at=restarted+acc.CONFIRMATION_NS).message.slcState.hasPending
assert drive.runtime.advance.encounter.declined
def test_fresh_different_boundary_supersedes_without_accepting_stale_action():
drive = Drive(confirm=True)
first = drive.tick().message.slcState
@@ -0,0 +1,73 @@
"""Manual confirmations follow the longitudinal axis, including pedal override."""
import pytest
from openpilot.cereal import messaging
from openpilot.starpilot.speed_limits.runtime import Action, Runtime
from openpilot.starpilot.speed_limits.runtime_settings import parse
from openpilot.starpilot.ui.tests.test_slc_ui_runtime import START, replay_inputs
@pytest.mark.parametrize('mode', ['disengaged', 'aol', 'longitudinal', 'combined', 'accelerator'])
def test_runtime_confirms_only_with_engaged_longitudinal_control(mode):
sm, cp = replay_inputs()
active = mode in ('longitudinal', 'combined')
engaged = active or mode == 'accelerator'
sm['selfdriveState'].enabled = sm['carControl'].enabled = engaged
sm['carControl'].longActive = active
sm['carControl'].latActive = mode in ('aol', 'combined')
sm['controlsState'].longControlState = 'pid' if active else 'off'
sm['carState'].gasPressed = mode == 'accelerator'
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCConfirmation': True, 'SLCPriority1': 'Dashboard'}))
output = runtime.step(sm, cp, now_ns=START)
assert not output.result.errors
assert output.message.slcState.hasPending == engaged
assert output.message.slcState.observationKind == 'valid'
assert output.message.slcState.presentationId > 0
assert output.command is None
def test_runtime_cancels_old_action_and_restarts_confirmation_after_disengagement():
sm, cp = replay_inputs()
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCConfirmation': True, 'SLCPriority1': 'Dashboard'}))
first = runtime.step(sm, cp, now_ns=START).message.slcState
action = Action(runtime.session_id, 1, first.decisionId, first.presentationId, 'accept')
sm.advance(START+50_000_000)
sm['selfdriveState'].enabled = sm['carControl'].enabled = sm['carControl'].longActive = False
sm['carControl'].latActive = True
sm['controlsState'].longControlState = 'off'
inactive = runtime.step(sm, cp, now_ns=START+50_000_000, request=action)
assert not inactive.message.slcState.hasPending and not inactive.message.slcState.hasAccepted
assert inactive.command is None and not inactive.action_result.change
now = START+20_000_000_000
sm.advance(now)
sm['slcDashboardObservation'].observedMonoTime = now
sm['slcDashboardObservation'].validUntilMonoTime = now+1_000_000_000
sm['selfdriveState'].enabled = sm['carControl'].enabled = sm['carControl'].longActive = True
sm['controlsState'].longControlState = 'pid'
resumed = runtime.step(sm, cp, now_ns=now).message.slcState
assert resumed.hasPending and resumed.decisionId != first.decisionId
assert resumed.pendingSinceMonoTime == now and not resumed.hasAccepted
@pytest.mark.parametrize('desired,acknowledged,fresh', [(False, True, True), (True, False, True),
(True, True, False), (True, True, True)])
def test_aol_pedal_override_requires_current_longitudinal_engagement(desired, acknowledged, fresh):
sm, cp = replay_inputs()
sm['carControl'].longActive = False
sm['carControl'].latActive = True
sm['carState'].gasPressed = True
sm['controlsState'].longControlState = 'off'
axis = messaging.new_message('aolAxisState').aolAxisState
axis.qualified, axis.nativeAcknowledged, axis.desiredLongitudinal = True, acknowledged, desired
axis.sessionId = 'aol-drive'
axis.observedMonoTime = START
axis.validUntilMonoTime = START+30_000_000
sm.data['aolAxisState'] = axis
sm.seen = {'aolAxisState': True}
sm.valid['aolAxisState'] = sm.alive['aolAxisState'] = True
sm.logMonoTime['aolAxisState'] = START if fresh else START-100_000_000
runtime = Runtime(parse({'SpeedLimitController': True, 'SLCConfirmation': True, 'SLCPriority1': 'Dashboard'}))
output = runtime.step(sm, cp, now_ns=START)
assert not output.result.errors
assert output.message.slcState.hasPending == (desired and acknowledged and fresh)
@@ -36,7 +36,7 @@ class RuntimeSettingsTests(unittest.TestCase):
authority, policy, now_ns=1)
self.assertEqual(result.state.accepted, accepted)
self.assertIsNone(result.history_write)
self.assertIsNotNone(result.state.pending)
self.assertEqual(result.state.pending is not None, authority.can_control)
self.assertEqual(result.control_target_mps, 20. if accepted is not None and authority.can_control else None)
def test_fresh_vision_display_default_preserves_explicit_saved_choices(self):
+2 -1
View File
@@ -264,7 +264,8 @@ class OnroadRequest:
def manual_slc_confirmation(profile: Profile, state: OnroadState) -> bool:
obs = state.speed_limit
widget = 'speed_limit' if profile == Profile.COMPACT else 'cruise_limits'
return bool(state.alert.size == AlertSize.NONE and state.toyota_cruise_status not in TOYOTA_NOTICE_STATUSES and
return bool((state.longitudinal_active or state.longitudinal_overridden) and
state.alert.size == AlertSize.NONE and state.toyota_cruise_status not in TOYOTA_NOTICE_STATUSES and
(profile != Profile.COMPACT or not state.reverse_driver_camera and state.appearance.camera_view != CameraViewChoice.DRIVER) and
obs.kind == ObservationKind.VALID and obs.action_enabled and not obs.automatic_adoption_pending and
obs.pending_speed_limit_mps is not None and obs.pending_speed_limit_mps > 0 and
@@ -39,11 +39,31 @@ def test_closed_and_open_card_share_anchor_and_physically_scaled_targets(profile
assert after[0] > 146
@pytest.mark.parametrize('profile', list(Profile))
@pytest.mark.parametrize('lateral', [False, True])
def test_disengaged_and_aol_views_cancel_confirmation_but_keep_the_sign(profile, lateral):
current, emitted = state(), []
touch = OnroadInput(emitted.append, profile)
tap_sign(touch, current)
assert touch.present(current).slc_prompt_open
inactive = replace(current, longitudinal_active=False, lateral_active=lateral)
assert not manual_slc_confirmation(profile, inactive)
assert not touch.present(inactive).slc_prompt_open
assert not slc_controls(profile, inactive)
assert speed_limit_bounds(profile, inactive) is not None
assert not emitted
@pytest.mark.parametrize('profile', list(Profile))
def test_accelerator_override_keeps_confirmation_visible(profile):
current = replace(state(), longitudinal_active=False, longitudinal_overridden=True)
assert manual_slc_confirmation(profile, current)
@pytest.mark.parametrize('profile', list(Profile))
@pytest.mark.parametrize('action', [SlcActionKind.ACCEPT, SlcActionKind.REJECT])
def test_touch_choice_remains_available_without_wheel_confirmation(profile, action):
current = replace(state(), longitudinal_active=False, slc_system_long_available=False,
slc_wheel_confirmation_available=False)
current = replace(state(), slc_wheel_confirmation_available=False)
emitted = []
touch = OnroadInput(emitted.append, profile)
tap_sign(touch, current)