import test from "node:test"; import assert from "node:assert/strict"; import { applyBumpsForRace, buildEventSchedule, buildOpenPracticeBundle, buildRaceFormatConfigFromForm, buildRaceSessionsFromWizard, generateFinalsForRace, generateQualifyingForRace, getEventDrivers, getTeamDriverPool, moveDriversIntoQualifyingHeat, normalizeEvent, parseQualifyingHeatName, reassignTeamMembers, reseedUpcomingQualifying, } from "../src/event_race_logic.js"; import { buildFinalStandings, buildLeaderboard, buildQualifyingStandings, buildQualifyingTieBreakNote as buildQualifyingTieBreakNoteLogic, buildSessionChannelStats, compareNumberSet, formatLeaderboardGap, getCompetitorElapsedMs, getCompetitorSeedMetric, getQualifyingPointsValue, getSessionLapWindow, getSessionTargetMs, getSessionTiming, hasQualifyingPrimaryTie, isHighPointsTable, isUntimedSession, ensureSessionResult, } from "../src/timing_logic.js"; import { getSessionSortWeight } from "../src/race_render_helpers.js"; const testFormatLap = (ms) => (Number.isFinite(ms) ? `${ms}ms` : "-"); const testFormatRaceClock = (ms) => `${ms}ms`; const testTranslate = (key, vars) => (vars ? `${key}:${JSON.stringify(vars)}` : key); 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("reassignTeamMembers strips a moved driver/car from every other team, leaving the excluded team untouched", () => { const teams = [ { id: "team_a", name: "A", driverIds: ["d1", "d2"], carIds: ["c1"] }, { id: "team_b", name: "B", driverIds: ["d3"], carIds: [] }, ]; const result = reassignTeamMembers(teams, { excludeTeamId: "team_b", driverIds: ["d1"], carIds: ["c1"] }); const teamA = result.find((team) => team.id === "team_a"); const teamB = result.find((team) => team.id === "team_b"); assert.deepEqual(teamA.driverIds, ["d2"], "d1 was moved out of team A"); assert.deepEqual(teamA.carIds, [], "c1 was moved out of team A too"); assert.deepEqual(teamB.driverIds, ["d3"], "the team being saved is left for the caller to update, not touched here"); }); test("reassignTeamMembers returns unchanged team objects as-is when nothing overlaps", () => { const teams = [{ id: "team_a", name: "A", driverIds: ["d1"], carIds: [] }]; const result = reassignTeamMembers(teams, { excludeTeamId: "team_b", driverIds: ["d9"], carIds: [] }); assert.strictEqual(result[0], teams[0]); }); test("getTeamDriverPool scopes to the event's class/participants, only falling back to every driver when that list is empty", () => { const state = { drivers: [ { id: "d1", name: "One", classId: "2wd" }, { id: "d2", name: "Two", classId: "4wd" }, ], }; const getEventDrivers = (event) => state.drivers.filter((driver) => !event?.classId || driver.classId === event.classId); const scoped = getTeamDriverPool({ classId: "2wd", raceConfig: {} }, { getEventDrivers, state }); assert.deepEqual(scoped.drivers.map((driver) => driver.id), ["d1"]); assert.equal(scoped.fallback, false); const noMatches = getTeamDriverPool({ classId: "unknown_class", raceConfig: {} }, { getEventDrivers, state }); assert.deepEqual(noMatches.drivers.map((driver) => driver.id), ["d1", "d2"], "falls back to every driver rather than leaving zero checkboxes"); assert.equal(noMatches.fallback, true); }); // Regression coverage for the user's report: a staggered heat's start-order announcement // (and its start grid, and any fallback qualifying/finals seeding with no practice data // yet) all derive from getEventDrivers - it used to return drivers in the club's global // directory/creation order, completely ignoring the order they were actually selected as // this event's participants, which is what a club admin actually sees as "the list". test("getEventDrivers follows the order participants were selected in, not the drivers directory's creation order", () => { const state = { drivers: [ { id: "d1", name: "One", classId: "2wd" }, { id: "d2", name: "Two", classId: "2wd" }, { id: "d3", name: "Three", classId: "2wd" }, { id: "d4", name: "Four", classId: "2wd" }, { id: "d5", name: "Five", classId: "4wd" }, ], }; const event = { classId: "2wd", raceConfig: { participantsConfigured: true, driverIds: ["d4", "d2", "d1"] } }; assert.deepEqual(getEventDrivers(event, { state }).map((driver) => driver.id), ["d4", "d2", "d1"], "matches selection order, not directory order, and excludes d3 who was never selected"); const stale = { classId: "2wd", raceConfig: { participantsConfigured: true, driverIds: ["d4", "d5", "d1"] } }; assert.deepEqual(getEventDrivers(stale, { state }).map((driver) => driver.id), ["d4", "d1"], "drops a selected id that no longer belongs to this event's class (d5 is 4wd)"); const unconfigured = { classId: "2wd", raceConfig: {} }; assert.deepEqual(getEventDrivers(unconfigured, { state }).map((driver) => driver.id), ["d1", "d2", "d3", "d4"], "before participants are configured, falls back to every driver in the class"); }); // Found while building buildEventSchedule below (which relies on this for running order): // open_practice's own weight is 0, which `order[key] || 99` treated as falsy and silently // replaced with the 99 "unknown type" fallback - sorting practice sessions dead last // instead of first everywhere this drives ordering (print/PDF start lists and results, // and now the schedule). test("getSessionSortWeight puts open_practice first, not last (0 is a valid weight, not a missing one)", () => { assert.equal(getSessionSortWeight({ type: "open_practice" }), 0); assert.ok(getSessionSortWeight({ type: "open_practice" }) < getSessionSortWeight({ type: "qualification" })); assert.ok(getSessionSortWeight({ type: "qualification" }) < getSessionSortWeight({ type: "final" })); assert.equal(getSessionSortWeight({ type: "something_unknown" }), 99, "a genuinely unknown type still falls back to last"); }); // Regression coverage for the user's feature request: a time-of-day schedule for the // whole race day, with each session's planned clock time worked out automatically, that // shifts forward on its own once reality (a late start, a session running long or being // paused) diverges from the plan - no separate "reschedule" step needed. test("buildEventSchedule cascades planned times in running order and re-anchors to reality once a session actually happens", () => { const event = { id: "ev1", mode: "race", raceConfig: { scheduleGapMin: 3 } }; const practice = { id: "s_practice", eventId: "ev1", mode: "race", type: "open_practice", name: "Open Practice", durationMin: 5 }; const qual = { id: "s_qual", eventId: "ev1", mode: "race", type: "qualification", name: "Q1 H1", durationMin: 5 }; const final = { id: "s_final", eventId: "ev1", mode: "race", type: "final", name: "A Final 1", durationMin: 8 }; const trackSession = { id: "s_track", eventId: "ev1", mode: "track", type: "heat", name: "Track Heat", durationMin: 5 }; const sessions = [practice, qual, final, trackSession]; assert.deepEqual(buildEventSchedule({ ...event, raceConfig: { ...event.raceConfig, scheduleStartAt: null } }, sessions, { getSessionSortWeight }), [], "no schedule at all until a start time is set"); const scheduleStartAt = 1_000_000; const plan = buildEventSchedule({ ...event, raceConfig: { ...event.raceConfig, scheduleStartAt } }, sessions, { getSessionSortWeight }); assert.equal(plan.length, 3, "track-mode sessions are excluded from the race-day schedule"); assert.deepEqual(plan.map((row) => row.sessionId), ["s_practice", "s_qual", "s_final"], "running order: practice, then qualifying, then finals"); assert.equal(plan[0].plannedStart, 1_000_000); assert.equal(plan[0].plannedEnd, 1_300_000, "5 minutes after the day's start time"); assert.equal(plan[1].plannedStart, 1_480_000, "starts 3 minutes (the gap) after practice's planned end"); assert.equal(plan[1].plannedEnd, 1_780_000); assert.equal(plan[2].plannedStart, 1_960_000, "again 3 minutes after qualifying's planned end"); assert.equal(plan[2].plannedEnd, 2_440_000, "8 minutes for the final"); assert.ok(plan.every((row) => row.delayed === false), "nothing has actually started yet, so nothing is flagged delayed"); // Practice started 2 minutes late (well past the 1-minute delay threshold) and is still // running - everything after it must re-anchor to this real start, not the original plan. const lateStart = { ...event, raceConfig: { ...event.raceConfig, scheduleStartAt } }; const lateSessions = [{ ...practice, startedAt: 1_120_000 }, qual, final]; const latePlan = buildEventSchedule(lateStart, lateSessions, { getSessionSortWeight }); assert.equal(latePlan[0].delayed, true, "started more than a minute after its own planned time"); assert.equal(latePlan[1].plannedStart, 1_600_000, "qualifying's plan shifts forward by practice's own 2-minute delay"); // Practice actually finished, having run 50s over (e.g. a track call) - the real endedAt // (which already reflects any pause) is what the rest of the day anchors to, not the // nominal 5-minute duration. const finishedSessions = [{ ...practice, startedAt: 1_000_000, status: "finished", endedAt: 1_350_000 }, qual, final]; const finishedPlan = buildEventSchedule(lateStart, finishedSessions, { getSessionSortWeight }); assert.equal(finishedPlan[1].plannedStart, 1_530_000, "1,350,000 (real end) + 180,000 (3-minute gap)"); }); 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"]); // Seven drivers, max three per heat: 3 + 2 + 2 (never someone driving alone). assert.deepEqual(state.sessions[2].driverIds, ["driver_4", "driver_5"]); assert.deepEqual(state.sessions[4].driverIds, ["driver_6", "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, mismatched: 0 }); assert.deepEqual(sessions[0].driverIds, ["driver_3", "driver_1"]); assert.deepEqual(sessions[1].driverIds, ["locked"]); assert.deepEqual(sessions[2].driverIds, ["finished"]); }); test("qualifying reseed sorts heats numerically, not as text (H10 must not sort before H2)", () => { const sessions = ["H1", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "H10", "H11"].map((heat) => ({ name: `Q1 ${heat}`, type: "qualification", generated: true, status: "ready", driverIds: [], manualGridIds: [], gridCustomized: false, })); const standings = Array.from({ length: 22 }, (_, index) => ({ driverId: `driver_${index + 1}`, rank: index + 1 })); const result = reseedUpcomingQualifying( { id: "event_1", raceConfig: { carsPerHeat: 2 } }, { buildQualifyingStandings: () => standings, buildPracticeStandings: () => [], getEventDrivers: () => [], getSessionsForEvent: () => sessions, chunkArray } ); assert.deepEqual(result, { updated: 11, locked: 0, mismatched: 0 }); const heat2 = sessions.find((session) => session.name === "Q1 H2"); const heat10 = sessions.find((session) => session.name === "Q1 H10"); assert.deepEqual(heat2.driverIds, ["driver_3", "driver_4"], "the 2nd-fastest pair lands in heat 2, not wherever text-sort would put it"); assert.deepEqual(heat10.driverIds, ["driver_19", "driver_20"], "heat 10 gets the pair its numeric position actually calls for"); }); test("qualifying reseed reports a field-size mismatch instead of silently dropping or ghosting heats", () => { const sessions = [ { name: "Q1 H1", type: "qualification", generated: true, status: "ready", driverIds: [], manualGridIds: [], gridCustomized: false }, { name: "Q1 H2", type: "qualification", generated: true, status: "ready", driverIds: [], manualGridIds: [], gridCustomized: false }, ]; // 4 heats' worth of drivers now exist, but only 2 ready heat sessions do. const standings = Array.from({ length: 8 }, (_, index) => ({ driverId: `driver_${index + 1}`, rank: index + 1 })); const result = reseedUpcomingQualifying( { id: "event_1", raceConfig: { carsPerHeat: 2 } }, { buildQualifyingStandings: () => standings, buildPracticeStandings: () => [], getEventDrivers: () => [], getSessionsForEvent: () => sessions, chunkArray } ); assert.equal(result.mismatched, 1, "the round's ready-heat count no longer matches the participant list"); }); test("parseQualifyingHeatName extracts the round and heat number, or null for anything else", () => { assert.deepEqual(parseQualifyingHeatName("Q2 H3"), { round: 2, heat: 3 }); assert.deepEqual(parseQualifyingHeatName("Q10 H11"), { round: 10, heat: 11 }); assert.equal(parseQualifyingHeatName("A Final 1"), null); assert.equal(parseQualifyingHeatName(""), null); }); test("moveDriversIntoQualifyingHeat moves a lone driver out of their old heat into a new one, in the same round only", () => { const sessions = [ { id: "q1h1", name: "Q1 H1", type: "qualification", driverIds: ["d1", "d2"], manualGridIds: ["d1", "d2"] }, { id: "q1h2", name: "Q1 H2", type: "qualification", driverIds: ["d3"], manualGridIds: ["d3"] }, // A different round's H1 must never be touched by an edit to Q1's heats. { id: "q2h1", name: "Q2 H1", type: "qualification", driverIds: ["d1", "d2"], manualGridIds: ["d1", "d2"] }, ]; moveDriversIntoQualifyingHeat(sessions, "q1h1", ["d1", "d2", "d3"]); const q1h1 = sessions.find((s) => s.id === "q1h1"); const q1h2 = sessions.find((s) => s.id === "q1h2"); const q2h1 = sessions.find((s) => s.id === "q2h1"); assert.deepEqual(q1h2.driverIds, [], "d3 was pulled out of Q1 H2, the heat they were lonely in"); assert.deepEqual(q2h1.driverIds, ["d1", "d2"], "Q2's heats are untouched - only Q1's heats are affected by editing a Q1 heat"); // moveDriversIntoQualifyingHeat only clears the OTHER sessions - the caller (the real // sessionEditForm handler) is the one that sets the target session's own driverIds. assert.equal(q1h1.id, "q1h1"); }); test("moveDriversIntoQualifyingHeat does nothing for a session with no parseable heat name", () => { const sessions = [{ id: "s1", name: "Open Practice", type: "practice", driverIds: ["d1"] }]; moveDriversIntoQualifyingHeat(sessions, "s1", ["d1"]); assert.deepEqual(sessions[0].driverIds, ["d1"], "left untouched - not a qualifying heat, nothing to move"); }); 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("final generation never produces more than 26 mains, even with a large field and an aggressive bump count", () => { const state = { sessions: [] }; const sourceRows = Array.from({ length: 60 }, (_, index) => ({ driverId: `driver_${index + 1}` })); const event = { id: "event_1", raceConfig: { finalsSource: "qualifying", finalLegs: 1, carsPerFinal: 4, finalDurationMin: 5, finalStartMode: "position", followUpSec: 0, bumpCount: 50, reserveBumpSlots: true, }, }; generateFinalsForRace(event, { buildPracticeStandings: () => [], buildQualifyingStandings: () => sourceRows, clearGeneratedFinals: () => {}, state, normalizeStartMode, normalizeSession: (session) => session, uid: createUid(), chunkArray, t: (key) => key, }); assert.ok(state.sessions.length <= 26, `expected at most 26 mains, got ${state.sessions.length}`); assert.ok(state.sessions.every((session) => /^[A-Z] /.test(session.name)), "every main name is a single real letter, never running past Z"); }); test("bump-up promotes lower-main winners into reserved upper-main slots and removes them from the lower main's remaining legs", () => { const sessions = [ { name: "A final 1", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 1, driverIds: ["a1", "a2", "a3"], manualGridIds: ["a1", "a2", "a3"] }, // B final 1 already ran (produced the standings below) and keeps its real result. B // final 2 is the next leg still to come - that's the one that should lose the // promoted driver, not the one that already happened. { name: "B final 1", type: "final", status: "finished", maxCars: 4, driverIds: ["b1", "b2", "b3"] }, { name: "B final 2", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 1, driverIds: ["b1", "b2", "b3"], manualGridIds: ["b1", "b2", "b3"] }, { name: "C final 1", type: "final", status: "finished", maxCars: 4, 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, reserveBumpSlots: true } }, { buildFinalStandings: () => standings, getSessionsForEvent: () => sessions } ); assert.equal(applied, 2); assert.deepEqual(sessions[0].driverIds, ["a1", "a2", "a3", "b1"], "b1 promoted into A"); assert.deepEqual(sessions[1].driverIds, ["b1", "b2", "b3"], "B's already-finished leg keeps its real historical result"); assert.deepEqual(sessions[2].driverIds, ["b2", "b3", "c1"], "b1 removed from B's upcoming leg (promoted out), c1 added (promoted in) - no more double-entry"); assert.equal(sessions[0].reservedBumpSlots, 0); }); test("bump-up still grows a main when reserveBumpSlots is off, instead of silently doing nothing because the main looks full", () => { const sessions = [ { name: "A final 1", type: "final", status: "ready", maxCars: 4, reservedBumpSlots: 0, driverIds: ["a1", "a2", "a3", "a4"], manualGridIds: ["a1", "a2", "a3", "a4"] }, { name: "B final 1", type: "final", status: "finished", maxCars: 4, driverIds: ["b1", "b2", "b3", "b4"] }, ]; const standings = [ { rank: "A1", driverId: "a1" }, { rank: "B1", driverId: "b1" }, ]; const applied = applyBumpsForRace( { id: "event_1", raceConfig: { bumpCount: 1, carsPerFinal: 4, reserveBumpSlots: false } }, { buildFinalStandings: () => standings, getSessionsForEvent: () => sessions } ); assert.equal(applied, 1, "b1 is actually added, not blocked because A already has 4/4 seeded drivers"); assert.deepEqual(sessions[0].driverIds, ["a1", "a2", "a3", "a4", "b1"]); }); 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); }); // Regression coverage for the user's report: a staggered-start qualifying heat with 4 // drivers shouldn't force-finish (and cut off) the last-released driver before they've // had their own full personal window - the shared clock must cover the whole release // schedule (3 gaps of staggerGapSec between 4 cars), on top of the plain duration. test("staggered sessions extend the shared clock/auto-finish target by the full release schedule, without changing each driver's own personal duration", () => { const getTarget = (session) => getSessionTargetMs(session, { isUntimedSession }); const staggeredHeat = { type: "qualification", startMode: "staggered", durationMin: 5, staggerGapSec: 3, driverIds: ["d1", "d2", "d3", "d4"], manualGridIds: ["d1", "d2", "d3", "d4"], startedAt: 1000 }; // Each competitor's own quota (used for buildLeaderboard's personalTimeUp) is untouched. assert.equal(getTarget(staggeredHeat), 300000); // The session-level clock adds 3 gaps of 3s (for 4 cars) on top of the 5-minute base. const timing = getSessionTiming(staggeredHeat, 301000, { getSessionTargetMs: getTarget }); assert.equal(timing.targetMs, 309000, "5:00 plus 3 gaps of 3s = 5:09"); assert.equal(timing.remainingMs, 9000, "still 9s of the extended window left at the nominal 5:00 mark"); // A mass/position start with the same driver count is never extended. const massFinal = { ...staggeredHeat, startMode: "mass" }; assert.equal(getSessionTiming(massFinal, 301000, { getSessionTargetMs: getTarget }).remainingMs, 0); // A single-driver staggered heat has no release gaps to cover. const soloHeat = { ...staggeredHeat, driverIds: ["d1"], manualGridIds: ["d1"] }; assert.equal(getSessionTiming(soloHeat, 301000, { getSessionTargetMs: getTarget }).remainingMs, 0); }); test("a finished session's elapsed time is frozen at endedAt, not still counting up against the real clock", () => { const getTarget = (session) => getSessionTargetMs(session, { isUntimedSession }); const finished = { type: "final", durationMin: 5, status: "finished", startedAt: 1000, endedAt: 301000 }; const rightAfter = getSessionTiming(finished, 301000, { getSessionTargetMs: getTarget }); const muchLater = getSessionTiming(finished, 301000 + 3600000, { getSessionTargetMs: getTarget }); assert.equal(rightAfter.elapsedMs, 300000); assert.equal(muchLater.elapsedMs, 300000, "checking an hour later must report the exact same, fixed elapsed time"); }); test("getCompetitorElapsedMs excludes the session's accumulated paused time", () => { const session = { startedAt: 1000, totalPausedMs: 5000 }; assert.equal(getCompetitorElapsedMs(session, { lastTimestamp: 21000 }), 15000); assert.equal(getCompetitorElapsedMs({ startedAt: 1000, totalPausedMs: 0 }, { lastTimestamp: 21000 }), 20000, "no pause, no change"); assert.equal(getCompetitorElapsedMs(session, { lastTimestamp: 0 }), 0, "no passing recorded yet"); }); test("buildLeaderboard excludes paused wall-clock time from a competitor's elapsed/comparison time, so a paused heat stays comparable to an unpaused one", () => { const result = { competitors: { driver_1: { key: "driver_1", driverName: "One", startTimestamp: 1000, laps: 1, lastLapMs: 20000, lastTimestamp: 21000, bestLapMs: 20000, manualLapAdjustment: 0, manualTimeAdjustmentMs: 0 }, }, }; const passings = [{ competitorKey: "driver_1", timestamp: 21000, lapMs: 20000, validLap: true }]; // A 5000ms track call happened mid-heat - nobody actually raced during it, so it must // not count against this driver's comparison time. const session = { id: "s1", type: "final", startedAt: 1000, totalPausedMs: 5000, durationMin: 5 }; const rows = buildLeaderboard(session, { ensureSessionResult: () => result, getSessionTargetMs: (s) => getSessionTargetMs(s, { isUntimedSession }), getCompetitorPassings: (_s, row, options = {}) => passings .filter((passing) => passing.competitorKey === row.key) .filter((passing) => options.includeInvalid || passing.validLap !== false), isCountedPassing: (passing) => passing.validLap !== false, getCompetitorElapsedMs, getCompetitorSeedMetric: () => null, getSessionLapWindow: () => ({ minLapMs: 0, maxLapMs: Infinity }), getPassingValidationLabel: () => "", formatSeedMetric: () => "", formatRaceClock: (ms) => `${ms}`, formatLeaderboardGap: () => "", formatLapDelta: () => "", formatLap: (ms) => `${ms}`, t: (key) => key, }); assert.equal(rows.length, 1); assert.equal(rows[0].totalElapsedMs, 15000, "the 5000ms pause is excluded from the driver's own elapsed/comparison time"); assert.equal(rows[0].comparisonMs, 15000); }); // Regression coverage for the user's report: in a staggered-start qualifying heat, each // driver's own personal 5-minute window runs from their own first crossing, independent of // the shared session clock - so one driver can be "done" while the session is still // running for others. Nothing surfaced that distinction anywhere in the UI before this. test("buildLeaderboard flags a staggered-mode competitor whose own personal window has elapsed, and only in staggered mode", () => { const buildRows = (session, competitors) => buildLeaderboard(session, { ensureSessionResult: () => ({ competitors }), getSessionTargetMs: (s) => getSessionTargetMs(s, { isUntimedSession }), getCompetitorPassings: () => [], isCountedPassing: () => true, getCompetitorElapsedMs, getCompetitorSeedMetric: () => null, getSessionLapWindow: () => ({ minLapMs: 0, maxLapMs: Infinity }), getPassingValidationLabel: () => "", formatSeedMetric: () => "", formatRaceClock: (ms) => `${ms}`, formatLeaderboardGap: () => "", formatLapDelta: () => "", formatLap: (ms) => `${ms}`, t: (key) => key, }); const now = Date.now(); const competitors = { driver_done: { key: "driver_done", driverName: "Done", startTimestamp: now - 6 * 60 * 1000, laps: 10, lastLapMs: 17000, lastTimestamp: now - 60000, bestLapMs: 16500, manualLapAdjustment: 0, manualTimeAdjustmentMs: 0 }, driver_still_going: { key: "driver_still_going", driverName: "Still going", startTimestamp: now - 2 * 60 * 1000, laps: 4, lastLapMs: 17000, lastTimestamp: now - 5000, bestLapMs: 16800, manualLapAdjustment: 0, manualTimeAdjustmentMs: 0 }, }; const staggered = buildRows({ id: "q1h1", type: "qualification", startMode: "staggered", durationMin: 5, totalPausedMs: 0 }, competitors); assert.equal(staggered.find((row) => row.key === "driver_done").personalTimeUp, true, "6 minutes into their own 5-minute window - done"); assert.equal(staggered.find((row) => row.key === "driver_still_going").personalTimeUp, false, "only 2 minutes into their own window - still going"); const mass = buildRows({ id: "final", type: "final", startMode: "mass", durationMin: 5, totalPausedMs: 0 }, competitors); assert.ok(mass.every((row) => row.personalTimeUp === false), "mass/position starts share one clock already shown at the session level - never flagged per-row"); }); test("buildLeaderboard's personalTimeUp excludes paused time from a staggered competitor's own window too", () => { const now = Date.now(); const competitors = { driver_1: { key: "driver_1", driverName: "One", startTimestamp: now - 5 * 60 * 1000 - 30000, laps: 10, lastLapMs: 17000, lastTimestamp: now - 60000, bestLapMs: 16500, manualLapAdjustment: 0, manualTimeAdjustmentMs: 0 }, }; const deps = { ensureSessionResult: () => ({ competitors }), getSessionTargetMs: (s) => getSessionTargetMs(s, { isUntimedSession }), getCompetitorPassings: () => [], isCountedPassing: () => true, getCompetitorElapsedMs, getCompetitorSeedMetric: () => null, getSessionLapWindow: () => ({ minLapMs: 0, maxLapMs: Infinity }), getPassingValidationLabel: () => "", formatSeedMetric: () => "", formatRaceClock: (ms) => `${ms}`, formatLeaderboardGap: () => "", formatLapDelta: () => "", formatLap: (ms) => `${ms}`, t: (key) => key, }; // 5:30 has passed since this driver's own first crossing, but a 1-minute track call is // excluded from their personal window the same way it already is from the shared clock - // net real driving time is only 4:30 of their 5:00, so they are not done yet. const paused = buildLeaderboard({ id: "q1h1", type: "qualification", startMode: "staggered", durationMin: 5, totalPausedMs: 60000 }, deps); assert.equal(paused[0].personalTimeUp, false); }); 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("final standings keep a driver who never recorded a passing (DNS) instead of dropping them, ranked last in their main", () => { const sessions = [{ id: "a1", type: "final", name: "A final 1", status: "finished" }]; const rowsBySession = { a1: [{ driverId: "driver_1", driverName: "One", totalElapsedMs: 301000, resultDisplay: "12/5:01.000" }], }; const entrants = [{ id: "driver_1", name: "One" }, { id: "driver_2", name: "Two" }]; const standings = buildFinalStandings( { id: "event_1", raceConfig: {} }, { getSessionsForEvent: () => sessions, buildLeaderboard: (session) => rowsBySession[session.id], getSessionEntrants: () => entrants, t: testTranslate, } ); assert.deepEqual(standings.map((row) => row.driverId), ["driver_1", "driver_2"]); assert.equal(standings[1].driverId, "driver_2"); assert.equal(standings[1].rank, "A2", "the DNS driver still gets a real, last-place rank in their main"); // A not-yet-run final must not invent DNS rows for drivers who simply haven't raced yet. const notYetRun = buildFinalStandings( { id: "event_1", raceConfig: {} }, { getSessionsForEvent: () => [{ id: "a1", type: "final", name: "A final 1", status: "ready" }], buildLeaderboard: () => rowsBySession.a1, getSessionEntrants: () => entrants, t: testTranslate, } ); assert.deepEqual(notYetRun.map((row) => row.driverId), ["driver_1"], "driver_2 hasn't raced yet, not DNS - correctly absent"); }); test("qualifying standings keep a driver who never recorded a passing (DNS) so they aren't silently dropped from future rounds or finals", () => { const sessions = [{ id: "q1h1", type: "qualification", name: "Q1 H1", status: "finished" }]; const rows = [{ driverId: "driver_1", driverName: "One", comparisonMs: 16000, bestLapMs: 16000, resultDisplay: "16.000" }]; const entrants = [{ id: "driver_1", name: "One" }, { id: "driver_2", name: "Two" }]; const standings = buildQualifyingStandings( { id: "event_1", raceConfig: {} }, { getSessionsForEvent: () => sessions, buildLeaderboard: () => rows, getSessionEntrants: () => entrants, isHighPointsTable, getQualifyingPointsValue, compareNumberSet, buildQualifyingTieBreakNote: (row, tieBreak) => buildQualifyingTieBreakNoteLogic(row, tieBreak, { t: testTranslate, formatLap: testFormatLap }), formatLap: testFormatLap, formatRaceClock: testFormatRaceClock, t: testTranslate, hasQualifyingPrimaryTie, } ); assert.deepEqual(standings.map((row) => row.driverId), ["driver_1", "driver_2"], "driver_2 (DNS) still appears, ranked last"); assert.equal(standings[1].bestSingleLapMs, Number.MAX_SAFE_INTEGER, "a DNS driver's missing lap is never confused for a 0ms fastest lap"); }); test("a driver who DNS'd in one heat but raced normally in the others is still fully counted across all of them, not dropped", () => { const sessions = [ { id: "q1h1", type: "qualification", name: "Q1 H1", status: "finished" }, { id: "q2h1", type: "qualification", name: "Q2 H1", status: "finished" }, { id: "q3h1", type: "qualification", name: "Q3 H1", status: "finished" }, ]; // driver_2 DNS'd heat 1 (dead battery), but raced - and won - heats 2 and 3. const rowsBySession = { q1h1: [{ driverId: "driver_1", driverName: "One", comparisonMs: 16000, bestLapMs: 16000, resultDisplay: "16.000" }], q2h1: [ { driverId: "driver_2", driverName: "Two", comparisonMs: 15000, bestLapMs: 15000, resultDisplay: "15.000" }, { driverId: "driver_1", driverName: "One", comparisonMs: 16500, bestLapMs: 16500, resultDisplay: "16.500" }, ], q3h1: [ { driverId: "driver_2", driverName: "Two", comparisonMs: 14800, bestLapMs: 14800, resultDisplay: "14.800" }, { driverId: "driver_1", driverName: "One", comparisonMs: 16200, bestLapMs: 16200, resultDisplay: "16.200" }, ], }; const entrants = [{ id: "driver_1", name: "One" }, { id: "driver_2", name: "Two" }]; const standings = buildQualifyingStandings( // Counting all 3 rounds (not just the best one) so a dropped round would be visible // in the total score, not silently hidden by "best of N" already excluding it. { id: "event_1", raceConfig: { countedQualRounds: 3 } }, { getSessionsForEvent: () => sessions, buildLeaderboard: (session) => rowsBySession[session.id], getSessionEntrants: () => entrants, isHighPointsTable, getQualifyingPointsValue, compareNumberSet, buildQualifyingTieBreakNote: (row, tieBreak) => buildQualifyingTieBreakNoteLogic(row, tieBreak, { t: testTranslate, formatLap: testFormatLap }), formatLap: testFormatLap, formatRaceClock: testFormatRaceClock, t: testTranslate, hasQualifyingPrimaryTie, } ); const driverTwo = standings.find((row) => row.driverId === "driver_2"); assert.ok(driverTwo, "driver_2 still appears at all - not dropped because of the heat 1 DNS"); assert.deepEqual(driverTwo.ranks.sort((a, b) => a - b), [1, 1, 2], "won heat 2 and heat 3 outright (rank 1), only heat 1's DNS counts against them (rank 2 of 2)"); assert.equal(driverTwo.countedPoints.length, 3, "all three heats count toward the score, including the DNS one - it isn't excluded or silently skipped"); }); function buildQualifyingStandingsForTest(event, sessions, rowsBySession, entrants) { return buildQualifyingStandings(event, { getSessionsForEvent: () => sessions, buildLeaderboard: (session) => rowsBySession[session.id], getSessionEntrants: () => entrants, isHighPointsTable, getQualifyingPointsValue, compareNumberSet, buildQualifyingTieBreakNote: (row, tieBreak) => buildQualifyingTieBreakNoteLogic(row, tieBreak, { t: testTranslate, formatLap: testFormatLap }), formatLap: testFormatLap, formatRaceClock: testFormatRaceClock, t: testTranslate, hasQualifyingPrimaryTie, }); } test("a DNS driver is scored correctly under a high-points table (field_desc), not just the default low-points one", () => { const sessions = [ { id: "h1", type: "qualification", name: "Q1 H1", status: "finished" }, { id: "h2", type: "qualification", name: "Q2 H1", status: "finished" }, { id: "h3", type: "qualification", name: "Q3 H1", status: "finished" }, ]; const entrants = [{ id: "driver_a", name: "A" }, { id: "driver_b", name: "B" }]; const rowsBySession = { // driver_a DNS'd heat 1 (driver_b is the only real finisher there), then won heats 2 // and 3 outright with driver_b second both times. h1: [{ driverId: "driver_b", driverName: "B", comparisonMs: 16000, resultDisplay: "16.000" }], h2: [ { driverId: "driver_a", driverName: "A", comparisonMs: 15500, resultDisplay: "15.500" }, { driverId: "driver_b", driverName: "B", comparisonMs: 16200, resultDisplay: "16.200" }, ], h3: [ { driverId: "driver_a", driverName: "A", comparisonMs: 15400, resultDisplay: "15.400" }, { driverId: "driver_b", driverName: "B", comparisonMs: 16100, resultDisplay: "16.100" }, ], }; const standings = buildQualifyingStandingsForTest( { id: "event_1", raceConfig: { qualifyingPointsTable: "field_desc", countedQualRounds: 3 } }, sessions, rowsBySession, entrants ); // field_desc in a 2-driver field: 1st = 2 points, 2nd (or DNS, ranked after the field) = 1. const driverA = standings.find((row) => row.driverId === "driver_a"); const driverB = standings.find((row) => row.driverId === "driver_b"); assert.equal(driverA.totalScore, 1 + 2 + 2, "1 point for the DNS heat (ranked last, not zero and not the winner's points), 2 for each heat won"); assert.equal(driverB.totalScore, 2 + 1 + 1, "2 points for winning the DNS heat by default, 1 for each 2nd place"); assert.equal(standings[0].driverId, "driver_a", "two outright wins plus a DNS still beats three consistent 2nd places under a high-points table"); }); test("a DNS driver is scored correctly under \"best time\" qualifying (not points-based) - a bad DNS heat never contaminates their best result", () => { const sessions = [ { id: "h1", type: "qualification", name: "Q1 H1", status: "finished" }, { id: "h2", type: "qualification", name: "Q2 H1", status: "finished" }, ]; const entrants = [{ id: "driver_a", name: "A" }, { id: "driver_b", name: "B" }]; const rowsBySession = { h1: [{ driverId: "driver_b", driverName: "B", comparisonMs: 20000, resultDisplay: "20.000" }], // driver_a DNS h2: [ { driverId: "driver_a", driverName: "A", comparisonMs: 15000, resultDisplay: "15.000" }, // driver_a's genuine best time { driverId: "driver_b", driverName: "B", comparisonMs: 16000, resultDisplay: "16.000" }, ], }; const standings = buildQualifyingStandingsForTest( { id: "event_1", raceConfig: { qualifyingScoring: "best" } }, sessions, rowsBySession, entrants ); const driverA = standings.find((row) => row.driverId === "driver_a"); assert.equal(driverA.bestRank, 1, "driver_a's single best placing (1st in heat 2) is what counts, unaffected by the DNS heat's last place"); assert.equal(driverA.bestRoundMetric, 15000, "their genuine 15.000s best time, not inflated by the DNS heat's Number.MAX_SAFE_INTEGER placeholder"); assert.equal(standings[0].driverId, "driver_a", "ranks ahead of driver_b, who never beat a 16.000s best"); }); 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: [] }); });