diff --git a/starpilot/controls/tests/test_fleet_aol.py b/starpilot/controls/tests/test_fleet_aol.py new file mode 100644 index 0000000000..59b8bebfdc --- /dev/null +++ b/starpilot/controls/tests/test_fleet_aol.py @@ -0,0 +1,206 @@ +"""Synthetic-input coverage of the real StarPilotCard latch across all platforms. + +CP/FPCP come from current interfaces with empty CAN fingerprints/firmware and +isolated default settings. These tests do not model CAN safety, selfdrived, +physical buttons, firmware-dependent variants, or vehicle actuation. Skipped +active sequences are coverage gaps, not evidence of fleet compatibility. +""" + +import importlib +from types import SimpleNamespace + +import pytest + +from opendbc.car import gen_empty_fingerprint +from opendbc.car.car_helpers import interfaces +from opendbc.car.values import PLATFORMS +from openpilot.common import params as params_module +from openpilot.starpilot.controls.tests.test_starpilot_card import FakeParams, make_car_state, make_sm, make_toggles, spc + + +class FleetParams(FakeParams): + def get_bool(self, key, default=False): + return bool(self._store.get(key, default)) + + def get_float(self, key, default=0.0): + return float(self._store.get(key, default)) + + +@pytest.fixture +def isolated_settings(monkeypatch, tmp_path): + # Interfaces have both imported Params aliases and dynamic imports. The Pre-AP + # configuration also retains a Params instance, so isolate all three forms. + monkeypatch.setattr(params_module, "Params", FleetParams) + for name in {"opendbc.car.interfaces", *(interface.__module__ for interface in interfaces.values())}: + module = importlib.import_module(name) + if hasattr(module, "Params"): + monkeypatch.setattr(module, "Params", FleetParams) + monkeypatch.setattr(importlib.import_module("opendbc.car.tesla.preap.nap_conf"), "_params", FleetParams()) + monkeypatch.setattr(spc, "Params", FleetParams) + monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path) + + +def fleet_config(platform, mode, enabled=True): + toggles = make_toggles(always_on_lateral=enabled, always_on_lateral_main=mode == "main", + always_on_lateral_lkas=mode == "engage", lkas_allowed_for_aol=mode == "engage", + always_on_lateral_pause_speed=5.0) + fingerprint = gen_empty_fingerprint() + interface = interfaces[platform] + cp = interface.get_params(platform, fingerprint, [], False, False, docs=False, starpilot_toggles=toggles) + fp = interface.get_starpilot_params(platform, fingerprint, [], cp, toggles) + assert cp.carFingerprint == platform + # Card startup installs this feature bit after the interface returns FPCP. + # This is an explicit feature-on fixture, not a call to the full card daemon. + if enabled and spc.always_on_lateral_available(cp): + cp.alternativeExperience |= spc.ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL + fp.alternativeExperience |= spc.ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL + else: + cp.alternativeExperience &= ~spc.ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL + fp.alternativeExperience &= ~spc.ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL + return cp, fp, toggles + + +class Sequence: + def __init__(self, cp, fp, toggles): + self.card = spc.StarPilotCard(cp, fp) + self.toggles = toggles + self.sm = make_sm() + self.cs = make_car_state() + self.output = SimpleNamespace(distancePressed=False) + self.preap_authorized = None + + def step(self, expected, *, frames=1): + for _ in range(frames): + kwargs = {} if self.preap_authorized is None else {"preap_authorized": self.preap_authorized} + output = self.card.update(self.cs, self.output, self.sm, self.toggles, **kwargs) + assert output.alwaysOnLateralEnabled is expected + return output + + def engage(self): + self.cs.cruiseState.available = self.cs.cruiseState.enabled = True + self.sm["selfdriveState"].active = True + self.sm["carControl"].longActive = True + self.cs.brakePressed = False + self.cs.vEgo = 15.0 + self.cs.standstill = False + self.cs.gearShifter = spc.GearShifter.drive + + def disengage(self): + self.cs.cruiseState.enabled = False + self.sm["selfdriveState"].active = False + self.sm["carControl"].longActive = False + + +@pytest.mark.parametrize("platform", sorted(PLATFORMS)) +def test_fleet_aol_disabled_configuration(platform, isolated_settings): + cp, fp, toggles = fleet_config(platform, "main", enabled=False) + sequence = Sequence(cp, fp, toggles) + sequence.step(False) + sequence.engage() + sequence.step(False, frames=20) + sequence.disengage() + sequence.cs.brakePressed = True + sequence.step(False, frames=20) + + +@pytest.mark.parametrize("platform", sorted(PLATFORMS)) +@pytest.mark.parametrize("mode", ("main", "engage")) +def test_fleet_aol_state_sequence(platform, mode, isolated_settings, record_property): + cp, fp, toggles = fleet_config(platform, mode) + record_property("platform", platform) + record_property("brand", cp.brand) + record_property("mode", mode) + record_property("fixture", "empty fingerprint/firmware; synthetic normalized state; no CAN/safety or selfdrived") + if not spc.always_on_lateral_available(cp): + # Even a stale feature bit must not grant a platform denied by current policy. + fp.alternativeExperience |= spc.ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL + denied = Sequence(cp, fp, toggles) + denied.engage() + denied.step(False, frames=20) + denied.disengage() + denied.step(False, frames=20) + pytest.skip(f"AOL active coverage gap: current platform policy excludes {platform}; denial checked") + if cp.dashcamOnly or cp.notCar: + pytest.skip(f"AOL active coverage gap: empty-fingerprint configuration has dashcamOnly={cp.dashcamOnly}, notCar={cp.notCar}") + if platform == "TESLA_MODEL_S_PREAP": + denied = Sequence(cp, fp, toggles) + denied.engage() + denied.step(False, frames=20) + denied.disengage() + denied.step(False, frames=20) + pytest.skip("AOL active coverage gap: Pre-AP requires external authorization; synthetic cruise state cannot supply it; denial checked") + + sequence = Sequence(cp, fp, toggles) + sequence.step(False, frames=20) + sequence.engage() + sequence.step(True, frames=120) + + # Braking out of longitudinal at speed leaves a stable AOL-only state. + sequence.disengage() + sequence.cs.brakePressed = True + sequence.step(True, frames=120) + assert not sequence.sm["selfdriveState"].active + assert not sequence.sm["carControl"].longActive + + # Both native and StarPilot immediate-disable sources suppress lateral. + for source in ("selfdriveState", "starpilotSelfdriveState"): + sequence.sm[source].alertType = f"controlsMismatch/{spc.ET.IMMEDIATE_DISABLE}" + sequence.step(False, frames=20) + sequence.sm[source].alertType = "" + sequence.step(True, frames=20) + sequence.sm["selfdriveState"].alertType = f"speedTooLow/{spc.ET.IMMEDIATE_DISABLE}" + sequence.step(True, frames=20) # Explicit existing longitudinal-only exception. + sequence.sm["selfdriveState"].alertType = "" + + sequence.cs.vEgo = 2.0 + sequence.step(False, frames=20) + sequence.cs.brakePressed = False + sequence.step(True, frames=20) + sequence.cs.vEgo = 0.0 + sequence.cs.brakePressed = sequence.cs.standstill = True + sequence.step(True, frames=20) # Existing standstill exception to brake pause. + + sequence.sm["liveCalibration"].calPerc = 0 + sequence.step(False, frames=20) + sequence.sm["liveCalibration"].calPerc = 100 + sequence.step(True) + sequence.sm["starpilotPlan"].lateralCheck = False + sequence.step(False, frames=20) + sequence.sm["starpilotPlan"].lateralCheck = True + sequence.step(True) + + for gear in (spc.GearShifter.park, spc.GearShifter.reverse): + sequence.cs.gearShifter = gear + sequence.step(False, frames=20) + sequence.engage() + sequence.step(True, frames=20) + sequence.disengage() + + # Explicit mode-off must remain off rather than repeatedly re-latching. + if mode == "main": + sequence.cs.cruiseState.available = False + sequence.step(False) + else: + sequence.cs.buttonEvents = [SimpleNamespace(type=spc.ButtonType.lkas, pressed=True)] + sequence.step(False) + sequence.cs.buttonEvents = [] + sequence.step(False, frames=120) + + +def test_preap_aol_explicit_authorization_boundary(isolated_settings): + cp, fp, toggles = fleet_config("TESLA_MODEL_S_PREAP", "main") + if not spc.always_on_lateral_available(cp): + pytest.skip("Pre-AP AOL unavailable by current policy") + sequence = Sequence(cp, fp, toggles) + sequence.engage() + sequence.step(False, frames=20) + sequence.preap_authorized = True + sequence.step(True, frames=20) + sequence.disengage() + sequence.step(True, frames=20) + sequence.sm["selfdriveState"].alertType = f"controlsMismatch/{spc.ET.IMMEDIATE_DISABLE}" + sequence.step(False, frames=20) + sequence.sm["selfdriveState"].alertType = "" + sequence.step(True, frames=20) + sequence.preap_authorized = False + sequence.step(False, frames=120)