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>
86 lines
5.9 KiB
JavaScript
86 lines
5.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { startFakeDecoder } from "./browser_fake_decoder.js";
|
|
|
|
// Open Practice: a driver who usually laps around 20 s suddenly does one lap of 14 s (cut
|
|
// across the track). A real decoder passing must flag it: the "suspiciously short laps" panel
|
|
// on Timing, a ⚠ next to the driver in the leaderboard and the overlay, not used as the best
|
|
// lap - and one click on Remove takes it away.
|
|
export async function runSuspectLapsSmoke({ evaluate, navigate, savedState, waitFor, base, dialogs }) {
|
|
const patchState = async (patch) => {
|
|
const current = await savedState();
|
|
Object.assign(current, patch);
|
|
const response = await fetch(base + "/api/state", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(current) });
|
|
assert.equal(response.ok, true);
|
|
};
|
|
const results = async () => (await savedState()).resultsBySession?.sl_practice;
|
|
|
|
const dialogsAtStart = dialogs.length;
|
|
const decoder = await startFakeDecoder();
|
|
const seeded = await savedState();
|
|
const event = { id: "sl_event", name: "SL Practice", classId: "sl_class", mode: "race", raceConfig: { practiceOnly: true, minLapMs: 3000, maxLapMs: 60000 } };
|
|
// Six normal laps around 20 s; the decoder then reports the next crossing 14 s later.
|
|
const lastTs = Date.now() - 1000;
|
|
const laps = [20400, 19800, 20200, 20600, 20100, 20300];
|
|
let ts = lastTs - laps.reduce((sum, value) => sum + value, 0);
|
|
const startedAt = ts;
|
|
const passings = laps.map((lapMs) => { ts += lapMs; return { driverId: "sl_driver", driverName: "SL Kid", displayName: "SL Kid", competitorKey: "driver_sl_driver", transponder: "8330001", lapMs, timestamp: ts, validLap: true }; });
|
|
const practice = { id: "sl_practice", eventId: event.id, name: "SL Open Practice", type: "open_practice", durationMin: 600, status: "running", startedAt };
|
|
await patchState({
|
|
classes: [...seeded.classes, { id: "sl_class", name: "SL Class" }],
|
|
drivers: [...seeded.drivers, { id: "sl_driver", name: "SL Kid", classId: "sl_class", transponder: "8330001" }],
|
|
events: [...seeded.events, event],
|
|
sessions: [...seeded.sessions.map((session) => (["running", "paused"].includes(session.status) ? { ...session, status: "finished", endedAt: Date.now() } : session)), practice],
|
|
resultsBySession: {
|
|
...seeded.resultsBySession,
|
|
[practice.id]: { passings, competitors: { driver_sl_driver: { key: "driver_sl_driver", driverId: "sl_driver", driverName: "SL Kid", displayName: "SL Kid", transponder: "8330001", laps: 6, lastLapMs: 20300, bestLapMs: 19800, startTimestamp: startedAt, lastTimestamp: lastTs } } },
|
|
},
|
|
activeSessionId: practice.id,
|
|
settings: { ...seeded.settings, autoReconnect: true, wsUrl: `ws://127.0.0.1:${decoder.port}` },
|
|
});
|
|
|
|
try {
|
|
await navigate("timing", "resetSession");
|
|
await waitFor(async () => evaluate('document.getElementById("connectionBadge")?.classList.contains("badge-online")'), "decoder connected");
|
|
decoder.sendPassing("8330001", lastTs + 14000);
|
|
await waitFor(async () => (await results())?.passings?.length === 7, "the short lap arrived");
|
|
const shortLap = (await results()).passings.at(-1);
|
|
assert.equal(shortLap.lapMs, 14000);
|
|
assert.equal(shortLap.suspect, true, "flagged as suspect");
|
|
assert.equal((await results()).competitors.driver_sl_driver.bestLapMs, 19800, "not used as the best lap");
|
|
|
|
await waitFor(async () => Boolean(await evaluate(`document.getElementById("suspectLapsPanel")`)), "suspect panel on Timing");
|
|
assert.match(await evaluate(`document.getElementById("suspectLapsPanel").innerText`), /SL Kid/);
|
|
assert.equal(await evaluate(`Boolean([...document.querySelectorAll(".table-primary")].find((node) => node.textContent.includes("SL Kid"))?.querySelector(".suspect-flag"))`), true, "⚠ in the leaderboard");
|
|
console.log("PASS a lap far quicker than the driver's own pace is flagged on Timing and not used as the best lap");
|
|
|
|
await navigate("overlay", "view");
|
|
await waitFor(async () => evaluate(`document.body.innerText.includes("SL Kid")`), "overlay shows the driver");
|
|
assert.equal(await evaluate(`[...document.querySelectorAll(".suspect-flag")].length > 0`), true, "⚠ in the overlay");
|
|
console.log("PASS the overlay marks the driver with a suspect lap");
|
|
|
|
await navigate("race_day", "raceDayStart");
|
|
await waitFor(async () => Boolean(await evaluate(`document.querySelector('#suspectLapsPanel [data-lap-action="approve"]')`)), "the same panel on Race Day");
|
|
console.log("PASS Race Day shows the suspect lap with Approve / Remove");
|
|
|
|
await navigate("timing", "resetSession");
|
|
await waitFor(async () => Boolean(await evaluate(`document.querySelector('#suspectLapsPanel [data-lap-action="remove"]')`)), "Remove button");
|
|
await evaluate(`document.querySelector('#suspectLapsPanel [data-lap-action="remove"]').click()`);
|
|
await waitFor(async () => (await results())?.passings?.at(-1)?.validLap === false, "the short lap removed");
|
|
const after = await results();
|
|
assert.equal(after.passings.at(-1).invalidReason, "manual_invalid");
|
|
assert.equal(after.competitors.driver_sl_driver.laps, 6, "no longer counted");
|
|
assert.ok(after.adjustments?.some((adjustment) => adjustment.category === "invalid"), "logged as a judge correction (undoable)");
|
|
await waitFor(async () => !(await evaluate(`Boolean(document.getElementById("suspectLapsPanel"))`)), "panel gone once nothing is pending");
|
|
console.log("PASS one click removes the suspect lap and it is logged as an undoable correction");
|
|
} finally {
|
|
dialogs.splice(dialogsAtStart);
|
|
const current = await savedState();
|
|
await patchState({
|
|
settings: { ...current.settings, autoReconnect: false },
|
|
sessions: current.sessions.map((session) => (session.id === practice.id ? { ...session, status: "finished", endedAt: Date.now() } : session)),
|
|
activeSessionId: "",
|
|
});
|
|
await decoder.close();
|
|
}
|
|
}
|