Files
StarPilot/openpilot/starpilot/flm/tests/test_source_uncertainty.py
T
Dom 487dfc4bf4 FLM: preserve fresh evidence across paired queue drains
Merge bounded control/output FIFO heads using original publisher headers. Preserve original age, native, source, overflow and malformed-input checks; retain producer order after joins.

Validated with actual pinned cloud dependencies and fresh-head regression tests.
2026-10-27 17:01:24 -05:00

198 lines
11 KiB
Python

"""Actual source reader/parent loop; synthetic transport, no fitted admission."""
from openpilot.starpilot.flm.tests._drain_fixture import separate_drain_fixture
from dataclasses import replace
from types import SimpleNamespace
from typing import cast
from unittest import mock
import pytest
from openpilot.starpilot.flm import runtime_adapter_registry as registry, steerlearnerd
from openpilot.starpilot.flm.fullrate_collector import FullrateCollector, RawBlock, RawPoint
from openpilot.starpilot.flm.physical_observation import PendingPhysical
from openpilot.starpilot.flm.tests.test_fixed_manual_baseline import fixed_controls, BASE
from openpilot.starpilot.flm.tests.test_fullrate_pipeline import SyntheticAdapter, snapshots, serialized_control, serialized_output
from openpilot.starpilot.lateral.learning_preferences import KEY, replace_preference
def test_actual_saved_read_failure_unknown_then_exact_configuration_recovery():
with fixed_controls() as (controls, _):
args = {'now_ns': BASE, 'calibration': (0., 0., 0.), 'vehicle_session': 'source-test', 'calibration_generation': 'a' * 32}
first = registry.read_source(controls.CP, controls.params, **args)
assert first.configuration_known and first.enabled and first.source_epoch
original = registry.read_saved
with mock.patch.object(registry, 'read_saved', side_effect=lambda params, key, bound:
(b'', False) if key == KEY else original(params, key, bound)):
missing = registry.read_source(controls.CP, controls.params, **args)
assert not missing.configuration_known and not missing.enabled and not missing.source_epoch
recovered = registry.read_source(controls.CP, controls.params, **args)
assert steerlearnerd._source_config_binding(recovered) == steerlearnerd._source_config_binding(first)
@pytest.mark.parametrize('raw', (b'', b'{"version":1,"version":1,"vehicles":{}}', b'x' * 4097))
def test_invalid_actual_preference_is_unknown_not_explicit_off(raw):
with fixed_controls() as (controls, _):
controls.params.put(KEY, raw, block=True)
value = registry.read_source(controls.CP, controls.params, now_ns=BASE, calibration=(0., 0., 0.), vehicle_session='source-test')
assert not value.configuration_known and not value.enabled and not value.source_epoch
def test_actual_explicit_off_is_known_without_calibration():
with fixed_controls() as (controls, _):
controls.params.put(KEY, replace_preference(controls.params.get(KEY), controls.CP, False), block=True)
value = registry.read_source(controls.CP, controls.params, now_ns=BASE, calibration=(), vehicle_session='source-test')
assert value.configuration_known and not value.enabled and value.reset == '0'
assert steerlearnerd._source_config_known(value, BASE)
def test_actual_preference_parsed_from_same_once_captured_bytes_as_epoch():
with fixed_controls() as (controls, _):
original, calls = registry.read_saved, []
def read(params, key, bound):
raw, readable = original(params, key, bound)
calls.append(key)
if key == KEY:
params.put(KEY, replace_preference(raw, controls.CP, False), block=True)
return raw, readable
with mock.patch.object(registry, 'read_saved', side_effect=read), \
mock.patch('openpilot.starpilot.lateral.learning_preferences.read_saved',
side_effect=AssertionError('read_source must not reread preference bytes')):
first = registry.read_source(controls.CP, controls.params, now_ns=BASE, calibration=(0., 0., 0.), vehicle_session='source-test')
second = registry.read_source(controls.CP, controls.params, now_ns=BASE + 1, calibration=(0., 0., 0.), vehicle_session='source-test')
assert calls.count(KEY) == 1
assert first.configuration_known and first.enabled
assert second.configuration_known and not second.enabled
def exercise(recovery=None, *, publication_uncertain=False, publication_only_recovery=False):
now, phase = [BASE], [-1]
key = ('feedforward', 'low', 'steady')
source = registry.SourceState(True, False, '0', '2' * 64, BASE, BASE + 500_000_000, 'b' * 64,
(0., 0., 0.), calibration_generation='a' * 32)
unknown = registry.SourceState(False, False, '', '', BASE, BASE + 500_000_000, 'b' * 64, configuration_known=False)
objects, published, saves, history = [], [], [], []
class Adapter(SyntheticAdapter):
raw_storage = False
@property
def event_collection(self):
return publication_only_recovery and phase[0] == 2
def __init__(self):
super().__init__()
self.current_physical_valid = False
self.last_physical_ns = 0
self.fit_profile = None
self.replay = SimpleNamespace(failed=False, records=('original-frame',), checkpoints=('original-checkpoint',))
def select_objective(self, value):
if publication_only_recovery and phase[0] == 2:
from openpilot.starpilot.flm.event_collection import EventCollectionAdapter
# Execute the actual event selector guard if the owner calls it without
# a newly selected control objective; this must never be reached.
EventCollectionAdapter.select_objective(cast(EventCollectionAdapter, SimpleNamespace(_event_initialized=True)), value)
raise AssertionError("publication selected an objective before new control")
def prepare_control(self, value):
return value
sm = mock.Mock()
sm.updated = {'deviceMotion': False}
sm.logMonoTime = {'deviceMotion': 0}
def update(timeout=None):
if timeout == 0:
return
phase[0] += 1
if phase[0] >= 3:
raise StopIteration
now[0] = BASE + phase[0] * 300_000_000
sm.update.side_effect = update
def read(stamp):
item = unknown if phase[0] == 1 or (publication_only_recovery and phase[0] == 2) else replace(source, **(recovery or {})) if phase[0] == 2 else source
return replace(item, observed_ns=stamp, expires_ns=stamp + 500_000_000)
def publication_read(stamp):
return replace(unknown if phase[0] == 1 else source, observed_ns=stamp, expires_ns=stamp + 500_000_000)
def factory(adapter):
collector = FullrateCollector(adapter)
objects.append(collector)
return collector
c_sock, a_sock = object(), object()
def drain(socket, callback, receive, sink, stamp):
if publication_only_recovery and phase[0] == 2:
return
control, output, _ = snapshots(now[0] - 1_000_000)
control = replace(control, source_receipt_ns=now[0], control_ns=now[0], state_ns=now[0] - 1_000_000)
output = replace(output, control_ns=now[0], state_ns=now[0], apply_ns=now[0])
callback(serialized_control(control) if socket is c_sock else serialized_output(output), sink, stamp)
if socket is a_sock and phase[0] == 0 and objects:
c = objects[0]
rows = tuple(RawPoint(control.control_ns, output.apply_ns, output.apply_ns + i,
control, output, {'synthetic_transport_only': i}, ('original-physical',)) for i in range(40))
c.blocks.append(RawBlock(control.identity, 'original-storage', 'original', control.stage_revision, control.response_revision, rows))
def send(service, event):
row = event.autoSteeringLearning.learning
published.append((phase[0], str(row.status), str(row.reason), row.runtimeProfile.version))
if objects:
c = objects[0]
history.append((phase[0], len(c.blocks), len(c.ready), c.active_since, c.last_qualified_response,
c.adapter.current_physical_valid, c.adapter.replay.failed))
fitter = mock.Mock(child=object())
fitter.poll.return_value = None
persistence, publisher = mock.Mock(), mock.Mock()
persistence.save_current.side_effect = lambda *args, **kwargs: saves.append(phase[0])
publisher.send.side_effect = send
with mock.patch.object(steerlearnerd, 'drain_pair', separate_drain_fixture(drain)), \
mock.patch.object(steerlearnerd, 'FullrateCollector', side_effect=factory), \
mock.patch('openpilot.starpilot.flm.native_health.current_native_health', return_value=True):
with pytest.raises(StopIteration):
steerlearnerd.run(SimpleNamespace(), sm, c_sock, a_sock, publisher,
lambda *args: Adapter(), lambda *args: PendingPhysical(now[0], now[0]),
(lambda stamp: replace(source, observed_ns=stamp, expires_ns=stamp + 500_000_000)) if publication_uncertain else read,
lambda _: (key,), lambda _: key, mock.Mock(), persistence, fitter, mock.Mock(), clock=lambda: now[0],
publication_source_reader=publication_read if publication_uncertain or publication_only_recovery else None)
return objects, published, saves, history, fitter
def test_actual_owner_uncertainty_keeps_storage_not_current_authority():
collectors, published, saves, history, fitter = exercise()
assert len(collectors) == 1 and len(collectors[0].blocks) == 1
uncertain = next(row for row in history if row[0] == 1)
assert uncertain[1:] == (1, 0, 0, 0, False, True)
assert 1 not in saves
assert any(row[0] == 1 and row[1] == 'unavailable' and row[3] == 0 for row in published)
assert any(call.args == ('source_configuration_uncertain',) for call in fitter.cancel.call_args_list)
assert collectors[0].fit_result is None and collectors[0].last_validation is None
@pytest.mark.parametrize('changes', ({'source_epoch': 'd' * 64}, {'reset': '1'},
{'calibration_generation': 'c' * 32}, {'cp_sha256': 'd' * 64},
{'manual_baseline': (3., 0., .1)}, {'enabled': False}, {'manual': True}))
def test_actual_owner_changed_or_explicit_disabled_recovery_purges_storage(changes):
collectors, _, _, _, _ = exercise(changes)
assert not collectors[0].blocks
def test_publication_uncertainty_cannot_save_or_publish_admission():
_, published, saves, _, _ = exercise(publication_uncertain=True)
assert 1 not in saves
assert any(row[0] == 1 and row[1] == 'unavailable' and row[3] == 0 for row in published)
def test_actual_legacy_explicit_off_is_known_without_calibration():
with fixed_controls() as (controls, _):
controls.params.remove(KEY)
controls.params.put_bool('ForceAutoTuneOff', True, block=True)
source = registry.read_source(controls.CP, controls.params, now_ns=BASE, calibration=(), vehicle_session='source-test')
assert source.configuration_known and not source.enabled
@pytest.mark.parametrize('changes', ({'observed_ns': BASE + 1}, {'expires_ns': BASE - 1},
{'expires_ns': BASE + 500_000_001}, {'reset': ''}, {'reset': 'invalid'},
{'cp_sha256': 'wrong'}, {'enabled': 0}, {'configuration_known': False}))
def test_malformed_or_stale_receipt_never_qualifies_for_explicit_off_purge(changes):
source = registry.SourceState(False, False, '0', 'a' * 64, BASE, BASE + 500_000_000, 'b' * 64)
assert not steerlearnerd._source_config_known(replace(source, **changes), BASE)
def test_actual_owner_publication_recovery_without_new_control_stays_unavailable():
collectors, published, saves, history, _ = exercise(publication_only_recovery=True)
assert len(collectors) == 1 and len(collectors[0].blocks) == 1
assert 2 not in saves
assert any(row[0] == 2 and row[1] == 'unavailable' and row[3] == 0 for row in published)
assert next(row for row in history if row[0] == 2)[2:] == (0, 0, 0, False, True)