import test from "node:test"; import assert from "node:assert/strict"; import { applyCompetitorCorrection, findPassingByUndoMarker, getJudgeFilteredLog, getJudgeFilteredRows, getManualCorrectionSummary, invalidateCompetitorLastLap, recalculateCompetitorFromPassings, 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("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"]); });