import test from "node:test"; import assert from "node:assert/strict"; import { applyBumpsForRace, buildRaceFormatConfigFromForm, buildRaceSessionsFromWizard, generateFinalsForRace, generateQualifyingForRace, reseedUpcomingQualifying, } from "../src/event_race_logic.js"; import { buildFinalStandings, compareNumberSet, formatLeaderboardGap, getCompetitorSeedMetric, getQualifyingPointsValue, getSessionLapWindow, getSessionTargetMs, getSessionTiming, isHighPointsTable, isUntimedSession, } 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("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)"); });