feat(practice): flag suspiciously short laps and remove any lap in one click

Practice laps under 85% of the driver's own median (a likely shortcut)
are flagged: counted, but never a best lap, record or training stat
until approved. Timing and Race Day get a one-click Approve/Remove
panel, overlays show a warning mark, and any lap can be removed or
restored from Timing and Judging with undo.

Also sends judge corrections straight to the server so its hot-results
protection can no longer hold back a correction made right after a
passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-10-07 19:53:23 +02:00
parent 3cc4fa0845
commit 8601954ea2
24 changed files with 556 additions and 60 deletions

View File

@@ -157,6 +157,33 @@ test("personal best is per session and track layout - not per stint, and an old
assert.equal(harness.state.resultsBySession.session_1.passings.at(-1).trackLayoutId, "layout_b", "each lap remembers its layout");
});
test("practice: a shortcut lap is flagged suspect - counted, but no best lap, personal best or record", () => {
const harness = createHarness({ sessionType: "open_practice" });
const entry = () => Object.values(harness.state.resultsBySession.session_1.competitors)[0];
let ts = 1_000;
[20_000, 20_400, 19_800, 20_200, 20_600, 20_100].forEach((lapMs) => { ts += lapMs; harness.passing(ts, "main"); });
assert.equal(entry().bestLapMs, 19_800);
const pbsBefore = harness.audioEvents.filter((event) => event === "personal_best").length;
ts += 15_000; // usually ~20 s, then one lap of 15 s: cut across the track
harness.passing(ts, "main");
const last = harness.state.resultsBySession.session_1.passings.at(-1);
assert.equal(last.suspect, true);
assert.equal(entry().laps, 7, "still counted as a lap");
assert.equal(entry().bestLapMs, 19_800, "not a best lap");
assert.equal(harness.audioEvents.filter((event) => event === "personal_best").length, pbsBefore, "no personal best call for it");
ts += 15_100; // a second shortcut in a row is flagged too (suspect laps aren't the reference)
harness.passing(ts, "main");
assert.equal(harness.state.resultsBySession.session_1.passings.at(-1).suspect, true);
assert.ok(harness.overlayEvents.some((event) => event.type === "suspect"), "the overlay event feed shows it");
});
test("races never flag suspect laps - that's what the minimum lap time is for", () => {
const harness = createHarness({ sessionType: "heat" });
let ts = 1_000;
[20_000, 20_400, 19_800, 20_200, 20_600, 12_000].forEach((lapMs) => { ts += lapMs; harness.passing(ts, "main"); });
assert.equal(harness.state.resultsBySession.session_1.passings.some((passing) => passing.suspect), false);
});
test("pit entry and exit create one timed stop without adding laps", () => {
const harness = createHarness();
harness.passing(17_000, "main");