Files
Live_RC/tests/race_timing_logic.test.js
larssand 94794350e5 Add a results archive and purge orphaned passing rows on delete
Until now there was no way to find a completed session without already
knowing which event it belonged to - navigation was strictly Events ->
one event -> its sessions. A new "Resultatarkiv" view lists every
finished session across all events, searchable by event/session name,
class, driver and date range.

Opening a session reuses the existing single-session leaderboard
builder for standings and the existing per-competitor lap lookup for
lap history - no new results logic - plus a session-wide penalties/
corrections list (resultsBySession[id].adjustments), previously only
visible in the live judging action log. Export buttons reuse the
existing per-event PDF and race-package JSON export functions rather
than adding a new format.

Investigated what should happen to raw timing data when a race is
deleted. Training statistics and session replay already read from
their own independent, deliberately-retained copies that survive
deletion by existing design - unaffected either way. The one thing
that outlived deletion with no purpose was the raw `passings` SQLite
rows (write-only; nothing in the app reads them), growing forever with
driver IDs never purged. A new POST /api/passings/purge endpoint now
removes exactly the deleted session(s)' rows, called as a best-effort
side call from both delete-event and delete-session.

Also fixes a real bug found while building this: ensureSessionResult
guarded adjustments/splits/pitEvents/pitStops against being missing on
an already-existing result, but not passings itself, so viewing lap
history for such a result (e.g. an older/imported race package)
crashed outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 15:49:10 +02:00

