This commit is contained in:
firestar5683
2026-07-12 23:09:58 -05:00
parent 6534ce356a
commit 46c3592563
10 changed files with 365 additions and 23 deletions
+1
View File
@@ -321,6 +321,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ForceTorqueController", {PERSISTENT, BOOL, "0", "0", 3}},
{"FTMActiveOverrides", {PERSISTENT, JSON, "{}", "{}", 2}},
{"FTMActiveProfileId", {PERSISTENT, STRING, "", "", 2}},
{"FTMTrialBaseline", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
{"FTMTrialApplied", {PERSISTENT, BOOL, "0", "0", 2}},
{"FPSCounter", {PERSISTENT, BOOL, "1", "0", 3}},
{"GalaxyDashboardStats", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
@@ -305,7 +305,53 @@ def classifier_reject_row(row: dict[str, str], split: str) -> dict[str, object]:
}
def positive_classifier_row(row: dict[str, str], split: str) -> dict[str, object]:
def corrected_classifier_crop(
row: dict[str, str],
output_dir: Path,
overwrite: bool,
) -> tuple[str, str, bool]:
original_crop = Path(row.get("crop_path", "")).expanduser()
original_bbox = parse_bbox(row.get("bbox", ""))
review_bbox = parse_bbox(row.get("review_bbox", ""))
if review_bbox is None or review_bbox == original_bbox:
return str(original_crop), row.get("crop_bbox", ""), False
frame_path = Path(row.get("frame_path", "")).expanduser()
frame = cv2.imread(str(frame_path))
if frame is None:
raise RuntimeError(f"Cannot regenerate corrected crop for {row['record_key']}: unreadable frame {frame_path}")
image_h, image_w = frame.shape[:2]
x1, y1, x2, y2 = review_bbox
pad_x = max(round((x2 - x1) * 0.10), 2)
pad_y = max(round((y2 - y1) * 0.10), 2)
crop_bbox = (
max(x1 - pad_x, 0),
max(y1 - pad_y, 0),
min(x2 + pad_x, image_w),
min(y2 + pad_y, image_h),
)
crop_x1, crop_y1, crop_x2, crop_y2 = crop_bbox
crop = frame[crop_y1:crop_y2, crop_x1:crop_x2]
if crop.size == 0:
raise RuntimeError(f"Cannot regenerate corrected crop for {row['record_key']}: empty review bbox {review_bbox}")
corrected_dir = output_dir / "corrected_classifier_crops"
corrected_path = corrected_dir / f"{safe_stem(row['record_key'])}_crop.jpg"
if overwrite or not corrected_path.is_file():
ensure_dir(corrected_dir)
if not cv2.imwrite(str(corrected_path), crop, [cv2.IMWRITE_JPEG_QUALITY, 94]):
raise RuntimeError(f"Cannot write corrected crop for {row['record_key']}: {corrected_path}")
crop_bbox_text = ",".join(str(value) for value in crop_bbox)
return str(corrected_path), crop_bbox_text, True
def positive_classifier_row(
row: dict[str, str],
split: str,
crop_path: str | None = None,
crop_bbox: str | None = None,
) -> dict[str, object]:
speed = parse_speed(row.get("review_speed_limit_mph", ""))
return {
"record_key": row["record_key"],
@@ -315,10 +361,10 @@ def positive_classifier_row(row: dict[str, str], split: str) -> dict[str, object
"split": split,
"speed_limit_mph": speed,
"review_sign_type": effective_sign_type(row),
"crop_path": row.get("crop_path", ""),
"crop_path": crop_path if crop_path is not None else row.get("crop_path", ""),
"frame_path": row.get("frame_path", ""),
"bbox": row.get("bbox", ""),
"crop_bbox": row.get("crop_bbox", ""),
"bbox": row.get("review_bbox") or row.get("bbox", ""),
"crop_bbox": crop_bbox if crop_bbox is not None else row.get("crop_bbox", ""),
"review_status": row.get("review_status", ""),
"candidate_speed_limit_mph": row.get("candidate_speed_limit_mph", ""),
"candidate_confidence": row.get("candidate_confidence", ""),
@@ -436,10 +482,13 @@ def main() -> int:
runtime_rows: list[dict[str, object]] = []
detector_rows: list[dict[str, object]] = []
reject_rows: list[dict[str, object]] = []
corrected_classifier_crops = 0
for row in positive_rows:
split = split_for_key(split_group_key(row), args.val_modulo, args.val_remainder)
classifier_rows.append(positive_classifier_row(row, split))
classifier_crop_path, classifier_crop_bbox, corrected = corrected_classifier_crop(row, output_dir, args.overwrite)
classifier_rows.append(positive_classifier_row(row, split, classifier_crop_path, classifier_crop_bbox))
corrected_classifier_crops += int(corrected)
sample_type = "advisory_negative" if is_advisory_positive(row) else "positive"
runtime_rows.append(runtime_row(row, split, sample_type))
detector_row = import_detector_example(workspace, row, split, args.source_name, "positive", args.mode, args.overwrite)
@@ -475,6 +524,7 @@ def main() -> int:
"uncertain_positive_rows": len(uncertain_positive_rows),
"true_negative_rows": len(true_negative_rows),
"classifier_reject_rows": len(reject_rows),
"corrected_classifier_crops": corrected_classifier_crops,
"classifier_manifest": str(classifier_manifest),
"runtime_manifest": str(runtime_manifest),
"detector_manifest": str(detector_manifest),
@@ -114,8 +114,8 @@ HTML = r"""<!doctype html>
<button data-status="uncertain">Uncertain (u)</button>
<button data-status="needs_later">Needs Later</button>
</div>
<label>Box</label>
<input id="bboxInput" placeholder="x1,y1,x2,y2 - drag on frame to set">
<label>Box (optional - redraw only when the current box is wrong)</label>
<input id="bboxInput" placeholder="Drag around the complete sign only when correction is needed">
<div class="buttons">
<button id="clearBBoxBtn">Clear Box (b)</button>
</div>
@@ -191,3 +191,31 @@ def test_rescore_row_preserves_before_values_and_marks_gained_read(tmp_path):
assert rescored["before_speed_limit_mph"] == ""
assert rescored["candidate_speed_limit_mph"] == "20"
assert rescored["comparison_change"] == "gained_read"
def test_corrected_bbox_regenerates_classifier_crop(tmp_path):
import cv2
import numpy as np
frame_path = tmp_path / "frame.jpg"
original_crop_path = tmp_path / "original_crop.jpg"
frame = np.zeros((100, 200, 3), dtype=np.uint8)
frame[20:80, 60:140] = 255
cv2.imwrite(str(frame_path), frame)
cv2.imwrite(str(original_crop_path), np.zeros((20, 20, 3), dtype=np.uint8))
row = {
"record_key": "corrected-box",
"frame_path": str(frame_path),
"crop_path": str(original_crop_path),
"bbox": "0,0,20,20",
"crop_bbox": "0,0,24,24",
"review_bbox": "60,20,140,80",
}
crop_path, crop_bbox, corrected = import_queue.corrected_classifier_crop(row, tmp_path, overwrite=False)
crop = cv2.imread(crop_path)
assert corrected
assert crop is not None and crop.shape[:2] == (72, 96)
assert crop.mean() > 150
assert crop_bbox == "52,14,148,86"
+1
View File
@@ -234,6 +234,7 @@ EXCLUDED_KEYS = {
"ExperimentalLongitudinalEnabled",
"FTMActiveOverrides",
"FTMActiveProfileId",
"FTMTrialBaseline",
"FTMTrialApplied",
"InstallDate",
"StarPilotCarParamsPersistent",
@@ -368,6 +368,7 @@ async function applyProfile(profileId) {
})
const payload = await response.json()
if (!response.ok) throw new Error(payload.error || "Failed to apply trial profile.")
state.error = ""
await fetchWorkspace()
showSnackbar(payload.message || "Trial profile applied.")
} catch (error) {
@@ -409,6 +410,7 @@ async function revertProfile() {
const response = await fetch("/api/ftm/trials/revert", { method: "POST" })
const payload = await response.json()
if (!response.ok) throw new Error(payload.error || "Failed to revert trial profile.")
state.error = ""
await fetchWorkspace()
showSnackbar(payload.message || "Trial profile reverted.")
} catch (error) {
@@ -419,6 +421,26 @@ async function revertProfile() {
}
}
async function acceptCurrentAsBaseline() {
if (state.runningAction || !state.workspace?.activeTrial) return
if (!window.confirm("Keep the currently applied tuning values and end this trial? This does not restore the previous tune.")) return
state.runningAction = true
try {
const response = await fetch("/api/ftm/trials/accept", { method: "POST" })
const payload = await response.json()
if (!response.ok) throw new Error(payload.error || "Failed to keep the current tune.")
state.error = ""
state.workspace = payload.workspace || state.workspace
showSnackbar(payload.message || "Current tune kept as the new baseline.")
} catch (error) {
state.error = error?.message || "Failed to keep the current tune."
showSnackbar(state.error, "error")
} finally {
state.runningAction = false
}
}
function setDimensionFeedback(dimensionId, mode) {
const accepted = new Set(state.feedbackAccepted)
const ignored = new Set(state.feedbackIgnored)
@@ -785,7 +807,7 @@ function renderProfile(profile) {
</div>
<button
class="longManeuverButton"
disabled="${() => state.runningAction || false}"
disabled="${() => state.runningAction || state.workspace?.activeTrial?.rollbackAvailable === false}"
@click="${() => applyProfile(profile.id)}">
Apply Trial
</button>
@@ -926,10 +948,18 @@ export function Tuning() {
</button>
<button
class="longManeuverButton"
disabled="${() => state.runningAction || !state.workspace?.activeTrial}"
disabled="${() => state.runningAction || !state.workspace?.activeTrial || state.workspace.activeTrial.rollbackAvailable === false}"
@click="${revertProfile}">
Revert Trial
</button>
${() => state.workspace?.activeTrial?.rollbackAvailable === false ? html`
<button
class="longManeuverButton"
disabled="${() => state.runningAction}"
@click="${acceptCurrentAsBaseline}">
Keep Current as Baseline
</button>
` : ""}
<button
class="longManeuverButton"
disabled="${() => state.runningAction}"
@@ -963,6 +993,12 @@ export function Tuning() {
<p class="longManeuverError">FTM analysis is offroad-only. Stop the car and go offroad before starting a run.</p>
` : ""}
${() => state.workspace?.activeTrial?.rollbackAvailable === false ? html`
<p class="longManeuverError">
The original rollback data is unavailable. Keep the current tune as the new baseline before applying another trial.
</p>
` : ""}
${() => state.status?.currentSegment ? html`
<div class="longManeuverCurrent">
<p><strong>Current Segment:</strong> ${state.status.currentSegment}</p>
+124 -14
View File
@@ -70,6 +70,7 @@ FTM_ADVANCED_LATERAL_PARAM_KEYS = {
"SteerLatAccel",
"SteerRatio",
}
FTM_TRIAL_BASELINE_PARAM = "FTMTrialBaseline"
GENERIC_PARAM_METADATA = {
"SteerDelay": {"min": 0.01, "max": 1.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
@@ -2052,13 +2053,23 @@ def list_workspace() -> dict[str, Any]:
params = Params(return_defaults=True)
current_profile_id = params.get("FTMActiveProfileId", encoding="utf-8") or ""
raw_active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
if not raw_active_snapshot and params.get_bool("FTMTrialApplied"):
raw_active_snapshot = _find_revert_snapshot(paths, {}, current_profile_id) or {}
if raw_active_snapshot:
if params.get_bool("FTMTrialApplied"):
active_payload = raw_active_snapshot if isinstance(raw_active_snapshot, dict) else {}
baseline_snapshot = _find_revert_snapshot(paths, raw_active_snapshot, current_profile_id, params)
if baseline_snapshot is not None:
raw_active_snapshot = {
**raw_active_snapshot,
"profileId": current_profile_id or raw_active_snapshot.get("profileId", ""),
**active_payload,
"params": baseline_snapshot["params"],
"profileId": current_profile_id or active_payload.get("profileId", ""),
"recoveryNeeded": baseline_snapshot is not raw_active_snapshot,
"rollbackAvailable": True,
}
else:
raw_active_snapshot = {
**active_payload,
"profileId": current_profile_id or active_payload.get("profileId", ""),
"recoveryNeeded": True,
"rollbackAvailable": False,
}
active_snapshot = _active_trial_display_state(paths, raw_active_snapshot)
return {
@@ -2071,6 +2082,9 @@ def list_workspace() -> dict[str, Any]:
def delete_report(report_id: str) -> dict[str, Any]:
paths = ensure_ftm_workspace()
params = Params(return_defaults=True)
if params.get_bool("FTMTrialApplied"):
raise RuntimeError("Revert or keep the active FTM trial before deleting tuning reports.")
active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
if isinstance(active_snapshot, dict) and active_snapshot.get("reportId") == report_id:
raise RuntimeError("Revert the active FTM trial before deleting its source report.")
@@ -2114,7 +2128,7 @@ def clear_workspace() -> dict[str, Any]:
params = Params(return_defaults=True)
active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
if params.get_bool("FTMTrialApplied") or (isinstance(active_snapshot, dict) and active_snapshot.get("params")):
raise RuntimeError("Revert the active FTM trial before clearing the workspace.")
raise RuntimeError("Revert or keep the active FTM trial before clearing the workspace.")
removed = []
for key in ("reports", "profiles", "feedback", "snapshots"):
@@ -2123,6 +2137,7 @@ def clear_workspace() -> dict[str, Any]:
path.unlink()
removed.append(str(path))
_clear_persistent_trial_baseline(params)
_clear_ftm_status()
return {
@@ -2146,6 +2161,73 @@ def _snapshot_current_trial_state(params: Params) -> dict[str, Any]:
return snapshot
def _read_persistent_trial_baseline(params: Params) -> dict[str, Any] | None:
raw = params.get(FTM_TRIAL_BASELINE_PARAM, encoding="utf-8") or {}
if isinstance(raw, str):
try:
raw = json.loads(raw)
except (TypeError, ValueError):
return None
if not isinstance(raw, dict) or not isinstance(raw.get("params"), dict):
return None
if raw["params"].get("FTMTrialApplied", False):
return None
return raw
def _persist_trial_baseline(params: Params, snapshot: dict[str, Any]) -> None:
if isinstance(snapshot.get("params"), dict) and not snapshot["params"].get("FTMTrialApplied", False):
params.put(FTM_TRIAL_BASELINE_PARAM, snapshot)
def _clear_persistent_trial_baseline(params: Params) -> None:
params.remove(FTM_TRIAL_BASELINE_PARAM)
def _profile_report_id(profile_id: str) -> str:
return str(profile_id or "").split(":", 1)[0]
def _recover_report_baseline(paths: dict[str, Path], profile_id: str,
visited_profiles: set[str] | None = None) -> dict[str, Any] | None:
profile_id = str(profile_id or "")
if not profile_id:
return None
visited_profiles = set(visited_profiles or set())
if profile_id in visited_profiles:
return None
visited_profiles.add(profile_id)
report_id = _profile_report_id(profile_id)
report = _read_json(paths["reports"] / f"{report_id}.json", {})
report_params = report.get("currentParams") if isinstance(report, dict) else None
if not isinstance(report_params, dict) or not report_params:
return None
baseline_params = {
key: value for key, value in report_params.items()
if key in TRIAL_PARAM_SPECS
}
if not baseline_params:
return None
if not baseline_params.get("FTMTrialApplied", False):
baseline_params["FTMTrialApplied"] = False
baseline_params.setdefault("FTMActiveProfileId", "")
return {
"reportId": report_id,
"profileId": profile_id,
"capturedAt": float(report.get("createdAt", 0.0) or 0.0),
"params": baseline_params,
"recoverySource": "report",
}
previous_profile_id = str(baseline_params.get("FTMActiveProfileId", "") or "")
if previous_profile_id and previous_profile_id != profile_id:
return _recover_report_baseline(paths, previous_profile_id, visited_profiles)
return None
def _apply_param_bundle(params: Params, bundle: dict[str, Any]) -> None:
for key, value in bundle.items():
kind = TRIAL_PARAM_SPECS.get(key)
@@ -2177,11 +2259,16 @@ def _merge_ftm_override_state(base: dict[str, Any], delta: dict[str, Any]) -> di
def _find_revert_snapshot(paths: dict[str, Path], active_snapshot: dict[str, Any],
current_profile_id: str = "") -> dict[str, Any] | None:
current_profile_id: str = "", params: Params | None = None) -> dict[str, Any] | None:
if isinstance(active_snapshot, dict) and isinstance(active_snapshot.get("params"), dict):
if not active_snapshot["params"].get("FTMTrialApplied", False):
return active_snapshot
if params is not None:
persistent_baseline = _read_persistent_trial_baseline(params)
if persistent_baseline is not None:
return persistent_baseline
cutoff = float(active_snapshot.get("capturedAt", math.inf) or math.inf) if isinstance(active_snapshot, dict) else math.inf
candidates = []
for path in paths["snapshots"].glob("*.json"):
@@ -2196,11 +2283,12 @@ def _find_revert_snapshot(paths: dict[str, Path], active_snapshot: dict[str, Any
continue
candidates.append(candidate)
if not candidates:
return None
matching = [candidate for candidate in candidates if current_profile_id and candidate.get("profileId") == current_profile_id]
pool = matching or candidates
return max(pool, key=lambda candidate: float(candidate.get("capturedAt", 0.0) or 0.0))
if candidates:
matching = [candidate for candidate in candidates if current_profile_id and candidate.get("profileId") == current_profile_id]
pool = matching or candidates
return max(pool, key=lambda candidate: float(candidate.get("capturedAt", 0.0) or 0.0))
return _recover_report_baseline(paths, current_profile_id)
def apply_trial_profile(report_id: str, profile_id: str) -> dict[str, Any]:
@@ -2225,9 +2313,10 @@ def apply_trial_profile(report_id: str, profile_id: str) -> dict[str, Any]:
paths,
raw_active_snapshot,
str(current_state.get("FTMActiveProfileId", "") or ""),
params,
)
if baseline_snapshot is None:
raise RuntimeError("The active FTM trial is missing its original rollback snapshot. Revert or reset the existing trial before applying another profile.")
raise RuntimeError("The active FTM trial has no recoverable rollback baseline. Keep the current tune as the new baseline before applying another profile.")
baseline_params = baseline_snapshot["params"]
session_started_at = float(baseline_snapshot.get("sessionStartedAt", baseline_snapshot.get("capturedAt", time.time())) or time.time())
else:
@@ -2283,6 +2372,7 @@ def apply_trial_profile(report_id: str, profile_id: str) -> dict[str, Any]:
}
_write_json(paths["snapshots"] / "active.json", snapshot)
_write_json(paths["snapshots"] / f"{report_id}-{profile_id.replace(':', '_')}.json", snapshot)
_persist_trial_baseline(params, snapshot)
bundle = generic_params
bundle["FTMActiveProfileId"] = profile_id
@@ -2304,10 +2394,11 @@ def revert_trial_profile() -> dict[str, Any]:
snapshot = _read_json(snapshot_path, {})
params = Params(return_defaults=True)
current_profile_id = params.get("FTMActiveProfileId", encoding="utf-8") or ""
revert_snapshot = _find_revert_snapshot(paths, snapshot if isinstance(snapshot, dict) else {}, current_profile_id)
revert_snapshot = _find_revert_snapshot(paths, snapshot if isinstance(snapshot, dict) else {}, current_profile_id, params)
if revert_snapshot is None:
raise FileNotFoundError("active trial snapshot")
_apply_param_bundle(params, revert_snapshot["params"])
_clear_persistent_trial_baseline(params)
try:
snapshot_path.unlink()
except FileNotFoundError:
@@ -2322,6 +2413,25 @@ def revert_trial_profile() -> dict[str, Any]:
}
def accept_trial_as_baseline() -> dict[str, Any]:
paths = ensure_ftm_workspace()
params = Params(return_defaults=True)
active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
if not params.get_bool("FTMTrialApplied") and not (isinstance(active_snapshot, dict) and active_snapshot):
raise FileNotFoundError("active trial")
params.put_bool("FTMTrialApplied", False)
params.put("FTMActiveProfileId", "")
_clear_persistent_trial_baseline(params)
for path in paths["snapshots"].glob("*.json"):
path.unlink()
return {
"message": "Kept the current tuning values and made them the new FTM baseline.",
"workspace": list_workspace(),
}
def record_feedback(report_id: str, feedback: dict[str, Any]) -> dict[str, Any]:
paths = ensure_ftm_workspace()
normalized = {
@@ -56,6 +56,9 @@ def _install_ftm_import_stubs(tmp_path):
def put_float(self, key, value):
self._store[key] = float(value)
def remove(self, key):
self._store.pop(key, None)
FakeParams._store = {}
class FakeHyundaiFlags:
@@ -642,6 +645,7 @@ def test_apply_and_revert_trial_profile_round_trip(tmp_path):
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.9)
assert fake_params_cls._store["FTMActiveProfileId"] == profile_id
assert fake_params_cls._store["FTMTrialApplied"] is True
assert fake_params_cls._store["FTMTrialBaseline"]["params"]["SteerLatAccel"] == pytest.approx(1.5)
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55)
@@ -650,6 +654,7 @@ def test_apply_and_revert_trial_profile_round_trip(tmp_path):
assert fake_params_cls._store["AdvancedLateralTune"] is False
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
assert fake_params_cls._store["FTMTrialApplied"] is False
assert "FTMTrialBaseline" not in fake_params_cls._store
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55)
@@ -747,6 +752,94 @@ def test_orphaned_previous_revision_can_recover_its_baseline(tmp_path):
assert fake_params_cls._store["FTMTrialApplied"] is False
def test_persistent_baseline_recovers_when_snapshot_files_are_missing(tmp_path):
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_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},
"ftmOverrides": {},
}
(workspace["profiles"] / f"{report_id}.json").write_text(json.dumps([profile]), encoding="utf-8")
fake_params_cls._store = {
"AdvancedLateralTune": False,
"SteerLatAccel": 1.5,
"FTMActiveProfileId": "",
"FTMActiveOverrides": {},
"FTMTrialApplied": 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["FTMTrialApplied"] is False
def test_legacy_orphan_recovers_baseline_from_source_report(tmp_path):
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_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,
"FTMActiveProfileId": "",
"FTMActiveOverrides": {},
"FTMTrialApplied": False,
},
}), encoding="utf-8")
fake_params_cls._store = {
"AdvancedLateralTune": True,
"SteerLatAccel": 1.9,
"FTMActiveProfileId": profile_id,
"FTMActiveOverrides": {},
"FTMTrialApplied": 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["FTMTrialApplied"] is False
def test_irrecoverable_trial_can_keep_current_values_as_new_baseline(tmp_path):
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
module.ensure_ftm_workspace()
fake_params_cls._store = {
"AdvancedLateralTune": True,
"SteerLatAccel": 1.9,
"FTMActiveProfileId": "missing-report:baseline_fix:assertive",
"FTMActiveOverrides": {"vehicleKnobs": {"generic.turn_in_boost_left": 0.1}},
"FTMTrialApplied": 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["FTMActiveOverrides"]["vehicleKnobs"]["generic.turn_in_boost_left"] == pytest.approx(0.1)
assert fake_params_cls._store["FTMActiveProfileId"] == ""
assert fake_params_cls._store["FTMTrialApplied"] is False
def test_workspace_hydrates_display_metadata_for_existing_active_trial(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_workspace()
@@ -800,6 +893,19 @@ def test_delete_report_removes_saved_artifacts(tmp_path):
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_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_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 = {"FTMTrialApplied": 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_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_workspace()
@@ -5790,6 +5790,15 @@ def setup(app):
return jsonify(result), 200
@app.route("/api/ftm/trials/accept", methods=["POST"])
def accept_ftm_trial():
try:
result = ftm_workspace.accept_trial_as_baseline()
except FileNotFoundError:
return jsonify({"error": "No active FTM trial was found."}), 404
return jsonify(result), 200
@app.route("/api/ftm/feedback", methods=["POST"])
def save_ftm_feedback():
data = request.get_json(silent=True) or {}
@@ -32,6 +32,7 @@ KNOWN_READ_ONLY = {
"ClusterOffset", "Compass", "DeveloperSidebarMetric1", "DeveloperSidebarMetric2",
"DeveloperSidebarMetric3", "DeveloperSidebarMetric4", "DeveloperSidebarMetric5",
"DeveloperSidebarMetric6", "DeveloperSidebarMetric7", "DongleId",
"FTMActiveOverrides", "FTMActiveProfileId", "FTMTrialApplied", "FTMTrialBaseline",
"StarPilotCarParamsPersistent", "StarPilotDrives", "StarPilotKilometers",
"StarPilotMinutes", "GitBranch", "GitCommit", "GitCommitDate", "GitDiff",
"GitRemote", "GithubSshKeys", "GithubUsername", "HardwareSerial", "IMEI",