mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-08 17:13:45 +08:00
1565 lines
60 KiB
Python
1565 lines
60 KiB
Python
import importlib.util
|
|
import json
|
|
import math
|
|
import sys
|
|
import time
|
|
|
|
from pathlib import Path
|
|
from types import ModuleType, SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
|
|
MODULE_PATH = Path(__file__).resolve().parents[1] / "flm_workspace.py"
|
|
|
|
|
|
def _simple_module(name, **attrs):
|
|
module = ModuleType(name)
|
|
for attr, value in attrs.items():
|
|
setattr(module, attr, value)
|
|
return module
|
|
|
|
|
|
def _install_flm_import_stubs(tmp_path):
|
|
class FakeParams:
|
|
_store = {}
|
|
_memory_store = {}
|
|
|
|
def __init__(self, return_defaults=False, memory=False):
|
|
self.return_defaults = return_defaults
|
|
self.memory = memory
|
|
|
|
@property
|
|
def _values(self):
|
|
return type(self)._memory_store if self.memory else type(self)._store
|
|
|
|
def get(self, key, block=False, return_default=False, encoding=None, default=None):
|
|
del block, return_default
|
|
value = self._values.get(key, default)
|
|
if encoding and isinstance(value, bytes):
|
|
return value.decode(encoding, errors="replace")
|
|
return value
|
|
|
|
def get_bool(self, key, default=False):
|
|
value = self._values.get(key, default)
|
|
if isinstance(value, bool):
|
|
return value
|
|
return str(value).strip().lower() in ("1", "true", "yes", "on")
|
|
|
|
def get_float(self, key, block=False, return_default=False, default=0.0):
|
|
del block, return_default
|
|
value = self._values.get(key, default)
|
|
try:
|
|
return float(value)
|
|
except Exception:
|
|
return default
|
|
|
|
def put(self, key, value):
|
|
self._values[key] = value
|
|
|
|
def put_bool(self, key, value):
|
|
self._values[key] = bool(value)
|
|
|
|
def put_float(self, key, value):
|
|
self._values[key] = float(value)
|
|
|
|
def remove(self, key):
|
|
self._values.pop(key, None)
|
|
|
|
FakeParams._store = {}
|
|
FakeParams._memory_store = {}
|
|
|
|
class FakeHyundaiFlags:
|
|
CANFD = 1
|
|
|
|
class FakeSteerControlType:
|
|
torque = 0
|
|
angle = 1
|
|
|
|
fake_car_params = SimpleNamespace(SteerControlType=FakeSteerControlType)
|
|
cereal_car = _simple_module("cereal.car", CarParams=fake_car_params)
|
|
cereal = _simple_module("cereal", car=cereal_car)
|
|
sys.modules["cereal"] = cereal
|
|
sys.modules["cereal.car"] = cereal_car
|
|
|
|
sys.modules["opendbc.car.hyundai.values"] = _simple_module("opendbc.car.hyundai.values", HyundaiFlags=FakeHyundaiFlags)
|
|
sys.modules["openpilot.common.params"] = _simple_module("openpilot.common.params", Params=FakeParams)
|
|
sys.modules["openpilot.selfdrive.controls.lib.latcontrol_torque"] = _simple_module(
|
|
"openpilot.selfdrive.controls.lib.latcontrol_torque",
|
|
KP=1.0,
|
|
)
|
|
|
|
def normalize_flm_overrides(payload):
|
|
if isinstance(payload, str):
|
|
payload = json.loads(payload)
|
|
payload = payload or {}
|
|
normalized = {
|
|
"schemaVersion": 1,
|
|
"baseFrictionThresholds": {},
|
|
"vehicleKnobs": {},
|
|
}
|
|
for family, family_payload in payload.get("baseFrictionThresholds", {}).items():
|
|
values = family_payload.get("values", [])
|
|
if len(values) == 5:
|
|
normalized["baseFrictionThresholds"][family] = {
|
|
"speedKnots": [0.0, 5.0, 10.0, 15.0, 25.0],
|
|
"values": [float(value) for value in values],
|
|
}
|
|
for key, value in payload.get("vehicleKnobs", {}).items():
|
|
normalized["vehicleKnobs"][key] = float(value)
|
|
if not normalized["baseFrictionThresholds"] and not normalized["vehicleKnobs"]:
|
|
return {}
|
|
return normalized
|
|
|
|
sys.modules["openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes"] = _simple_module(
|
|
"openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes",
|
|
FLM_FRICTION_SPEED_KNOTS=[0.0, 5.0, 10.0, 15.0, 25.0],
|
|
get_flm_capabilities=lambda *args, **kwargs: {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"},
|
|
get_flm_rich_profile_key=lambda *args, **kwargs: "hyundai_ioniq_6",
|
|
get_flm_supported_vehicle_knobs=lambda: {
|
|
"hyundai_ioniq_6.ff_gain_left": {"min": 0.0, "max": 0.6, "precision": 0.001, "defaultValue": 0.1, "profile": "hyundai_ioniq_6"},
|
|
"hyundai_ioniq_6.ff_gain_right": {"min": 0.0, "max": 0.6, "precision": 0.001, "defaultValue": 0.12, "profile": "hyundai_ioniq_6"},
|
|
"hyundai_ioniq_6.turn_in_boost_left": {"min": 0.4, "max": 2.8, "precision": 0.001, "defaultValue": 1.64, "profile": "hyundai_ioniq_6"},
|
|
"hyundai_ioniq_6.unwind_taper_left": {"min": 0.0, "max": 1.2, "precision": 0.001, "defaultValue": 0.4, "profile": "hyundai_ioniq_6"},
|
|
"hyundai_ioniq_6.low_speed_angle_assist_max_torque": {"min": 0.0, "max": 0.8, "precision": 0.001, "defaultValue": 0.46, "profile": "hyundai_ioniq_6"},
|
|
"hyundai_ioniq_6.crawl_turn_in_ff_boost_left": {"min": 0.0, "max": 0.5, "precision": 0.001, "defaultValue": 0.18, "profile": "hyundai_ioniq_6"},
|
|
"hyundai_ioniq_6.curvy_turn_in_trim_left": {"min": 0.0, "max": 0.2, "precision": 0.001, "defaultValue": 0.06, "profile": "hyundai_ioniq_6"},
|
|
"hyundai_ioniq_6.curvy_unwind_extra_reduction_left": {"min": 0.0, "max": 0.45, "precision": 0.001, "defaultValue": 0.18, "profile": "hyundai_ioniq_6"},
|
|
"hyundai_ioniq_6.center_deadband_low_deg": {"min": 0.0, "max": 0.3, "precision": 0.005, "defaultValue": 0.0, "profile": "hyundai_ioniq_6"},
|
|
"hyundai_ioniq_6.center_deadband_mid_deg": {"min": 0.0, "max": 0.3, "precision": 0.005, "defaultValue": 0.0, "profile": "hyundai_ioniq_6"},
|
|
"hyundai_ioniq_6.center_deadband_fast_deg": {"min": 0.0, "max": 0.3, "precision": 0.005, "defaultValue": 0.0, "profile": "hyundai_ioniq_6"},
|
|
"hyundai_ioniq_6.center_deadband_highway_deg": {"min": 0.0, "max": 0.3, "precision": 0.005, "defaultValue": 0.0, "profile": "hyundai_ioniq_6"},
|
|
"torque_universal.ff_gain_left": {"min": -0.4, "max": 0.6, "precision": 0.001, "defaultValue": 0.0, "profile": "torque_universal"},
|
|
"torque_universal.ff_gain_right": {"min": -0.4, "max": 0.6, "precision": 0.001, "defaultValue": 0.0, "profile": "torque_universal"},
|
|
"torque_universal.center_deadband_low_deg": {"min": 0.0, "max": 0.3, "precision": 0.005, "defaultValue": 0.0, "profile": "torque_universal"},
|
|
"torque_universal.center_deadband_mid_deg": {"min": 0.0, "max": 0.3, "precision": 0.005, "defaultValue": 0.0, "profile": "torque_universal"},
|
|
"torque_universal.center_deadband_fast_deg": {"min": 0.0, "max": 0.3, "precision": 0.005, "defaultValue": 0.0, "profile": "torque_universal"},
|
|
"torque_universal.center_deadband_highway_deg": {"min": 0.0, "max": 0.3, "precision": 0.005, "defaultValue": 0.0, "profile": "torque_universal"},
|
|
},
|
|
get_gm_base_friction_threshold=lambda v_ego: 0.20 + (0.001 * float(v_ego)),
|
|
get_hkg_canfd_base_friction_threshold=lambda v_ego: 0.39 + (0.001 * float(v_ego)),
|
|
get_standard_friction_threshold=lambda v_ego: 0.30 + (0.001 * float(v_ego)),
|
|
normalize_flm_overrides=normalize_flm_overrides,
|
|
)
|
|
sys.modules["openpilot.system.hardware"] = _simple_module("openpilot.system.hardware", PC=True)
|
|
sys.modules["openpilot.system.hardware.hw"] = _simple_module(
|
|
"openpilot.system.hardware.hw",
|
|
Paths=SimpleNamespace(comma_home=lambda: str(tmp_path), log_root=lambda **kwargs: str(tmp_path / "logs")),
|
|
)
|
|
sys.modules["openpilot.tools.lib.logreader"] = _simple_module("openpilot.tools.lib.logreader", LogReader=lambda *args, **kwargs: [])
|
|
sys.modules["openpilot.starpilot.system.the_galaxy.utilities"] = _simple_module(
|
|
"openpilot.starpilot.system.the_galaxy.utilities",
|
|
get_segments_in_route=lambda route, footage_path: [],
|
|
)
|
|
|
|
return FakeParams
|
|
|
|
|
|
def _load_flm_workspace_module(tmp_path):
|
|
fake_params_cls = _install_flm_import_stubs(tmp_path)
|
|
module_name = f"test_flm_workspace_{hash(tmp_path)}"
|
|
spec = importlib.util.spec_from_file_location(module_name, MODULE_PATH)
|
|
module = importlib.util.module_from_spec(spec)
|
|
assert spec.loader is not None
|
|
sys.modules[module_name] = module
|
|
spec.loader.exec_module(module)
|
|
return module, fake_params_cls
|
|
|
|
|
|
def _sample(module, **kwargs):
|
|
base = dict(
|
|
route="route",
|
|
segment=0,
|
|
t=0.0,
|
|
v_ego=28.0,
|
|
lat_active=True,
|
|
steering_pressed=False,
|
|
saturated=False,
|
|
actual_la=0.0,
|
|
desired_la=0.0,
|
|
desired_jerk=0.0,
|
|
error=0.0,
|
|
error_rate=0.0,
|
|
p=0.0,
|
|
i=0.0,
|
|
d=0.0,
|
|
f=0.0,
|
|
output=0.0,
|
|
steering_angle_deg=0.0,
|
|
steering_torque=0.0,
|
|
cmd_torque=0.0,
|
|
out_torque=0.0,
|
|
roll_deg=0.0,
|
|
)
|
|
base.update(kwargs)
|
|
return module.FLMSample(**base)
|
|
|
|
|
|
def test_effective_control_path_prefers_logged_controller_state(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
pid_cp = SimpleNamespace(
|
|
steerControlType=module.car.CarParams.SteerControlType.torque,
|
|
lateralTuning=SimpleNamespace(which=lambda: "pid"),
|
|
)
|
|
|
|
assert module._effective_control_path(pid_cp, {"torqueState": 1200}) == ("torque", "controlsState")
|
|
assert module._effective_control_path(pid_cp, {"pidState": 1200}) == ("pid", "controlsState")
|
|
assert module._effective_control_path(pid_cp, {}) == ("pid", "carParams")
|
|
|
|
|
|
def test_effective_control_path_keeps_true_angle_and_mixed_routes_diagnostic(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
angle_cp = SimpleNamespace(
|
|
steerControlType=module.car.CarParams.SteerControlType.angle,
|
|
lateralTuning=SimpleNamespace(which=lambda: "torque"),
|
|
)
|
|
|
|
assert module._effective_control_path(angle_cp, {}) == ("angle", "carParams")
|
|
assert module._effective_control_path(angle_cp, {"angleState": 900}) == ("angle", "controlsState")
|
|
assert module._effective_control_path(angle_cp, {"angleState": 900, "torqueState": 900}) == ("mixed", "controlsState")
|
|
|
|
|
|
def test_segment_ranges_limit_resolved_route_sources(tmp_path, monkeypatch):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
route = "00000001--abcdef1234"
|
|
segment_names = [f"{route}--{segment}" for segment in range(12)]
|
|
for segment_name in segment_names:
|
|
segment_path = tmp_path / segment_name
|
|
segment_path.mkdir()
|
|
(segment_path / "rlog.zst").write_bytes(b"log")
|
|
|
|
monkeypatch.setattr(module.utilities, "get_segments_in_route", lambda *_args: segment_names)
|
|
sources, warnings = module.resolve_route_sources(
|
|
[route],
|
|
[str(tmp_path)],
|
|
{route: {"start": 4, "end": 9}},
|
|
)
|
|
|
|
assert [source.segment_num for source in sources] == [4, 5, 6, 7, 8, 9]
|
|
assert warnings == []
|
|
|
|
|
|
def test_segment_range_rejects_reversed_bounds(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
with pytest.raises(ValueError, match="first segment"):
|
|
module.normalize_segment_ranges(["route"], {"route": {"start": 9, "end": 4}})
|
|
|
|
|
|
def test_segment_reader_timeout_interrupts_stalled_log(tmp_path, monkeypatch):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
source = module.RouteSource(
|
|
route="route",
|
|
footage_path=str(tmp_path),
|
|
segment="route--41",
|
|
segment_num=41,
|
|
log_path=str(tmp_path / "route--41" / "rlog.zst"),
|
|
used_qlog=False,
|
|
)
|
|
monkeypatch.setattr(module, "_segment_samples", lambda *_args, **_kwargs: time.sleep(0.2))
|
|
|
|
with pytest.raises(module.FLMSegmentTimeout, match="segment 41"):
|
|
module._segment_samples_with_timeout(source, module.Params(), timeout_seconds=0.02)
|
|
|
|
assert module.FLM_SEGMENT_TIMEOUT_SECONDS == 60.0
|
|
|
|
|
|
def test_analysis_is_rejected_while_onroad(tmp_path):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
fake_params_cls._store = {"IsOnroad": True}
|
|
|
|
with pytest.raises(module.FLMAnalysisCancelled, match="went onroad"):
|
|
module._require_flm_offroad()
|
|
assert module.start_flm_background_analysis(["route"], [str(tmp_path)]) is False
|
|
|
|
|
|
def test_segment_analysis_stops_on_mid_run_onroad_transition(tmp_path, monkeypatch):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
|
|
class TransitionParams:
|
|
calls = 0
|
|
|
|
def get_bool(self, key):
|
|
assert key == "IsOnroad"
|
|
self.calls += 1
|
|
return self.calls >= 2
|
|
|
|
monkeypatch.setattr(module, "LogReader", lambda *args, **kwargs: iter([SimpleNamespace()]))
|
|
source = SimpleNamespace(log_path=tmp_path / "rlog")
|
|
|
|
with pytest.raises(module.FLMAnalysisCancelled, match="went onroad"):
|
|
module._segment_samples(source, params=TransitionParams())
|
|
|
|
|
|
def test_onroad_stop_terminates_process_group_and_preserves_reason(tmp_path, monkeypatch):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
module.FLM_STATUS_PATH = tmp_path / "flm_status.json"
|
|
module._write_flm_status({"pid": 4321, "startedAt": 1.0, "running": True, "state": "analyzing"})
|
|
signals = []
|
|
|
|
class FakeProcess:
|
|
pid = 4321
|
|
|
|
@staticmethod
|
|
def poll():
|
|
return None
|
|
|
|
@staticmethod
|
|
def wait(timeout):
|
|
del timeout
|
|
return 0
|
|
|
|
monkeypatch.setattr(module.os, "getpgid", lambda pid: pid)
|
|
monkeypatch.setattr(module.os, "killpg", lambda pgid, sig: signals.append((pgid, sig)))
|
|
module.FLM_ANALYZER_PROCESS = FakeProcess()
|
|
|
|
assert module.stop_flm_background_analysis(reason="onroad") is True
|
|
assert signals == [(4321, module.signal.SIGTERM)]
|
|
assert module.FLM_ANALYZER_PROCESS is None
|
|
assert module.read_flm_status()["state"] == "cancelled_onroad"
|
|
assert "went onroad" in module.read_flm_status()["error"]
|
|
|
|
|
|
def test_worker_watchdog_exits_immediately_when_vehicle_goes_onroad(tmp_path, monkeypatch):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
module.FLM_STATUS_PATH = tmp_path / "flm_status.json"
|
|
fake_params_cls._store = {"IsOnroad": True}
|
|
module._write_flm_status({"pid": 4321, "startedAt": 1.0, "running": True, "state": "analyzing"})
|
|
|
|
def fake_exit(code):
|
|
raise SystemExit(code)
|
|
|
|
signals = []
|
|
monkeypatch.setattr(module.os, "getpid", lambda: 4321)
|
|
monkeypatch.setattr(module.os, "getpgrp", lambda: 4321)
|
|
monkeypatch.setattr(module.os, "killpg", lambda pgid, sig: signals.append((pgid, sig)))
|
|
monkeypatch.setattr(module.os, "_exit", fake_exit)
|
|
with pytest.raises(SystemExit) as exc:
|
|
module._watch_flm_worker_for_onroad()
|
|
|
|
assert exc.value.code == 0
|
|
assert signals == [(4321, module.signal.SIGTERM)]
|
|
assert module.read_flm_status()["state"] == "cancelled_onroad"
|
|
|
|
|
|
def test_legacy_workspace_is_migrated_to_flm(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
legacy_name = "".join(("f", "t", "m"))
|
|
legacy_root = tmp_path / "starpilot" / "data" / "galaxy" / legacy_name
|
|
legacy_report = legacy_root / "reports" / "legacy.json"
|
|
legacy_report.parent.mkdir(parents=True)
|
|
legacy_report.write_text(json.dumps({
|
|
"reportId": "legacy",
|
|
f"{legacy_name}Overrides": {"vehicleKnobs": {"generic.ff_gain_left": 0.1}},
|
|
"profileLabel": legacy_name.upper(),
|
|
}), encoding="utf-8")
|
|
|
|
workspace = module.ensure_flm_workspace()
|
|
migrated = json.loads((workspace["reports"] / "legacy.json").read_text(encoding="utf-8"))
|
|
|
|
assert not legacy_root.exists()
|
|
assert migrated["flmOverrides"]["vehicleKnobs"]["generic.ff_gain_left"] == pytest.approx(0.1)
|
|
assert migrated["profileLabel"] == "FLM"
|
|
|
|
|
|
def test_classify_torque_samples_detects_center_chatter(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
samples = []
|
|
for idx in range(60):
|
|
angle = 0.75 * math.sin(idx * 0.9)
|
|
samples.append(_sample(
|
|
module,
|
|
t=idx * 0.1,
|
|
desired_la=0.04 * math.sin(idx * 0.1),
|
|
actual_la=0.03 * math.sin(idx * 0.1),
|
|
steering_angle_deg=angle,
|
|
output=0.02 * math.sin(idx * 0.9),
|
|
))
|
|
|
|
summaries, stats = module.classify_torque_samples(samples)
|
|
assert stats["sampleCount"] == len(samples) - 2 # Segment edges are event boundaries, not analysis samples.
|
|
chatter = next(summary for summary in summaries if summary["bucket"] == "center_chatter")
|
|
assert chatter["plotData"]["driverOverrideFree"] is True
|
|
assert len(chatter["plotData"]["times"]) == len(chatter["plotData"]["desired"])
|
|
assert len(chatter["plotData"]["times"]) == len(chatter["plotData"]["actual"])
|
|
|
|
|
|
def test_classify_torque_samples_detects_mid_speed_center_chatter(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
samples = []
|
|
for idx in range(80):
|
|
samples.append(_sample(
|
|
module,
|
|
t=idx * 0.1,
|
|
v_ego=10.0,
|
|
desired_la=0.025 * math.sin(idx * 0.08),
|
|
actual_la=0.09 * math.sin(idx * 0.85),
|
|
steering_angle_deg=0.65 * math.sin(idx * 0.85),
|
|
output=0.035 * math.sin(idx * 0.85),
|
|
))
|
|
|
|
summaries, _ = module.classify_torque_samples(samples)
|
|
chatter = next(summary for summary in summaries if summary["bucket"] == "center_chatter")
|
|
assert chatter["speedBand"] == "mid"
|
|
assert chatter["evidence"]["chatterMetrics"]["steeringReversals"] >= 3
|
|
|
|
|
|
def test_classify_torque_samples_detects_low_speed_center_chatter(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
samples = []
|
|
for idx in range(80):
|
|
samples.append(_sample(
|
|
module,
|
|
t=idx * 0.1,
|
|
v_ego=4.0,
|
|
desired_la=0.018 * math.sin(idx * 0.07),
|
|
actual_la=0.14 * math.sin(idx * 0.78),
|
|
steering_angle_deg=1.05 * math.sin(idx * 0.78),
|
|
output=0.065 * math.sin(idx * 0.78),
|
|
))
|
|
|
|
summaries, _ = module.classify_torque_samples(samples)
|
|
chatter = next(summary for summary in summaries if summary["bucket"] == "center_chatter")
|
|
assert chatter["speedBand"] == "low"
|
|
assert chatter["evidence"]["chatterMetrics"]["outputReversals"] >= 3
|
|
|
|
|
|
def test_classify_torque_samples_rejects_model_driven_center_motion(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
samples = []
|
|
for idx in range(80):
|
|
desired = 0.14 * math.sin(idx * 0.85)
|
|
samples.append(_sample(
|
|
module,
|
|
t=idx * 0.1,
|
|
v_ego=24.0,
|
|
desired_la=desired,
|
|
actual_la=desired * 0.95,
|
|
steering_angle_deg=0.55 * math.sin(idx * 0.85),
|
|
output=0.04 * math.sin(idx * 0.85),
|
|
))
|
|
|
|
summaries, _ = module.classify_torque_samples(samples)
|
|
assert not any(summary["bucket"] == "center_chatter" for summary in summaries)
|
|
|
|
|
|
def test_plot_context_stops_at_ineligible_samples(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
samples = [_sample(module, t=idx * 0.1, desired_la=idx * 0.01, actual_la=idx * 0.009) for idx in range(20)]
|
|
eligibility = [True] * len(samples)
|
|
eligibility[5] = False
|
|
eligibility[14] = False
|
|
event = {
|
|
"startIdx": 8,
|
|
"endIdx": 10,
|
|
"direction": "left",
|
|
"speedBand": "mid",
|
|
}
|
|
|
|
plot = module._build_plot_data(samples, event, eligibility)
|
|
assert plot["driverOverrideFree"] is True
|
|
assert plot["times"] == pytest.approx([idx * 0.1 for idx in range(8)])
|
|
assert plot["eventStartSec"] == pytest.approx(0.2)
|
|
assert plot["eventEndSec"] == pytest.approx(0.4)
|
|
assert plot["segmentLabel"] == "route/0"
|
|
|
|
|
|
def test_analysis_eligibility_masks_driver_override_with_settle_buffer(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
samples = [
|
|
_sample(module, t=idx * 0.1, steering_pressed=(idx == 20))
|
|
for idx in range(50)
|
|
]
|
|
|
|
eligible = module._analysis_eligibility_mask(samples)
|
|
assert eligible[16] is True
|
|
assert eligible[17] is False
|
|
assert eligible[20] is False
|
|
assert eligible[30] is False
|
|
assert eligible[31] is True
|
|
|
|
|
|
def test_stock_param_state_captures_generic_and_rich_defaults(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
torque_tune = SimpleNamespace(friction=0.09, latAccelFactor=3.0)
|
|
lateral_tuning = SimpleNamespace(which=lambda: "torque", torque=torque_tune)
|
|
CP = SimpleNamespace(lateralTuning=lateral_tuning, steerActuatorDelay=0.1, steerRatio=14.26)
|
|
capabilities = {"frictionFamily": "hkg_canfd", "richProfileKey": "hyundai_ioniq_6"}
|
|
|
|
stock = module._stock_param_state(CP, capabilities)
|
|
assert stock["SteerLatAccel"] == pytest.approx(3.0)
|
|
assert stock["SteerFriction"] == pytest.approx(0.09)
|
|
assert stock["UseAutoSteerDelay"] is True
|
|
assert stock["SteerDelay"] == pytest.approx(0.3)
|
|
assert stock["SteerRatio"] == pytest.approx(14.26)
|
|
assert len(stock["FLMBaseFrictionThresholds"]["hkg_canfd"]["values"]) == 5
|
|
assert stock["FLMVehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(1.64)
|
|
|
|
|
|
def test_classify_torque_samples_does_not_bridge_driver_override(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
samples = []
|
|
for idx in range(80):
|
|
samples.append(_sample(
|
|
module,
|
|
t=idx * 0.1,
|
|
desired_la=-0.5,
|
|
actual_la=-0.1,
|
|
desired_jerk=-0.5,
|
|
steering_pressed=30 <= idx <= 40,
|
|
))
|
|
|
|
summaries, stats = module.classify_torque_samples(samples)
|
|
late_events = [event for summary in summaries if summary["bucket"] == "late_turn_in" for event in summary["events"]]
|
|
assert stats["excludedDriverOverrideSamples"] > 11
|
|
assert all(event["endIdx"] < 27 or event["startIdx"] > 50 for event in late_events)
|
|
|
|
|
|
def test_build_suggestions_prefers_rich_low_speed_turn_in_knob(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summary = {
|
|
"bucket": "low_speed_unwillingness",
|
|
"dimensionId": "low_speed_unwillingness:left:low",
|
|
"direction": "left",
|
|
"speedBand": "low",
|
|
"severity": 0.9,
|
|
"evidence": {"speedBand": "low", "directionBias": "left", "eventCount": 3, "segments": [{"label": "route/2"}]},
|
|
"plotSvg": "",
|
|
}
|
|
capabilities = {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"}
|
|
current = {"SteerLatAccel": 1.8, "SteerFriction": 0.2}
|
|
|
|
suggestions = module.build_suggestions([summary], capabilities, current)
|
|
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
|
assert adjustment["type"] == "vehicle_knob"
|
|
assert adjustment["symbol"] == "hyundai_ioniq_6.low_speed_angle_assist_max_torque"
|
|
|
|
|
|
def test_build_suggestions_baseline_prefers_generic_lat_accel_for_understeer(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summary = {
|
|
"bucket": "understeer",
|
|
"dimensionId": "understeer:left:mid",
|
|
"direction": "left",
|
|
"speedBand": "mid",
|
|
"severity": 1.0,
|
|
"evidence": {"speedBand": "mid", "directionBias": "left", "eventCount": 3, "segments": [{"label": "route/2"}]},
|
|
"plotSvg": "",
|
|
}
|
|
capabilities = {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"}
|
|
current = {"SteerLatAccel": 1.8, "SteerFriction": 0.2}
|
|
|
|
suggestions = module.build_suggestions([summary], capabilities, current, strategy="baseline")
|
|
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
|
assert adjustment["type"] == "generic_param"
|
|
assert adjustment["paramKey"] == "SteerLatAccel"
|
|
assert adjustment["suggested"] > adjustment["current"]
|
|
|
|
|
|
def test_build_suggestions_baseline_respects_asymmetric_nonlinear_map(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summary = {
|
|
"bucket": "understeer",
|
|
"dimensionId": "understeer:right:mid",
|
|
"direction": "right",
|
|
"speedBand": "mid",
|
|
"severity": 1.0,
|
|
"evidence": {"speedBand": "mid", "directionBias": "right", "eventCount": 3, "segments": [{"label": "route/2"}]},
|
|
"plotSvg": "",
|
|
}
|
|
capabilities = {
|
|
"richProfileKey": "hyundai_ioniq_6",
|
|
"frictionFamily": "gm",
|
|
"nonlinearTorqueMap": {
|
|
"type": "siglin",
|
|
"left": [2.6, 1.1, 0.19, 0.0],
|
|
"right": [2.7, 1.0, 0.15, 0.0],
|
|
"asymmetric": True,
|
|
},
|
|
}
|
|
current = {"SteerLatAccel": 1.8, "SteerFriction": 0.2}
|
|
|
|
suggestions = module.build_suggestions([summary], capabilities, current, strategy="baseline")
|
|
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
|
assert adjustment["type"] == "vehicle_knob"
|
|
assert adjustment["symbol"] == "hyundai_ioniq_6.ff_gain_right"
|
|
assert adjustment["suggested"] > adjustment["current"]
|
|
|
|
|
|
def test_nonlinear_torque_map_resolves_gm_integration_alias(tmp_path, monkeypatch):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
volt_map = {
|
|
"left": [1.525, 1.05, 0.155, 0.0],
|
|
"right": [1.525, 0.95, 0.150, 0.0],
|
|
}
|
|
monkeypatch.setitem(sys.modules, "opendbc.car.gm.interface", _simple_module(
|
|
"opendbc.car.gm.interface",
|
|
NON_LINEAR_TORQUE_PARAM_ALIASES={"CHEVROLET_VOLT_ASCM": "CHEVROLET_VOLT"},
|
|
get_nonlinear_torque_params=lambda candidate: volt_map if candidate == "CHEVROLET_VOLT_ASCM" else None,
|
|
))
|
|
cp = SimpleNamespace(brand="gm", carFingerprint="CHEVROLET_VOLT_ASCM")
|
|
|
|
nonlinear_map = module._nonlinear_torque_map(cp)
|
|
|
|
assert nonlinear_map["type"] == "siglin"
|
|
assert nonlinear_map["asymmetric"] is True
|
|
assert nonlinear_map["sourceFingerprint"] == "CHEVROLET_VOLT"
|
|
assert nonlinear_map["left"] == volt_map["left"]
|
|
assert nonlinear_map["right"] == volt_map["right"]
|
|
|
|
summary = {
|
|
"bucket": "understeer",
|
|
"dimensionId": "understeer:right:mid",
|
|
"direction": "right",
|
|
"speedBand": "mid",
|
|
"severity": 1.0,
|
|
"evidence": {"speedBand": "mid", "directionBias": "right", "eventCount": 3, "segments": [{"label": "route/2"}]},
|
|
"plotSvg": "",
|
|
}
|
|
capabilities = {"richProfileKey": "torque_universal", "frictionFamily": "gm", "nonlinearTorqueMap": nonlinear_map}
|
|
current = {"SteerLatAccel": 1.8, "SteerFriction": 0.2}
|
|
adjustment = module.build_suggestions([summary], capabilities, current, strategy="cleanup")[0]["primaryAdjustmentRaw"]
|
|
|
|
assert adjustment["type"] == "vehicle_knob"
|
|
assert adjustment["symbol"] == "torque_universal.ff_gain_right"
|
|
assert adjustment["suggested"] > adjustment["current"]
|
|
|
|
summary["bucket"] = "oversteer"
|
|
summary["dimensionId"] = "oversteer:right:mid"
|
|
adjustment = module.build_suggestions([summary], capabilities, current, strategy="cleanup")[0]["primaryAdjustmentRaw"]
|
|
assert adjustment["symbol"] == "torque_universal.ff_gain_right"
|
|
assert adjustment["suggested"] < adjustment["current"]
|
|
|
|
|
|
def test_build_suggestions_rebases_rich_knob_against_active_override(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summary = {
|
|
"bucket": "low_speed_unwillingness",
|
|
"dimensionId": "low_speed_unwillingness:left:low",
|
|
"direction": "left",
|
|
"speedBand": "low",
|
|
"severity": 1.0,
|
|
"evidence": {"speedBand": "low", "directionBias": "left", "eventCount": 3, "segments": [{"label": "route/2"}]},
|
|
"plotSvg": "",
|
|
}
|
|
capabilities = {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"}
|
|
current = {
|
|
"SteerLatAccel": 1.8,
|
|
"SteerFriction": 0.2,
|
|
"FLMActiveOverrides": {
|
|
"schemaVersion": 1,
|
|
"baseFrictionThresholds": {},
|
|
"vehicleKnobs": {
|
|
"hyundai_ioniq_6.low_speed_angle_assist_max_torque": 0.62,
|
|
},
|
|
},
|
|
}
|
|
|
|
suggestions = module.build_suggestions([summary], capabilities, current)
|
|
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
|
assert adjustment["type"] == "vehicle_knob"
|
|
assert adjustment["symbol"] == "hyundai_ioniq_6.low_speed_angle_assist_max_torque"
|
|
assert adjustment["current"] == pytest.approx(0.62)
|
|
assert adjustment["suggested"] > adjustment["current"]
|
|
|
|
|
|
def test_build_suggestions_prefers_ioniq_6_curvy_trim_for_mid_speed_turn_in(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summary = {
|
|
"bucket": "oversteer",
|
|
"dimensionId": "oversteer:left:fast",
|
|
"direction": "left",
|
|
"speedBand": "fast",
|
|
"severity": 1.0,
|
|
"evidence": {"speedBand": "fast", "directionBias": "left", "eventCount": 2, "segments": [{"label": "route/4"}]},
|
|
"plotSvg": "",
|
|
}
|
|
capabilities = {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"}
|
|
current = {"SteerLatAccel": 1.8, "SteerFriction": 0.2}
|
|
|
|
suggestions = module.build_suggestions([summary], capabilities, current)
|
|
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
|
assert adjustment["type"] == "vehicle_knob"
|
|
assert adjustment["symbol"] == "hyundai_ioniq_6.curvy_turn_in_trim_left"
|
|
assert adjustment["suggested"] > adjustment["current"]
|
|
|
|
|
|
def test_build_suggestions_rebases_friction_curve_against_active_override(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summary = {
|
|
"bucket": "center_chatter",
|
|
"dimensionId": "center_chatter:center:highway",
|
|
"direction": "center",
|
|
"speedBand": "highway",
|
|
"severity": 1.0,
|
|
"evidence": {"speedBand": "highway", "directionBias": "center", "eventCount": 4, "segments": [{"label": "route/5"}]},
|
|
"plotSvg": "",
|
|
}
|
|
capabilities = {"richProfileKey": "torque_universal", "frictionFamily": "standard"}
|
|
current_curve = [0.34, 0.35, 0.36, 0.32, 0.33]
|
|
current = {
|
|
"SteerLatAccel": 1.8,
|
|
"SteerFriction": 0.2,
|
|
"FLMActiveOverrides": {
|
|
"schemaVersion": 1,
|
|
"baseFrictionThresholds": {
|
|
"standard": {
|
|
"speedKnots": [0.0, 5.0, 10.0, 15.0, 25.0],
|
|
"values": current_curve,
|
|
},
|
|
},
|
|
"vehicleKnobs": {},
|
|
},
|
|
}
|
|
|
|
suggestions = module.build_suggestions([summary], capabilities, current)
|
|
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
|
assert adjustment["type"] == "friction_curve"
|
|
assert adjustment["family"] == "standard"
|
|
assert adjustment["current"] == current_curve
|
|
assert adjustment["suggested"][4] > current_curve[4]
|
|
|
|
|
|
def test_center_chatter_cleanup_moves_to_deadband_after_threshold_pass(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summary = {
|
|
"bucket": "center_chatter",
|
|
"dimensionId": "center_chatter:center:mid",
|
|
"direction": "center",
|
|
"speedBand": "mid",
|
|
"severity": 0.9,
|
|
"evidence": {"speedBand": "mid", "directionBias": "center", "eventCount": 3, "segments": [{"label": "route/2"}]},
|
|
"plotSvg": "",
|
|
}
|
|
capabilities = {"richProfileKey": "torque_universal", "frictionFamily": "standard"}
|
|
current = {
|
|
"SteerLatAccel": 1.8,
|
|
"SteerFriction": 0.2,
|
|
"FLMActiveOverrides": {
|
|
"schemaVersion": 1,
|
|
"baseFrictionThresholds": {
|
|
"standard": {
|
|
"speedKnots": [0.0, 5.0, 10.0, 15.0, 25.0],
|
|
"values": [0.30, 0.32, 0.34, 0.33, 0.34],
|
|
},
|
|
},
|
|
"vehicleKnobs": {},
|
|
},
|
|
}
|
|
|
|
suggestions = module.build_suggestions([summary], capabilities, current, strategy="cleanup")
|
|
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
|
assert adjustment["type"] == "vehicle_knob"
|
|
assert adjustment["symbol"] == "torque_universal.center_deadband_mid_deg"
|
|
assert adjustment["stage"] == "center_deadband"
|
|
assert adjustment["suggested"] > adjustment["current"]
|
|
|
|
|
|
def test_center_chatter_friction_merge_preserves_each_speed_band(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
current_curve = [0.30, 0.30, 0.30, 0.30, 0.30]
|
|
suggestions = []
|
|
for speed_band in ("low", "highway"):
|
|
adjustment = module._center_chatter_friction_adjustment("standard", speed_band, 1.0, {
|
|
"FLMActiveOverrides": {
|
|
"baseFrictionThresholds": {
|
|
"standard": {"speedKnots": [0.0, 5.0, 10.0, 15.0, 25.0], "values": current_curve},
|
|
},
|
|
},
|
|
})
|
|
suggestions.append({"severity": 1.0, "primaryAdjustmentRaw": adjustment})
|
|
|
|
_, overrides, _ = module._merge_primary_adjustments(suggestions, 1.0)
|
|
merged = overrides["baseFrictionThresholds"]["standard"]["values"]
|
|
assert merged[0] == pytest.approx(0.312)
|
|
assert merged[1] == pytest.approx(0.320)
|
|
assert merged[3] == pytest.approx(0.312)
|
|
assert merged[4] == pytest.approx(0.325)
|
|
|
|
|
|
def test_select_primary_tuning_path_prefers_baseline_for_broad_mismatch(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summaries = [
|
|
{"bucket": "understeer", "severity": 1.0},
|
|
{"bucket": "center_chatter", "severity": 0.9},
|
|
{"bucket": "unwind_too_slow", "severity": 0.85},
|
|
{"bucket": "saturation_limited", "severity": 0.8},
|
|
]
|
|
stats = {"meanErrorAbs": 0.16}
|
|
|
|
decision = module.select_primary_tuning_path(summaries, stats)
|
|
assert decision["primaryPathKey"] == "baseline_fix"
|
|
assert decision["alternatePathKey"] == "cleanup_pass"
|
|
|
|
|
|
def test_select_primary_tuning_path_does_not_automatically_demote_cleanup_progress(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summaries = [
|
|
{"bucket": "understeer", "severity": 1.0},
|
|
{"bucket": "center_chatter", "severity": 0.9},
|
|
{"bucket": "unwind_too_slow", "severity": 0.85},
|
|
{"bucket": "saturation_limited", "severity": 0.8},
|
|
]
|
|
|
|
decision = module.select_primary_tuning_path(summaries, {"meanErrorAbs": 0.16}, cleanup_progress_locked=True)
|
|
|
|
assert decision["primaryPathKey"] == "cleanup_pass"
|
|
assert decision["alternatePathKey"] == "baseline_fix"
|
|
assert decision["rawPrimaryPathKey"] == "baseline_fix"
|
|
assert decision["automaticBaselineDemotionBlocked"] is True
|
|
|
|
|
|
def test_cleanup_progress_bootstraps_from_existing_vehicle_report(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
report = {
|
|
"reportId": "existing-cleanup",
|
|
"primaryPathKey": "cleanup_pass",
|
|
"car": {"carFingerprint": "TEST_TRUCK", "controlPath": "torque"},
|
|
}
|
|
(workspace["reports"] / "existing-cleanup.json").write_text(json.dumps(report), encoding="utf-8")
|
|
|
|
assert module._cleanup_progress_locked("TEST_TRUCK") is True
|
|
progress = json.loads((workspace["root"] / module.FLM_PROGRESS_FILENAME).read_text(encoding="utf-8"))
|
|
assert progress["vehicles"]["TEST_TRUCK"]["minimumPathKey"] == "cleanup_pass"
|
|
|
|
|
|
def test_select_primary_tuning_path_prefers_cleanup_for_localized_issue(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summaries = [
|
|
{"bucket": "notchy_mid_curve", "severity": 0.7},
|
|
{"bucket": "center_chatter", "severity": 0.55},
|
|
]
|
|
stats = {"meanErrorAbs": 0.07}
|
|
|
|
decision = module.select_primary_tuning_path(summaries, stats)
|
|
assert decision["primaryPathKey"] == "cleanup_pass"
|
|
assert decision["alternatePathKey"] == "baseline_fix"
|
|
|
|
|
|
def test_select_primary_tuning_path_vetoes_baseline_when_global_fit_is_strong(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summaries = [
|
|
{
|
|
"bucket": "late_turn_in",
|
|
"severity": 1.05,
|
|
"direction": "right",
|
|
"speedBand": "mid",
|
|
"evidence": {"segments": [{"label": "route/37"}]},
|
|
},
|
|
{"bucket": "center_chatter", "severity": 0.55, "direction": "center", "speedBand": "highway", "evidence": {"segments": []}},
|
|
{"bucket": "unwind_too_slow", "severity": 0.6, "direction": "right", "speedBand": "mid", "evidence": {"segments": [{"label": "route/37"}]}},
|
|
]
|
|
|
|
decision = module.select_primary_tuning_path(summaries, {"meanErrorAbs": 0.054})
|
|
assert decision["primaryPathKey"] == "cleanup_pass"
|
|
assert "already strong" in decision["reason"]
|
|
|
|
|
|
def test_conflicting_summary_resolution_keeps_dominant_direction(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
summaries = [
|
|
{"bucket": "early_turn_in", "severity": 1.34, "evidence": {"directionBias": "right", "speedBand": "mid", "eventCount": 1}},
|
|
{"bucket": "late_turn_in", "severity": 1.03, "evidence": {"directionBias": "right", "speedBand": "mid", "eventCount": 18}},
|
|
]
|
|
|
|
resolved = module._resolve_conflicting_actionable_suggestions(summaries)
|
|
assert [summary["bucket"] for summary in resolved] == ["late_turn_in"]
|
|
|
|
|
|
def test_build_trial_profiles_suppresses_ignored_dimensions(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
suggestions = [
|
|
{
|
|
"dimensionId": "center_chatter:center:highway",
|
|
"primaryAdjustmentRaw": {
|
|
"type": "friction_curve",
|
|
"family": "standard",
|
|
"current": [0.30, 0.31, 0.32, 0.33, 0.34],
|
|
"suggested": [0.31, 0.32, 0.33, 0.34, 0.35],
|
|
"delta": [0.01, 0.01, 0.01, 0.01, 0.01],
|
|
},
|
|
},
|
|
{
|
|
"dimensionId": "understeer:left:mid",
|
|
"primaryAdjustmentRaw": {
|
|
"type": "generic_param",
|
|
"paramKey": "SteerLatAccel",
|
|
"current": 1.6,
|
|
"suggested": 1.7,
|
|
"delta": 0.1,
|
|
},
|
|
},
|
|
]
|
|
feedback = {"acceptedDimensions": ["understeer:left:mid"], "ignoredDimensions": ["center_chatter:center:highway"]}
|
|
profiles = module.build_trial_profiles("report-1", suggestions, feedback, {"richProfileKey": None})
|
|
|
|
assert profiles
|
|
assert profiles[0]["genericParams"]["ForceAutoTuneOff"] is True
|
|
assert profiles[0]["genericParams"]["SteerLatAccel"] > 1.6
|
|
assert profiles[0]["flmOverrides"] == {}
|
|
|
|
|
|
def test_build_trial_profiles_returns_none_when_every_dimension_is_ignored(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
suggestion = {
|
|
"dimensionId": "understeer:left:mid",
|
|
"severity": 0.8,
|
|
"primaryAdjustmentRaw": {
|
|
"type": "generic_param",
|
|
"paramKey": "SteerLatAccel",
|
|
"current": 1.6,
|
|
"suggested": 1.7,
|
|
"delta": 0.1,
|
|
},
|
|
}
|
|
|
|
profiles = module.build_trial_profiles(
|
|
"report-all-ignored",
|
|
[suggestion],
|
|
{"acceptedDimensions": [], "ignoredDimensions": ["understeer:left:mid"]},
|
|
{"richProfileKey": None},
|
|
)
|
|
assert profiles == []
|
|
|
|
|
|
def test_merge_primary_adjustments_averages_conflicting_deltas(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
suggestions = [
|
|
{
|
|
"severity": 1.0,
|
|
"primaryAdjustmentRaw": {
|
|
"type": "generic_param",
|
|
"paramKey": "SteerLatAccel",
|
|
"current": 1.6,
|
|
"suggested": 1.7,
|
|
"delta": 0.1,
|
|
},
|
|
},
|
|
{
|
|
"severity": 0.5,
|
|
"primaryAdjustmentRaw": {
|
|
"type": "generic_param",
|
|
"paramKey": "SteerLatAccel",
|
|
"current": 1.6,
|
|
"suggested": 1.55,
|
|
"delta": -0.05,
|
|
},
|
|
},
|
|
]
|
|
|
|
params_delta, overrides, _ = module._merge_primary_adjustments(suggestions, 1.0)
|
|
assert params_delta["SteerLatAccel"] == pytest.approx(1.65, abs=1e-4)
|
|
assert overrides == {}
|
|
|
|
|
|
def test_merge_primary_adjustments_disables_auto_delay_for_manual_delay_trial(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
suggestions = [{
|
|
"severity": 1.0,
|
|
"primaryAdjustmentRaw": {
|
|
"type": "generic_param",
|
|
"paramKey": "SteerDelay",
|
|
"current": 0.31,
|
|
"suggested": 0.33,
|
|
"delta": 0.02,
|
|
},
|
|
}]
|
|
|
|
params_delta, overrides, _ = module._merge_primary_adjustments(suggestions, 1.0)
|
|
|
|
assert params_delta["SteerDelay"] == pytest.approx(0.33)
|
|
assert params_delta["UseAutoSteerDelay"] is False
|
|
assert overrides == {}
|
|
|
|
|
|
def test_apply_and_revert_trial_profile_round_trip(tmp_path):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
report_id = "report-apply"
|
|
profile_id = f"{report_id}:recommended"
|
|
profile = {
|
|
"id": profile_id,
|
|
"reportId": report_id,
|
|
"label": "Recommended",
|
|
"description": "Recommended trial",
|
|
"genericParams": {
|
|
"AdvancedLateralTune": True,
|
|
"SteerLatAccel": 1.9,
|
|
"ForceAutoTuneOff": True,
|
|
"ForceAutoTune": False,
|
|
},
|
|
"flmOverrides": {
|
|
"schemaVersion": 1,
|
|
"baseFrictionThresholds": {},
|
|
"vehicleKnobs": {
|
|
"hyundai_ioniq_6.turn_in_boost_left": 0.08,
|
|
},
|
|
},
|
|
}
|
|
(workspace["profiles"] / f"{report_id}.json").write_text(json.dumps([profile]), encoding="utf-8")
|
|
|
|
fake_params_cls._store = {
|
|
"AdvancedLateralTune": False,
|
|
"ForceAutoTune": True,
|
|
"ForceAutoTuneOff": False,
|
|
"SteerLatAccel": 1.5,
|
|
"FLMActiveProfileId": "",
|
|
"FLMActiveOverrides": {
|
|
"schemaVersion": 1,
|
|
"baseFrictionThresholds": {},
|
|
"vehicleKnobs": {"hyundai_ioniq_6.unwind_taper_left": 0.55},
|
|
},
|
|
"FLMTrialApplied": False,
|
|
}
|
|
|
|
result = module.apply_trial_profile(report_id, profile_id)
|
|
assert result["profile"]["id"] == profile_id
|
|
active_snapshot = json.loads((workspace["snapshots"] / "active.json").read_text(encoding="utf-8"))
|
|
assert active_snapshot["profileLabel"] == "Recommended"
|
|
assert active_snapshot["appliedGenericParams"]["SteerLatAccel"] == pytest.approx(1.9)
|
|
assert active_snapshot["appliedGenericParams"]["ForceAutoTuneOff"] is True
|
|
assert active_snapshot["appliedVehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
|
|
assert active_snapshot["params"]["SteerLatAccel"] == pytest.approx(1.5)
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.9)
|
|
assert fake_params_cls._store["FLMActiveProfileId"] == profile_id
|
|
assert fake_params_cls._store["FLMTrialApplied"] is True
|
|
assert fake_params_cls._store["FLMTrialBaseline"]["params"]["SteerLatAccel"] == pytest.approx(1.5)
|
|
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
|
|
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55)
|
|
assert fake_params_cls._memory_store["StarPilotTogglesUpdated"] is True
|
|
|
|
fake_params_cls._memory_store["StarPilotTogglesUpdated"] = False
|
|
revert_result = module.revert_trial_profile()
|
|
assert revert_result["snapshot"]["profileId"] == profile_id
|
|
assert fake_params_cls._store["AdvancedLateralTune"] is False
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
|
|
assert fake_params_cls._store["FLMTrialApplied"] is False
|
|
assert "FLMTrialBaseline" not in fake_params_cls._store
|
|
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55)
|
|
assert fake_params_cls._memory_store["StarPilotTogglesUpdated"] is True
|
|
|
|
|
|
def test_repeated_trial_revisions_revert_to_original_baseline(tmp_path):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
first_report_id = "report-first"
|
|
first_profile_id = f"{first_report_id}:cleanup_pass:recommended"
|
|
second_report_id = "report-second"
|
|
second_profile_id = f"{second_report_id}:cleanup_pass:recommended"
|
|
first_profile = {
|
|
"id": first_profile_id,
|
|
"label": "Recommended",
|
|
"pathKey": "cleanup_pass",
|
|
"pathLabel": "Cleanup Pass",
|
|
"genericParams": {"AdvancedLateralTune": True, "SteerLatAccel": 1.8},
|
|
"flmOverrides": {
|
|
"schemaVersion": 1,
|
|
"baseFrictionThresholds": {},
|
|
"vehicleKnobs": {"hyundai_ioniq_6.turn_in_boost_left": 0.08},
|
|
},
|
|
}
|
|
second_profile = {
|
|
"id": second_profile_id,
|
|
"label": "Recommended",
|
|
"pathKey": "cleanup_pass",
|
|
"pathLabel": "Cleanup Pass",
|
|
"genericParams": {"AdvancedLateralTune": True, "SteerLatAccel": 1.9},
|
|
"flmOverrides": {
|
|
"schemaVersion": 1,
|
|
"baseFrictionThresholds": {},
|
|
"vehicleKnobs": {"hyundai_ioniq_6.unwind_taper_left": 0.62},
|
|
},
|
|
}
|
|
(workspace["profiles"] / f"{first_report_id}.json").write_text(json.dumps([first_profile]), encoding="utf-8")
|
|
(workspace["profiles"] / f"{second_report_id}.json").write_text(json.dumps([second_profile]), encoding="utf-8")
|
|
fake_params_cls._store = {
|
|
"AdvancedLateralTune": False,
|
|
"SteerLatAccel": 1.5,
|
|
"FLMActiveProfileId": "",
|
|
"FLMActiveOverrides": {},
|
|
"FLMTrialApplied": False,
|
|
}
|
|
|
|
module.apply_trial_profile(first_report_id, first_profile_id)
|
|
module.apply_trial_profile(second_report_id, second_profile_id)
|
|
|
|
active_snapshot = json.loads((workspace["snapshots"] / "active.json").read_text(encoding="utf-8"))
|
|
assert active_snapshot["revisionCount"] == 2
|
|
assert active_snapshot["params"]["SteerLatAccel"] == pytest.approx(1.5)
|
|
assert active_snapshot["params"]["FLMTrialApplied"] is False
|
|
assert active_snapshot["appliedGenericParams"]["SteerLatAccel"] == pytest.approx(1.9)
|
|
assert active_snapshot["appliedVehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
|
|
assert active_snapshot["appliedVehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.62)
|
|
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
|
|
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.62)
|
|
|
|
module.revert_trial_profile()
|
|
assert fake_params_cls._store["AdvancedLateralTune"] is False
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
|
|
assert fake_params_cls._store["FLMTrialApplied"] is False
|
|
assert fake_params_cls._store["FLMActiveOverrides"] == {}
|
|
|
|
|
|
def test_saved_tunes_switch_cleanly_and_revert_to_original_baseline(tmp_path, monkeypatch):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
monkeypatch.setattr(module, "_current_car_identity", lambda _params: {"carFingerprint": "TEST_CAR", "brand": "test"})
|
|
|
|
first_report_id = "report-save-first"
|
|
first_profile_id = f"{first_report_id}:cleanup_pass:recommended"
|
|
second_report_id = "report-save-second"
|
|
second_profile_id = f"{second_report_id}:cleanup_pass:recommended"
|
|
first_profile = {
|
|
"id": first_profile_id,
|
|
"label": "First Trial",
|
|
"pathKey": "cleanup_pass",
|
|
"pathLabel": "Cleanup Pass",
|
|
"genericParams": {
|
|
"AdvancedLateralTune": True,
|
|
"SteerFriction": 0.2,
|
|
"SteerLatAccel": 1.9,
|
|
},
|
|
"flmOverrides": {
|
|
"baseFrictionThresholds": {},
|
|
"vehicleKnobs": {"hyundai_ioniq_6.turn_in_boost_left": 0.08},
|
|
},
|
|
}
|
|
second_profile = {
|
|
"id": second_profile_id,
|
|
"label": "Second Trial",
|
|
"pathKey": "cleanup_pass",
|
|
"pathLabel": "Cleanup Pass",
|
|
"genericParams": {
|
|
"AdvancedLateralTune": True,
|
|
"SteerLatAccel": 2.0,
|
|
},
|
|
"flmOverrides": {
|
|
"baseFrictionThresholds": {},
|
|
"vehicleKnobs": {"hyundai_ioniq_6.unwind_taper_left": 0.62},
|
|
},
|
|
}
|
|
for report_id, profile in ((first_report_id, first_profile), (second_report_id, second_profile)):
|
|
(workspace["reports"] / f"{report_id}.json").write_text(json.dumps({
|
|
"reportId": report_id,
|
|
"car": {"carFingerprint": "TEST_CAR", "brand": "test"},
|
|
}), encoding="utf-8")
|
|
(workspace["profiles"] / f"{report_id}.json").write_text(json.dumps([profile]), encoding="utf-8")
|
|
|
|
fake_params_cls._store = {
|
|
"AdvancedLateralTune": False,
|
|
"ForceAutoTune": False,
|
|
"ForceAutoTuneOff": True,
|
|
"UseAutoSteerDelay": False,
|
|
"SteerDelay": 0.35,
|
|
"SteerFriction": 0.1,
|
|
"SteerKP": 1.0,
|
|
"SteerLatAccel": 1.5,
|
|
"SteerRatio": 15.0,
|
|
"FLMActiveProfileId": "",
|
|
"FLMActiveOverrides": {},
|
|
"FLMTrialApplied": False,
|
|
}
|
|
|
|
module.apply_trial_profile(first_report_id, first_profile_id)
|
|
first_tune = module.save_active_trial_as_tune("No Trailer")["tune"]
|
|
assert fake_params_cls._store["FLMActiveProfileId"] == f"saved:{first_tune['tuneId']}"
|
|
assert next(tune for tune in module.list_workspace()["savedTunes"] if tune["tuneId"] == first_tune["tuneId"])["active"] is True
|
|
module.revert_trial_profile()
|
|
module.apply_trial_profile(second_report_id, second_profile_id)
|
|
second_tune = module.save_active_trial_as_tune("With Trailer")["tune"]
|
|
|
|
module.apply_saved_tune(first_tune["tuneId"])
|
|
assert fake_params_cls._store["SteerFriction"] == pytest.approx(0.2)
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.9)
|
|
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"] == {
|
|
"hyundai_ioniq_6.turn_in_boost_left": pytest.approx(0.08),
|
|
}
|
|
|
|
module.apply_saved_tune(second_tune["tuneId"])
|
|
assert fake_params_cls._store["SteerFriction"] == pytest.approx(0.1)
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(2.0)
|
|
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"] == {
|
|
"hyundai_ioniq_6.unwind_taper_left": pytest.approx(0.62),
|
|
}
|
|
workspace_state = module.list_workspace()
|
|
assert next(tune for tune in workspace_state["savedTunes"] if tune["tuneId"] == second_tune["tuneId"])["active"] is True
|
|
|
|
module.revert_trial_profile()
|
|
assert fake_params_cls._store["AdvancedLateralTune"] is False
|
|
assert fake_params_cls._store["SteerFriction"] == pytest.approx(0.1)
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
|
|
assert fake_params_cls._store["FLMActiveOverrides"] == {}
|
|
assert fake_params_cls._store["FLMTrialApplied"] is False
|
|
|
|
|
|
def test_saved_tune_rename_delete_and_vehicle_guard(tmp_path, monkeypatch):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
tune_id = "tune-test"
|
|
tune_path = workspace["savedTunes"] / f"{tune_id}.json"
|
|
tune_path.write_text(json.dumps({
|
|
"schemaVersion": 1,
|
|
"tuneId": tune_id,
|
|
"name": "Original",
|
|
"createdAt": 1.0,
|
|
"updatedAt": 1.0,
|
|
"carFingerprint": "CAR_A",
|
|
"genericParams": {"SteerLatAccel": 1.9},
|
|
"flmOverrides": {},
|
|
}), encoding="utf-8")
|
|
fake_params_cls._store = {
|
|
"SteerLatAccel": 1.5,
|
|
"FLMActiveProfileId": "",
|
|
"FLMActiveOverrides": {},
|
|
"FLMTrialApplied": False,
|
|
}
|
|
|
|
monkeypatch.setattr(module, "_current_car_identity", lambda _params: {"carFingerprint": "CAR_B", "brand": "test"})
|
|
with pytest.raises(RuntimeError, match="connected car is CAR_B"):
|
|
module.apply_saved_tune(tune_id)
|
|
|
|
monkeypatch.setattr(module, "_current_car_identity", lambda _params: {"carFingerprint": "CAR_A", "brand": "test"})
|
|
rename_result = module.rename_saved_tune(tune_id, " Tow Setup ")
|
|
assert rename_result["tune"]["name"] == "Tow Setup"
|
|
module.apply_saved_tune(tune_id)
|
|
with pytest.raises(RuntimeError, match="Revert or switch"):
|
|
module.delete_saved_tune(tune_id)
|
|
module.revert_trial_profile()
|
|
delete_result = module.delete_saved_tune(tune_id)
|
|
assert "Deleted saved tune Tow Setup" in delete_result["message"]
|
|
assert not tune_path.exists()
|
|
|
|
|
|
def test_submit_saved_tune_queues_credit_and_tune_only(tmp_path):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
tune_id = "tune-submit"
|
|
(workspace["savedTunes"] / f"{tune_id}.json").write_text(json.dumps({
|
|
"schemaVersion": 1,
|
|
"tuneId": tune_id,
|
|
"name": "Good Curve Tune",
|
|
"createdAt": 1.0,
|
|
"updatedAt": 1.0,
|
|
"carFingerprint": "HYUNDAI_IONIQ_6",
|
|
"brand": "hyundai",
|
|
"sourceReportId": "report-private",
|
|
"pathLabel": "Cleanup Pass",
|
|
"baselineParams": {"SteerLatAccel": 2.1},
|
|
"genericParams": {"SteerLatAccel": 2.3},
|
|
"flmOverrides": {"vehicleKnobs": {"turn_in_boost": 0.1}},
|
|
"routeNames": ["must-not-be-submitted"],
|
|
}), encoding="utf-8")
|
|
fake_params_cls._store = {"IsOnroad": False}
|
|
fake_params_cls._memory_store = {}
|
|
|
|
result = module.submit_saved_tune(tune_id, "@tuner")
|
|
submission = fake_params_cls._memory_store["FLMSubmittedTune"]
|
|
|
|
assert result["carName"] == "Hyundai Ioniq 6"
|
|
assert submission["discordUsername"] == "@tuner"
|
|
assert submission["carName"] == "Hyundai Ioniq 6"
|
|
assert submission["tune"]["genericParams"] == {"SteerLatAccel": 2.3}
|
|
assert "routeNames" not in submission["tune"]
|
|
assert "routes" not in submission["tune"]
|
|
assert "sourceReportId" not in submission["tune"]
|
|
assert "pathLabel" not in submission["tune"]
|
|
|
|
with pytest.raises(ValueError, match="Discord username"):
|
|
module.submit_saved_tune(tune_id, "")
|
|
|
|
fake_params_cls._store["IsOnroad"] = True
|
|
with pytest.raises(module.FLMAnalysisCancelled, match="went onroad"):
|
|
module.submit_saved_tune(tune_id, "@tuner")
|
|
|
|
|
|
def test_saved_tune_car_switch_uses_the_destination_car_baseline(tmp_path, monkeypatch):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
tune_id = "tune-car-b"
|
|
(workspace["savedTunes"] / f"{tune_id}.json").write_text(json.dumps({
|
|
"schemaVersion": 1,
|
|
"tuneId": tune_id,
|
|
"name": "Car B",
|
|
"createdAt": 1.0,
|
|
"updatedAt": 1.0,
|
|
"carFingerprint": "CAR_B",
|
|
"baselineParams": {
|
|
"AdvancedLateralTune": False,
|
|
"SteerFriction": 0.08,
|
|
"SteerLatAccel": 1.3,
|
|
"FLMActiveProfileId": "",
|
|
"FLMActiveOverrides": {},
|
|
"FLMTrialApplied": False,
|
|
},
|
|
"genericParams": {"AdvancedLateralTune": True, "SteerLatAccel": 2.1},
|
|
"flmOverrides": {},
|
|
}), encoding="utf-8")
|
|
car_a_baseline = {
|
|
"AdvancedLateralTune": False,
|
|
"SteerFriction": 0.12,
|
|
"SteerLatAccel": 1.6,
|
|
"FLMActiveProfileId": "",
|
|
"FLMActiveOverrides": {},
|
|
"FLMTrialApplied": False,
|
|
}
|
|
(workspace["snapshots"] / "active.json").write_text(json.dumps({
|
|
"reportId": "",
|
|
"profileId": "saved:tune-car-a",
|
|
"profileLabel": "Car A",
|
|
"savedTuneId": "tune-car-a",
|
|
"carFingerprint": "CAR_A",
|
|
"capturedAt": 1.0,
|
|
"params": car_a_baseline,
|
|
"appliedGenericParams": {"AdvancedLateralTune": True, "SteerLatAccel": 1.9},
|
|
"appliedFrictionThresholds": {},
|
|
"appliedVehicleKnobs": {},
|
|
}), encoding="utf-8")
|
|
fake_params_cls._store = {
|
|
"AdvancedLateralTune": True,
|
|
"SteerFriction": 0.12,
|
|
"SteerLatAccel": 1.9,
|
|
"FLMActiveProfileId": "saved:tune-car-a",
|
|
"FLMActiveOverrides": {},
|
|
"FLMTrialApplied": True,
|
|
"FLMTrialBaseline": {"params": car_a_baseline},
|
|
}
|
|
monkeypatch.setattr(module, "_current_car_identity", lambda _params: {"carFingerprint": "CAR_B", "brand": "test"})
|
|
|
|
module.apply_saved_tune(tune_id)
|
|
assert fake_params_cls._store["SteerFriction"] == pytest.approx(0.08)
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(2.1)
|
|
module.revert_trial_profile()
|
|
assert fake_params_cls._store["AdvancedLateralTune"] is False
|
|
assert fake_params_cls._store["SteerFriction"] == pytest.approx(0.08)
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.3)
|
|
|
|
|
|
def test_orphaned_previous_revision_can_recover_its_baseline(tmp_path):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
report_id = "report-recovery"
|
|
profile_id = f"{report_id}:cleanup_pass:recommended"
|
|
profile = {
|
|
"id": profile_id,
|
|
"label": "Recommended",
|
|
"pathKey": "cleanup_pass",
|
|
"pathLabel": "Cleanup Pass",
|
|
"genericParams": {"AdvancedLateralTune": True, "SteerLatAccel": 1.8},
|
|
"flmOverrides": {},
|
|
}
|
|
(workspace["profiles"] / f"{report_id}.json").write_text(json.dumps([profile]), encoding="utf-8")
|
|
fake_params_cls._store = {
|
|
"AdvancedLateralTune": False,
|
|
"SteerLatAccel": 1.5,
|
|
"FLMActiveProfileId": "",
|
|
"FLMActiveOverrides": {},
|
|
"FLMTrialApplied": False,
|
|
}
|
|
module.apply_trial_profile(report_id, profile_id)
|
|
(workspace["snapshots"] / "active.json").unlink()
|
|
|
|
active_trial = module.list_workspace()["activeTrial"]
|
|
assert active_trial["recoveryNeeded"] is True
|
|
assert active_trial["params"]["SteerLatAccel"] == pytest.approx(1.5)
|
|
|
|
module.revert_trial_profile()
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
|
|
assert fake_params_cls._store["FLMTrialApplied"] is False
|
|
|
|
|
|
def test_persistent_baseline_recovers_when_snapshot_files_are_missing(tmp_path):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
report_id = "report-persistent-recovery"
|
|
profile_id = f"{report_id}:cleanup_pass:recommended"
|
|
profile = {
|
|
"id": profile_id,
|
|
"label": "Recommended",
|
|
"genericParams": {"AdvancedLateralTune": True, "SteerLatAccel": 1.8},
|
|
"flmOverrides": {},
|
|
}
|
|
(workspace["profiles"] / f"{report_id}.json").write_text(json.dumps([profile]), encoding="utf-8")
|
|
fake_params_cls._store = {
|
|
"AdvancedLateralTune": False,
|
|
"SteerLatAccel": 1.5,
|
|
"FLMActiveProfileId": "",
|
|
"FLMActiveOverrides": {},
|
|
"FLMTrialApplied": False,
|
|
}
|
|
|
|
module.apply_trial_profile(report_id, profile_id)
|
|
for path in workspace["snapshots"].glob("*.json"):
|
|
path.unlink()
|
|
|
|
active_trial = module.list_workspace()["activeTrial"]
|
|
assert active_trial["rollbackAvailable"] is True
|
|
assert active_trial["recoveryNeeded"] is True
|
|
|
|
module.revert_trial_profile()
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
|
|
assert fake_params_cls._store["FLMTrialApplied"] is False
|
|
|
|
|
|
def test_legacy_orphan_recovers_baseline_from_source_report(tmp_path):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
report_id = "report-source-recovery"
|
|
profile_id = f"{report_id}:baseline_fix:assertive"
|
|
(workspace["reports"] / f"{report_id}.json").write_text(json.dumps({
|
|
"reportId": report_id,
|
|
"createdAt": 123.0,
|
|
"currentParams": {
|
|
"AdvancedLateralTune": False,
|
|
"SteerLatAccel": 1.5,
|
|
"FLMActiveProfileId": "",
|
|
"FLMActiveOverrides": {},
|
|
"FLMTrialApplied": False,
|
|
},
|
|
}), encoding="utf-8")
|
|
fake_params_cls._store = {
|
|
"AdvancedLateralTune": True,
|
|
"SteerLatAccel": 1.9,
|
|
"FLMActiveProfileId": profile_id,
|
|
"FLMActiveOverrides": {},
|
|
"FLMTrialApplied": True,
|
|
}
|
|
|
|
active_trial = module.list_workspace()["activeTrial"]
|
|
assert active_trial["rollbackAvailable"] is True
|
|
assert active_trial["recoveryNeeded"] is True
|
|
|
|
module.revert_trial_profile()
|
|
assert fake_params_cls._store["AdvancedLateralTune"] is False
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
|
|
assert fake_params_cls._store["FLMTrialApplied"] is False
|
|
|
|
|
|
def test_irrecoverable_trial_can_keep_current_values_as_new_baseline(tmp_path):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
module.ensure_flm_workspace()
|
|
fake_params_cls._store = {
|
|
"AdvancedLateralTune": True,
|
|
"SteerLatAccel": 1.9,
|
|
"FLMActiveProfileId": "missing-report:baseline_fix:assertive",
|
|
"FLMActiveOverrides": {"vehicleKnobs": {"generic.turn_in_boost_left": 0.1}},
|
|
"FLMTrialApplied": True,
|
|
}
|
|
|
|
active_trial = module.list_workspace()["activeTrial"]
|
|
assert active_trial["rollbackAvailable"] is False
|
|
|
|
module.accept_trial_as_baseline()
|
|
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.9)
|
|
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["generic.turn_in_boost_left"] == pytest.approx(0.1)
|
|
assert fake_params_cls._store["FLMActiveProfileId"] == ""
|
|
assert fake_params_cls._store["FLMTrialApplied"] is False
|
|
assert fake_params_cls._memory_store["StarPilotTogglesUpdated"] is True
|
|
|
|
|
|
def test_workspace_hydrates_display_metadata_for_existing_active_trial(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
report_id = "report-existing"
|
|
profile_id = f"{report_id}:cleanup_pass:recommended"
|
|
profile = {
|
|
"id": profile_id,
|
|
"label": "Recommended",
|
|
"pathKey": "cleanup_pass",
|
|
"pathLabel": "Cleanup Pass",
|
|
"genericParams": {"AdvancedLateralTune": True, "SteerFriction": 0.25},
|
|
"flmOverrides": {
|
|
"schemaVersion": 1,
|
|
"baseFrictionThresholds": {},
|
|
"vehicleKnobs": {"hyundai_ioniq_6.ff_gain_left": 0.15},
|
|
},
|
|
}
|
|
(workspace["profiles"] / f"{report_id}.json").write_text(json.dumps([profile]), encoding="utf-8")
|
|
(workspace["snapshots"] / "active.json").write_text(json.dumps({
|
|
"reportId": report_id,
|
|
"profileId": profile_id,
|
|
"capturedAt": 123.0,
|
|
"params": {"SteerFriction": 0.1},
|
|
}), encoding="utf-8")
|
|
|
|
active_trial = module.list_workspace()["activeTrial"]
|
|
assert active_trial["pathLabel"] == "Cleanup Pass"
|
|
assert active_trial["appliedGenericParams"]["SteerFriction"] == pytest.approx(0.25)
|
|
assert active_trial["appliedVehicleKnobs"]["hyundai_ioniq_6.ff_gain_left"] == pytest.approx(0.15)
|
|
|
|
|
|
def test_delete_report_removes_saved_artifacts(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
report_id = "report-delete"
|
|
for path in (
|
|
workspace["reports"] / f"{report_id}.json",
|
|
workspace["reports"] / f"{report_id}.html",
|
|
workspace["profiles"] / f"{report_id}.json",
|
|
workspace["feedback"] / f"{report_id}.json",
|
|
workspace["snapshots"] / f"{report_id}-recommended.json",
|
|
):
|
|
path.write_text("{}", encoding="utf-8")
|
|
|
|
result = module.delete_report(report_id)
|
|
assert "Deleted tuning report" in result["message"]
|
|
assert not (workspace["reports"] / f"{report_id}.json").exists()
|
|
assert not (workspace["reports"] / f"{report_id}.html").exists()
|
|
assert not (workspace["profiles"] / f"{report_id}.json").exists()
|
|
assert not (workspace["feedback"] / f"{report_id}.json").exists()
|
|
assert not (workspace["snapshots"] / f"{report_id}-recommended.json").exists()
|
|
|
|
|
|
def test_delete_report_is_blocked_while_trial_is_active(tmp_path):
|
|
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
report_id = "report-active-delete"
|
|
report_path = workspace["reports"] / f"{report_id}.json"
|
|
report_path.write_text("{}", encoding="utf-8")
|
|
fake_params_cls._store = {"FLMTrialApplied": True}
|
|
|
|
with pytest.raises(RuntimeError, match="Revert or keep"):
|
|
module.delete_report(report_id)
|
|
assert report_path.exists()
|
|
|
|
|
|
def test_select_report_path_persists_manual_override(tmp_path):
|
|
module, _ = _load_flm_workspace_module(tmp_path)
|
|
workspace = module.ensure_flm_workspace()
|
|
report_id = "report-path"
|
|
suggestion_base = {
|
|
"evidence": {"speedBand": "mixed", "directionBias": "center", "eventCount": 0, "segments": []},
|
|
"currentVsSuggested": None,
|
|
"observedBehavior": "test",
|
|
"likelyInterpretation": "test",
|
|
"primaryAdjustment": "test",
|
|
"whatNotToTouchYet": "test",
|
|
"ifThatWasWrong": "test",
|
|
"plotSvg": "",
|
|
}
|
|
cleanup_suggestion = {**suggestion_base, "dimensionId": "cleanup", "bucket": "model_limited"}
|
|
baseline_suggestion = {**suggestion_base, "dimensionId": "baseline", "bucket": "understeer"}
|
|
report = {
|
|
"reportId": report_id,
|
|
"routeNames": ["route"],
|
|
"car": {"carFingerprint": "TEST", "controlPath": "torque", "gitBranch": "", "gitCommit": ""},
|
|
"capabilities": {"frictionFamily": "standard", "richProfileKey": "hyundai_ioniq_6", "nonlinearTorqueMap": {}},
|
|
"primaryPathKey": "cleanup_pass",
|
|
"selectedPathKey": "cleanup_pass",
|
|
"pathSelectionSource": "auto",
|
|
"paths": [
|
|
{"key": "cleanup_pass", "title": "Cleanup Pass", "isPrimary": True, "suggestions": [cleanup_suggestion], "profiles": []},
|
|
{"key": "baseline_fix", "title": "Baseline Fix", "isPrimary": False, "suggestions": [baseline_suggestion], "profiles": []},
|
|
],
|
|
"suggestions": [cleanup_suggestion],
|
|
"profiles": [],
|
|
"addTheseParametersAndStartHere": [],
|
|
}
|
|
(workspace["reports"] / f"{report_id}.json").write_text(json.dumps(report), encoding="utf-8")
|
|
|
|
result = module.select_report_path(report_id, "baseline_fix")
|
|
selected = result["report"]
|
|
assert selected["selectedPathKey"] == "baseline_fix"
|
|
assert selected["pathSelectionSource"] == "manual"
|
|
assert selected["primaryPathKey"] == "cleanup_pass"
|
|
assert selected["suggestions"] == [baseline_suggestion]
|