477 lines
18 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import {
applyBumpsForRace,
buildOpenPracticeBundle,
buildRaceFormatConfigFromForm,
buildRaceSessionsFromWizard,
generateFinalsForRace,
generateQualifyingForRace,
normalizeEvent,
reseedUpcomingQualifying,
} from "../src/event_race_logic.js";
import {
buildFinalStandings,
buildSessionChannelStats,
compareNumberSet,
formatLeaderboardGap,
getCompetitorSeedMetric,
getQualifyingPointsValue,
getSessionLapWindow,
getSessionTargetMs,
getSessionTiming,
isHighPointsTable,
isUntimedSession,
ensureSessionResult,
} from "../src/timing_logic.js";
function createUid() {
let index = 0;
return (prefix) => `${prefix}_${++index}`;
}
function createForm(values) {
const fields = new Map(Object.entries(values));
return { get: (name) => fields.get(name) ?? null };
}
const normalizeStartMode = (mode) => ["mass", "staggered", "position"].includes(mode) ? mode : "mass";
const chunkArray = (items, size) => {
const chunks = [];
for (let index = 0; index < items.length; index += size) {
chunks.push(items.slice(index, index + size));
}
return chunks;
};
test("race format form normalizes limits and preserves participants and teams", () => {
const event = {
raceConfig: {
driverIds: ["driver_1", "driver_2"],
participantsConfigured: true,
teams: [{ id: "team_1", name: "Red" }],
},
};
const config = buildRaceFormatConfigFromForm(
createForm({
presetId: "club_qualifying",
qualifyingScoring: "best",
qualifyingRounds: "0",
carsPerHeat: "1",
qualDurationMin: "0",
qualStartMode: "position",
qualSeedLapCount: "3",
qualSeedMethod: "invalid",
countedQualRounds: "2",
qualifyingPointsTable: "ifmar",
qualifyingTieBreak: "best_lap",
carsPerFinal: "1",
finalLegs: "3",
countedFinalLegs: "2",
finalDurationMin: "5",
finalStartMode: "staggered",
followUpSec: "15",
minLapSec: "11.25",
maxLapSec: "60.5",
bumpCount: "1",
reserveBumpSlots: "on",
finalsSource: "practice",
}),
event,
{ normalizeStartMode, getEventTeams: (item) => item.raceConfig.teams }
);
assert.equal(config.qualifyingRounds, 3);
assert.equal(config.carsPerHeat, 2);
assert.equal(config.qualDurationMin, 5);
assert.equal(config.qualSeedMethod, "best_sum");
assert.equal(config.carsPerFinal, 2);
assert.equal(config.minLapMs, 11250);
assert.equal(config.maxLapMs, 60500);
assert.equal(config.reserveBumpSlots, true);
assert.equal(config.finalsSource, "practice");
assert.deepEqual(config.driverIds, ["driver_1", "driver_2"]);
assert.deepEqual(config.teams, [{ id: "team_1", name: "Red" }]);
});
test("open practice bundle is untimed, unrestricted, and independent of race presets", () => {
const uid = createUid();
const bundle = buildOpenPracticeBundle(
{ name: "Friday Practice", date: "2026-08-24" },
{
uid,
normalizeEvent: (event) => ({ ...event, raceConfig: { presetId: "club_qualifying", driverIds: ["old"] } }),
buildRaceSession: (eventId, name, type, durationMin, overrides) => ({ eventId, name, type, durationMin, ...overrides }),
}
);
assert.equal(bundle.event.name, "Friday Practice");
assert.equal(bundle.event.classId, "");
assert.equal(bundle.event.mode, "race");
assert.equal(bundle.event.raceConfig.presetId, "custom");
assert.equal(bundle.event.raceConfig.practiceOnly, true);
assert.equal(bundle.event.raceConfig.participantsConfigured, false);
assert.deepEqual(bundle.event.raceConfig.driverIds, []);
assert.equal(bundle.session.eventId, bundle.event.id);
assert.equal(bundle.session.type, "open_practice");
assert.equal(bundle.session.startMode, "mass");
assert.equal(bundle.session.followUpSec, 0);
assert.deepEqual(bundle.session.driverIds, []);
assert.equal(isUntimedSession(bundle.session), true);
});
test("event normalization preserves the practice-only marker", () => {
const event = normalizeEvent(
{ id: "event_1", raceConfig: { practiceOnly: true } },
{
normalizeBrandingConfig: (branding) => branding || {},
normalizeStartMode,
normalizeRaceTeam: (team) => team,
}
);
assert.equal(event.raceConfig.practiceOnly, true);
assert.equal(event.raceConfig.presetId, "custom");
});
test("race wizard creates configured practice, qualifying, and endurance sessions", () => {
const event = {
id: "event_1",
raceConfig: {
driverIds: ["driver_1", "driver_2"],
qualDurationMin: 6,
qualStartMode: "staggered",
qualSeedLapCount: 3,
qualSeedMethod: "consecutive",
finalDurationMin: 240,
followUpSec: 20,
},
};
let sequence = 0;
const buildRaceSession = (eventId, name, type, durationMin, overrides) => ({
id: `session_${++sequence}`,
eventId,
name,
type,
durationMin,
...overrides,
});
const sessions = buildRaceSessionsFromWizard(
event,
{
createPractice: true,
practiceSessions: 2,
createQualifying: true,
qualifyingRounds: 3,
createTeamRace: true,
teamRaceDurationMin: 180,
},
{ t: (key) => key, buildRaceSession }
);
assert.equal(sessions.length, 6);
assert.deepEqual(sessions.map((session) => session.type), [
"practice",
"practice",
"qualification",
"qualification",
"qualification",
"team_race",
]);
assert.equal(sessions[2].durationMin, 6);
assert.equal(sessions[2].seedMethod, "consecutive");
assert.equal(sessions[2].followUpSec, 20);
assert.equal(sessions[5].durationMin, 180);
assert.deepEqual(sessions[5].driverIds, ["driver_1", "driver_2"]);
});
test("qualifying generation splits seeded drivers into every configured round", () => {
const state = { sessions: [] };
const event = {
id: "event_1",
raceConfig: {
qualifyingRounds: 2,
carsPerHeat: 3,
qualDurationMin: 7,
qualStartMode: "staggered",
qualSeedLapCount: 2,
qualSeedMethod: "average",
followUpSec: 12,
},
};
const standings = Array.from({ length: 7 }, (_, index) => ({
driverId: `driver_${index + 1}`,
rank: index + 1,
}));
let cleared = false;
const created = generateQualifyingForRace(event, {
buildPracticeStandings: () => standings,
getEventDrivers: () => [],
state,
normalizeStartMode,
normalizeSession: (session) => session,
uid: createUid(),
chunkArray,
clearGeneratedQualifying: () => { cleared = true; },
});
assert.equal(cleared, true);
assert.equal(created, 6);
assert.deepEqual(state.sessions.map((session) => session.name), ["Q1 H1", "Q2 H1", "Q1 H2", "Q2 H2", "Q1 H3", "Q2 H3"]);
assert.deepEqual(state.sessions[0].driverIds, ["driver_1", "driver_2", "driver_3"]);
assert.deepEqual(state.sessions[4].driverIds, ["driver_7"]);
assert.equal(state.sessions[0].durationMin, 7);
assert.equal(state.sessions[0].seedMethod, "average");
assert.equal(state.sessions[0].followUpSec, 12);
});
test("qualifying reseed updates ready auto grids but leaves manual grids locked", () => {
const sessions = [
{ name: "Q2 H1", type: "qualification", generated: true, status: "ready", driverIds: ["old_1"], manualGridIds: ["old_1"], gridCustomized: false },
{ name: "Q2 H2", type: "qualification", generated: true, status: "ready", driverIds: ["locked"], manualGridIds: ["locked"], gridCustomized: true },
{ name: "Q1 H1", type: "qualification", generated: true, status: "finished", driverIds: ["finished"] },
];
const standings = ["driver_3", "driver_1", "driver_2"].map((driverId, index) => ({ driverId, rank: index + 1 }));
const result = reseedUpcomingQualifying(
{ id: "event_1", raceConfig: { carsPerHeat: 2 } },
{
buildQualifyingStandings: () => standings,
buildPracticeStandings: () => [],
getEventDrivers: () => [],
getSessionsForEvent: () => sessions,
chunkArray,
}
);
assert.deepEqual(result, { updated: 1, locked: 1 });
assert.deepEqual(sessions[0].driverIds, ["driver_3", "driver_1"]);
assert.deepEqual(sessions[1].driverIds, ["locked"]);
assert.deepEqual(sessions[2].driverIds, ["finished"]);
});
test("final generation reserves bump slots and keeps seeded grid order", () => {
const state = { sessions: [] };
const sourceRows = Array.from({ length: 7 }, (_, index) => ({ driverId: `driver_${index + 1}` }));
const event = {
id: "event_1",
raceConfig: {
finalsSource: "qualifying",
finalLegs: 2,
carsPerFinal: 4,
finalDurationMin: 8,
finalStartMode: "position",
followUpSec: 15,
bumpCount: 1,
reserveBumpSlots: true,
},
};
const created = generateFinalsForRace(event, {
buildPracticeStandings: () => [],
buildQualifyingStandings: () => sourceRows,
clearGeneratedFinals: () => {},
state,
normalizeStartMode,
normalizeSession: (session) => session,
uid: createUid(),
chunkArray,
t: (key) => key,
});
assert.equal(created, 6);
assert.deepEqual(state.sessions.map((session) => session.name), [
"A session.final 1",
"A session.final 2",
"B session.final 1",
"B session.final 2",
"C session.final 1",
"C session.final 2",
]);
assert.deepEqual(state.sessions[0].driverIds, ["driver_1", "driver_2", "driver_3"]);
assert.equal(state.sessions[0].reservedBumpSlots, 1);
assert.equal(state.sessions[4].reservedBumpSlots, 0);
assert.deepEqual(state.sessions[0].manualGridIds, state.sessions[0].driverIds);
});
test("bump-up promotes lower-main winners into reserved upper-main slots", () => {
const sessions = [
{ name: "A final 1", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 1, driverIds: ["a1", "a2", "a3"], manualGridIds: ["a1", "a2", "a3"] },
{ name: "B final 1", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 1, driverIds: ["b1", "b2", "b3"], manualGridIds: ["b1", "b2", "b3"] },
{ name: "C final 1", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 0, driverIds: ["c1"] },
];
const standings = [
{ rank: "A1", driverId: "a1" },
{ rank: "B1", driverId: "b1" },
{ rank: "B2", driverId: "b2" },
{ rank: "C1", driverId: "c1" },
];
const applied = applyBumpsForRace(
{ id: "event_1", raceConfig: { bumpCount: 1, carsPerFinal: 4 } },
{ buildFinalStandings: () => standings, getSessionsForEvent: () => sessions }
);
assert.equal(applied, 2);
assert.deepEqual(sessions[0].driverIds, ["a1", "a2", "a3", "b1"]);
assert.deepEqual(sessions[1].driverIds, ["b1", "b2", "b3", "c1"]);
assert.equal(sessions[0].reservedBumpSlots, 0);
assert.equal(sessions[1].reservedBumpSlots, 0);
});
test("session timing handles untimed practice, countdown, and follow-up", () => {
const getTarget = (session) => getSessionTargetMs(session, { isUntimedSession });
assert.equal(getTarget({ type: "open_practice", durationMin: 5 }), null);
assert.equal(getTarget({ type: "final", durationMin: 5 }), 300000);
const timing = getSessionTiming(
{ type: "final", durationMin: 5, startedAt: 1000, followUpSec: 15, followUpStartedAt: 301000 },
306000,
{ getSessionTargetMs: getTarget }
);
assert.equal(timing.elapsedMs, 305000);
assert.equal(timing.remainingMs, 0);
assert.equal(timing.followUpActive, true);
assert.equal(timing.followUpRemainingMs, 10000);
const untimed = getSessionTiming({ type: "open_practice", startedAt: 1000 }, 61000, { getSessionTargetMs: getTarget });
assert.equal(untimed.untimed, true);
assert.equal(untimed.remainingMs, null);
assert.equal(untimed.elapsedMs, 60000);
});
test("race lap window and seed metrics enforce timing rules", () => {
const state = {
events: [
{ id: "race", mode: "race", raceConfig: { minLapMs: 11000, maxLapMs: 60000 } },
{ id: "track", mode: "track" },
{ id: "invalid", mode: "race", raceConfig: { minLapMs: 20000, maxLapMs: 15000 } },
],
};
assert.deepEqual(getSessionLapWindow({ eventId: "race" }, { state }), { minLapMs: 11000, maxLapMs: 60000 });
assert.deepEqual(getSessionLapWindow({ eventId: "invalid" }, { state }), { minLapMs: 20000, maxLapMs: 20000 });
assert.equal(getSessionLapWindow({ eventId: "track" }, { state }).maxLapMs, Number.POSITIVE_INFINITY);
const passings = [16000, 21000, 16500, 17000].map((lapMs) => ({ lapMs }));
const consecutive = getCompetitorSeedMetric(
{ seedBestLapCount: 2, seedMethod: "consecutive" },
{},
{ getCompetitorPassings: () => passings }
);
const best = getCompetitorSeedMetric(
{ seedBestLapCount: 2, seedMethod: "best_sum" },
{},
{ getCompetitorPassings: () => passings }
);
assert.deepEqual(consecutive.laps, [16500, 17000]);
assert.equal(consecutive.totalMs, 33500);
assert.deepEqual(best.laps, [16000, 16500]);
assert.equal(best.totalMs, 32500);
});
test("points tables, tie comparison, and leaderboard gaps remain deterministic", () => {
assert.equal(getQualifyingPointsValue(2, 8, "rank_low"), 2);
assert.equal(getQualifyingPointsValue(2, 8, "field_desc"), 7);
assert.equal(getQualifyingPointsValue(2, 8, "ifmar"), 9);
assert.equal(isHighPointsTable("rank_low"), false);
assert.equal(isHighPointsTable("ifmar"), true);
assert.equal(compareNumberSet([1, 3], [1, 4]), -1);
assert.equal(compareNumberSet([10, 8], [10, 7], true), -1);
const t = (key) => key;
const leader = { key: "leader", laps: 17, comparisonMs: 16000, totalElapsedMs: 301000, distanceToTargetMs: 1000 };
assert.equal(formatLeaderboardGap(leader, leader, { isFreePractice: true }, { t }), "status.free_practice");
assert.equal(formatLeaderboardGap({ key: "second", laps: 17, comparisonMs: 16550 }, leader, { isFreePractice: true }, { t }), "+0.550s");
assert.equal(formatLeaderboardGap({ key: "second", laps: 16, totalElapsedMs: 290000 }, leader, {}, { t }), "+1L");
assert.equal(
formatLeaderboardGap(
{ key: "second", laps: 17, totalElapsedMs: 306000, distanceToTargetMs: 6000 },
leader,
{ useTargetTieBreak: true },
{ t }
),
"+5.000s"
);
});
test("final standings count the configured best legs per main", () => {
const sessions = [
{ id: "a1", type: "final", name: "A final 1" },
{ id: "a2", type: "final", name: "A final 2" },
{ id: "a3", type: "final", name: "A final 3" },
{ id: "b1", type: "final", name: "B final 1" },
];
const rowsBySession = {
a1: [
{ driverId: "driver_1", driverName: "One", totalElapsedMs: 301000 },
{ driverId: "driver_2", driverName: "Two", totalElapsedMs: 302000 },
],
a2: [
{ driverId: "driver_2", driverName: "Two", totalElapsedMs: 300000 },
{ driverId: "driver_1", driverName: "One", totalElapsedMs: 303000 },
],
a3: [
{ driverId: "driver_1", driverName: "One", totalElapsedMs: 299000 },
{ driverId: "driver_2", driverName: "Two", totalElapsedMs: 304000 },
],
b1: [{ driverId: "driver_3", driverName: "Three", totalElapsedMs: 305000 }],
};
const standings = buildFinalStandings(
{ id: "event_1", raceConfig: { countedFinalLegs: 2 } },
{ getSessionsForEvent: () => sessions, buildLeaderboard: (session) => rowsBySession[session.id] }
);
assert.deepEqual(standings.map((row) => row.rank), ["A1", "A2", "B1"]);
assert.equal(standings[0].driverId, "driver_1");
assert.equal(standings[0].score, "2 (1+1+2)");
assert.equal(standings[1].score, "3 (1+2+2)");
});
test("session channel statistics summarize splits and pit stops per competitor", () => {
const result = {
competitors: {
team_1: { displayName: "Team One", driverName: "Driver One", carName: "Car One" },
},
splits: [
{ competitorKey: "team_1", splitMs: 8000 },
{ competitorKey: "team_1", splitMs: 7600 },
],
pitStops: [
{ competitorKey: "team_1", driverName: "Driver One", carName: "Car One", enteredAt: 100000, exitedAt: 112000, pitStopMs: 12000 },
{ competitorKey: "team_1", driverName: "Driver Two", carName: "Car Two", enteredAt: 200000, exitedAt: 209000, pitStopMs: 9000 },
],
};
const stats = buildSessionChannelStats({ id: "session_1" }, { ensureSessionResult: () => result });
assert.equal(stats.splitCount, 2);
assert.equal(stats.pitStopCount, 2);
assert.equal(stats.totalPitMs, 21000);
assert.equal(stats.competitors[0].averageSplitMs, 7800);
assert.equal(stats.competitors[0].bestPitStopMs, 9000);
assert.equal(stats.pitStops[1].driverName, "Driver Two");
});
test("paused session timing freezes and resumes without counting the pause", () => {
const getSessionTargetMs = () => 300_000;
const paused = getSessionTiming({ startedAt: 1_000, pausedAt: 61_000, totalPausedMs: 0, followUpSec: 0 }, 121_000, { getSessionTargetMs });
assert.equal(paused.elapsedMs, 60_000);
assert.equal(paused.remainingMs, 240_000);
const resumed = getSessionTiming({ startedAt: 1_000, pausedAt: null, totalPausedMs: 60_000, followUpSec: 0 }, 121_000, { getSessionTargetMs });
assert.equal(resumed.elapsedMs, 60_000);
assert.equal(resumed.remainingMs, 240_000);
});
test("ensureSessionResult backfills a missing passings array on an already-existing result", () => {
// A result can already exist (loaded from an older export, or a hand-edited/imported race
// package) without ever going through the brand-new-session initialization branch, the same
// way it can be missing adjustments/splits/pitEvents/pitStops - passings needs the same guard.
const state = { resultsBySession: { session_1: { competitors: {} } } };
const result = ensureSessionResult("session_1", { state });
assert.deepEqual(result.passings, []);
assert.deepEqual(result.adjustments, []);
assert.deepEqual(result.splits, []);
assert.deepEqual(result.pitEvents, []);
assert.deepEqual(result.pitStops, []);
const brandNew = ensureSessionResult("session_2", { state });
assert.deepEqual(brandNew, { passings: [], splits: [], pitEvents: [], pitStops: [], competitors: {}, adjustments: [] });
});