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>
234 lines
10 KiB
JavaScript
234 lines
10 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
applyCompetitorCorrection,
|
|
findPassingByUndoMarker,
|
|
getJudgeFilteredLog,
|
|
getJudgeFilteredRows,
|
|
getManualCorrectionSummary,
|
|
invalidateCompetitorLastLap,
|
|
recalculateCompetitorFromPassings,
|
|
setCompetitorLapStatus,
|
|
restoreCompetitorLastInvalidLap,
|
|
undoJudgingAdjustment,
|
|
} from "../src/judging_logic.js";
|
|
|
|
function createHarness() {
|
|
const session = { id: "session_1", startedAt: 1000 };
|
|
const result = {
|
|
passings: [
|
|
{ competitorKey: "driver_1", timestamp: 17000, lapMs: 16000, validLap: true },
|
|
{ competitorKey: "driver_1", timestamp: 34000, lapMs: 17000, validLap: true },
|
|
{ competitorKey: "driver_1", timestamp: 50500, lapMs: 16500, validLap: true },
|
|
],
|
|
competitors: {
|
|
driver_1: {
|
|
key: "driver_1",
|
|
driverName: "Driver One",
|
|
startTimestamp: 1000,
|
|
laps: 3,
|
|
lastLapMs: 16500,
|
|
lastTimestamp: 50500,
|
|
bestLapMs: 16000,
|
|
manualLapAdjustment: 0,
|
|
manualTimeAdjustmentMs: 0,
|
|
},
|
|
},
|
|
adjustments: [],
|
|
};
|
|
let id = 0;
|
|
let saveCount = 0;
|
|
const ensureSessionResult = () => result;
|
|
const isCountedPassing = (passing) => passing.validLap !== false;
|
|
const getCompetitorPassings = (_session, row, options = {}) => result.passings
|
|
.filter((passing) => passing.competitorKey === row.key)
|
|
.filter((passing) => options.includeInvalid || isCountedPassing(passing))
|
|
.sort((left, right) => left.timestamp - right.timestamp);
|
|
const recalculate = (targetSession, rowKey) => recalculateCompetitorFromPassings(targetSession, rowKey, {
|
|
ensureSessionResult,
|
|
getCompetitorPassings,
|
|
isCountedPassing,
|
|
});
|
|
const findPassing = (targetSession, rowKey, timestamp) => findPassingByUndoMarker(targetSession, rowKey, timestamp, {
|
|
ensureSessionResult,
|
|
});
|
|
const deps = {
|
|
ensureSessionResult,
|
|
getCompetitorPassings,
|
|
isCountedPassing,
|
|
recalculateCompetitorFromPassings: recalculate,
|
|
findPassingByUndoMarker: findPassing,
|
|
uid: (prefix) => `${prefix}_${++id}`,
|
|
t: (key) => key,
|
|
formatLap: (lapMs) => `${lapMs}ms`,
|
|
saveState: () => { saveCount += 1; },
|
|
};
|
|
return {
|
|
session,
|
|
result,
|
|
row: result.competitors.driver_1,
|
|
deps,
|
|
getSaveCount: () => saveCount,
|
|
};
|
|
}
|
|
|
|
test("recalculating after a judge correction keeps the best lap on the current track layout", () => {
|
|
const harness = createHarness();
|
|
harness.session.trackLayoutId = "layout_b";
|
|
harness.result.passings[0].trackLayoutId = "layout_a"; // the 16.0 s lap was on the old layout
|
|
harness.result.passings[1].trackLayoutId = "layout_b";
|
|
harness.result.passings[2].trackLayoutId = "layout_b";
|
|
harness.deps.recalculateCompetitorFromPassings(harness.session, "driver_1");
|
|
assert.equal(harness.row.bestLapMs, 16500);
|
|
assert.equal(harness.row.laps, 3, "laps from the old layout still count as laps");
|
|
delete harness.result.passings[0].trackLayoutId; // untagged (from before layouts were tagged) still counts
|
|
harness.deps.recalculateCompetitorFromPassings(harness.session, "driver_1");
|
|
assert.equal(harness.row.bestLapMs, 16000);
|
|
});
|
|
|
|
test("any lap can be removed, restored or (if suspect) approved - each logged and undoable", () => {
|
|
const harness = createHarness();
|
|
const act = (timestamp, action) => setCompetitorLapStatus(harness.session, "driver_1", timestamp, action, harness.deps);
|
|
// Remove the FIRST lap, not just the last one.
|
|
assert.equal(act(17000, "remove"), true);
|
|
assert.equal(harness.row.laps, 2);
|
|
assert.equal(harness.row.bestLapMs, 16500);
|
|
assert.equal(act(17000, "remove"), false, "already removed");
|
|
assert.equal(act(17000, "restore"), true);
|
|
assert.equal(harness.row.laps, 3);
|
|
assert.equal(harness.row.bestLapMs, 16000);
|
|
|
|
// A suspect lap doesn't count for best lap until approved; undo makes it suspect again.
|
|
harness.result.passings[2].lapMs = 12000;
|
|
harness.result.passings[2].suspect = true;
|
|
harness.deps.recalculateCompetitorFromPassings(harness.session, "driver_1");
|
|
assert.equal(harness.row.bestLapMs, 16000);
|
|
assert.equal(act(50500, "approve"), true);
|
|
assert.equal(harness.row.bestLapMs, 12000);
|
|
const approval = harness.result.adjustments.at(-1);
|
|
assert.equal(approval.category, "invalid");
|
|
assert.equal(undoJudgingAdjustment(harness.session, approval.id, { ...harness.deps, findPassingByUndoMarker: (session, key, ts) => harness.result.passings.find((passing) => passing.competitorKey === key && passing.timestamp === ts) }), true);
|
|
assert.equal(harness.result.passings[2].suspect, true);
|
|
assert.equal(harness.row.bestLapMs, 16000);
|
|
assert.equal(act(99999, "remove"), false, "unknown lap");
|
|
assert.equal(act(17000, "explode"), false, "unknown action");
|
|
});
|
|
|
|
test("manual corrections accumulate, reset, and undo to the prior state", () => {
|
|
const harness = createHarness();
|
|
|
|
applyCompetitorCorrection(
|
|
harness.session,
|
|
harness.row,
|
|
{ lapDelta: -1, timeMsDelta: 5000 },
|
|
harness.deps
|
|
);
|
|
assert.equal(harness.row.manualLapAdjustment, -1);
|
|
assert.equal(harness.row.manualTimeAdjustmentMs, 5000);
|
|
assert.equal(getManualCorrectionSummary(harness.row, harness.deps), "-1L • +5000ms");
|
|
assert.equal(harness.result.adjustments[0].detail, "-1L • +5000ms");
|
|
|
|
applyCompetitorCorrection(harness.session, harness.row, { reset: true }, harness.deps);
|
|
assert.equal(harness.row.manualLapAdjustment, 0);
|
|
assert.equal(harness.row.manualTimeAdjustmentMs, 0);
|
|
|
|
const resetAdjustment = harness.result.adjustments[1];
|
|
assert.equal(undoJudgingAdjustment(harness.session, resetAdjustment.id, harness.deps), true);
|
|
assert.equal(harness.row.manualLapAdjustment, -1);
|
|
assert.equal(harness.row.manualTimeAdjustmentMs, 5000);
|
|
assert.equal(resetAdjustment.undoneAt > 0, true);
|
|
assert.equal(harness.result.adjustments.at(-1).category, "undo");
|
|
assert.equal(harness.getSaveCount(), 3);
|
|
});
|
|
|
|
test("undoing two time penalties in either order always removes exactly their own contribution", () => {
|
|
const forward = createHarness();
|
|
applyCompetitorCorrection(forward.session, forward.row, { timeMsDelta: 5000 }, forward.deps);
|
|
applyCompetitorCorrection(forward.session, forward.row, { timeMsDelta: 5000 }, forward.deps);
|
|
assert.equal(forward.row.manualTimeAdjustmentMs, 10000, "both +5s penalties applied");
|
|
const [first, second] = forward.result.adjustments;
|
|
|
|
// Undo the FIRST one first.
|
|
assert.equal(undoJudgingAdjustment(forward.session, first.id, forward.deps), true);
|
|
assert.equal(forward.row.manualTimeAdjustmentMs, 5000, "only the first penalty's +5s is removed, the second's stays");
|
|
assert.equal(undoJudgingAdjustment(forward.session, second.id, forward.deps), true);
|
|
assert.equal(forward.row.manualTimeAdjustmentMs, 0, "both undone, in first-applied order, nets to zero");
|
|
|
|
const reverse = createHarness();
|
|
applyCompetitorCorrection(reverse.session, reverse.row, { timeMsDelta: 5000 }, reverse.deps);
|
|
applyCompetitorCorrection(reverse.session, reverse.row, { timeMsDelta: 5000 }, reverse.deps);
|
|
const [firstR, secondR] = reverse.result.adjustments;
|
|
|
|
// Undo the SECOND one first (out of order) - this is exactly the sequence the user
|
|
// reported: undoing an earlier penalty used to wipe out both, then undoing the other
|
|
// silently re-added one back, leaving a live +5s penalty behind two "undone" entries.
|
|
assert.equal(undoJudgingAdjustment(reverse.session, secondR.id, reverse.deps), true);
|
|
assert.equal(reverse.row.manualTimeAdjustmentMs, 5000, "only the second penalty's +5s is removed, the first's stays");
|
|
assert.equal(undoJudgingAdjustment(reverse.session, firstR.id, reverse.deps), true);
|
|
assert.equal(reverse.row.manualTimeAdjustmentMs, 0, "both undone, in reverse order, still nets to zero - order must not matter");
|
|
});
|
|
|
|
test("invalidating and restoring the latest lap recalculates the competitor", () => {
|
|
const harness = createHarness();
|
|
|
|
assert.equal(invalidateCompetitorLastLap(harness.session, harness.row, harness.deps), true);
|
|
assert.equal(harness.result.passings[2].validLap, false);
|
|
assert.equal(harness.result.passings[2].invalidReason, "manual_invalid");
|
|
assert.equal(harness.row.laps, 2);
|
|
assert.equal(harness.row.lastLapMs, 17000);
|
|
assert.equal(harness.row.lastTimestamp, 34000);
|
|
assert.equal(harness.row.bestLapMs, 16000);
|
|
|
|
assert.equal(restoreCompetitorLastInvalidLap(harness.session, harness.row, harness.deps), true);
|
|
assert.equal(harness.result.passings[2].validLap, true);
|
|
assert.equal(harness.row.laps, 3);
|
|
assert.equal(harness.row.lastLapMs, 16500);
|
|
assert.equal(harness.row.lastTimestamp, 50500);
|
|
assert.equal(harness.getSaveCount(), 2);
|
|
});
|
|
|
|
test("undo restores passing validity and cannot be applied twice", () => {
|
|
const harness = createHarness();
|
|
invalidateCompetitorLastLap(harness.session, harness.row, harness.deps);
|
|
const invalidation = harness.result.adjustments[0];
|
|
|
|
assert.equal(undoJudgingAdjustment(harness.session, invalidation.id, harness.deps), true);
|
|
assert.equal(harness.result.passings[2].validLap, true);
|
|
assert.equal(harness.result.passings[2].invalidReason, "");
|
|
assert.equal(harness.row.laps, 3);
|
|
assert.equal(undoJudgingAdjustment(harness.session, invalidation.id, harness.deps), false);
|
|
});
|
|
|
|
test("lap invalidation safely rejects competitors without counted laps", () => {
|
|
const harness = createHarness();
|
|
harness.result.passings.forEach((passing) => {
|
|
passing.validLap = false;
|
|
});
|
|
assert.equal(invalidateCompetitorLastLap(harness.session, harness.row, harness.deps), false);
|
|
assert.equal(harness.result.adjustments.length, 0);
|
|
assert.equal(harness.getSaveCount(), 0);
|
|
});
|
|
|
|
test("judging row and action-log filters select only matching entries", () => {
|
|
const rows = [
|
|
{ key: "invalid", invalidPending: true },
|
|
{ key: "corrected", manualLapAdjustment: -1 },
|
|
{ key: "team", teamId: "team_1" },
|
|
{ key: "plain" },
|
|
];
|
|
const log = [
|
|
{ id: "correction", category: "correction" },
|
|
{ id: "invalid", category: "invalid" },
|
|
{ id: "undo", category: "undo" },
|
|
];
|
|
|
|
assert.deepEqual(getJudgeFilteredRows(rows, "invalid").map((row) => row.key), ["invalid"]);
|
|
assert.deepEqual(getJudgeFilteredRows(rows, "corrected").map((row) => row.key), ["corrected"]);
|
|
assert.deepEqual(getJudgeFilteredRows(rows, "team").map((row) => row.key), ["team"]);
|
|
assert.equal(getJudgeFilteredRows(rows, "all").length, 4);
|
|
assert.deepEqual(getJudgeFilteredLog(log, "corrections").map((entry) => entry.id), ["correction"]);
|
|
assert.deepEqual(getJudgeFilteredLog(log, "invalid").map((entry) => entry.id), ["invalid"]);
|
|
assert.deepEqual(getJudgeFilteredLog(log, "undo").map((entry) => entry.id), ["undo"]);
|
|
});
|