import test from "node:test"; import assert from "node:assert/strict"; import { recordTransponderSighting, buildWarmupCheck, createPrologue, getPrologueRemainingMs, holdPrologue, resumePrologue, setPrologueRemaining, takeDuePrologueCall, getPrologueHandoverMs, normalizePrologueSec, formatPrologueTime, PROLOGUE_OPTIONS_SEC, getLatestClockStartTs, usesPersonalClock, } from "../src/prologue_logic.js"; const entrants = [ { id: "a", name: "Anna", transponder: "1001" }, { id: "b", name: "Bo", transponder: "1002" }, { id: "c", name: "Cia", transponder: "" }, ]; test("a driver is green only once their transponder crosses after the warm-up started", () => { const sightings = new Map(); recordTransponderSighting(sightings, "1001", 500); // before the warm-up - must not count recordTransponderSighting(sightings, "1002", 1500); let check = buildWarmupCheck({ entrants, sightings, since: 1000 }); assert.deepEqual(check.rows.map((row) => row.seen), [false, true, false]); assert.equal(check.seenCount, 1); assert.equal(check.total, 3); assert.deepEqual(check.missing.map((row) => row.name), ["Anna", "Cia"], "a driver with no transponder is never green"); recordTransponderSighting(sightings, "1001", 2000); check = buildWarmupCheck({ entrants, sightings, since: 1000 }); assert.equal(check.rows[0].seen, true); assert.equal(check.rows[0].lastSeenAt, 2000); }); test("unknown transponders are listed newest first, naming the owner if it belongs to another driver", () => { const sightings = new Map(); recordTransponderSighting(sightings, "9999", 1100); recordTransponderSighting(sightings, "7777", 1200); recordTransponderSighting(sightings, "1001", 1300); recordTransponderSighting(sightings, "5555", 900); // before the warm-up const check = buildWarmupCheck({ entrants, drivers: [...entrants, { id: "x", name: "Xerxes", transponder: "7777" }], sightings, since: 1000 }); assert.deepEqual(check.unknownTransponders.map((item) => [item.transponder, item.ownerName]), [["7777", "Xerxes"], ["9999", ""]]); }); test("sightings count repeat passings and stay bounded", () => { const sightings = new Map(); recordTransponderSighting(sightings, " 42 ", 1); recordTransponderSighting(sightings, "42", 2); assert.equal(sightings.get("42").count, 2); recordTransponderSighting(sightings, "", 3); assert.equal(sightings.size, 1); for (let i = 0; i < 600; i += 1) recordTransponderSighting(sightings, `t${i}`, 10 + i); assert.equal(sightings.size, 500); assert.equal(sightings.has("t599"), true, "the newest are kept"); }); test("prologue counts down, holds and resumes without losing time", () => { let prologue = createPrologue("s1", 120000, 0); assert.equal(getPrologueRemainingMs(prologue, 30000), 90000); prologue = holdPrologue(prologue, 30000); assert.equal(getPrologueRemainingMs(prologue, 500000), 90000, "a held count-down doesn't move"); prologue = resumePrologue(prologue, 500000); assert.equal(getPrologueRemainingMs(prologue, 510000), 80000); assert.equal(getPrologueRemainingMs(prologue, 999999), 0); assert.equal(getPrologueRemainingMs(null), 0); }); // Runs a prologue second by second and collects what the announcer says. function collectCalls(totalMs, startMode) { let prologue = createPrologue("s1", totalMs, 0); const calls = []; for (let now = 0; now <= totalMs; now += 250) { const next = takeDuePrologueCall(prologue, startMode, now); prologue = next.prologue; if (next.call) calls.push(next.call.variables?.seconds ?? next.call.variables?.minutes ?? next.call.event); } return calls; } test("flying start calls the EFRA 9.2b marks: 1 min, 30 s, 10 s (2 min is the opening call)", () => { assert.deepEqual(collectCalls(120000, "flying"), ["race_1_minute", "race_30_seconds", "race_10_seconds"]); assert.deepEqual(collectCalls(300000, "flying"), [4, 3, 2, "race_1_minute", "race_30_seconds", "race_10_seconds"]); assert.deepEqual(collectCalls(30000, "flying"), ["race_10_seconds"]); }); test("grid start calls cars to the grid at 30 s and counts 10..3 second by second (IFMAR 4.2.5-4.2.6)", () => { assert.deepEqual(collectCalls(90000, "position"), ["race_1_minute", "prologue_grid", "race_10_seconds", 9, 8, 7, 6, 5, 4, 3]); assert.equal(getPrologueHandoverMs("position"), 3000, "the start signal follows 0-5 s after the 3 s mark"); assert.equal(getPrologueHandoverMs("flying"), 0); }); test("jumping to 10 s marks the skipped calls done and says only the latest", () => { let prologue = createPrologue("s1", 120000, 0); prologue = setPrologueRemaining(prologue, 10000, 1000); const { prologue: after, call } = takeDuePrologueCall(prologue, "flying", 1000); assert.equal(call.event, "race_10_seconds"); assert.deepEqual(after.announced, ["m1", "s30", "s10"]); // +1 min from there brings the 1 min / 30 s calls back. const back = setPrologueRemaining(after, 70000, 1000); assert.deepEqual(back.announced, []); assert.equal(getPrologueRemainingMs(back, 1000), 70000); }); test("prologue lengths snap to the offered choices", () => { assert.deepEqual(PROLOGUE_OPTIONS_SEC, [0, 30, 60, 90, 120, 180, 300]); assert.equal(normalizePrologueSec(90), 90); assert.equal(normalizePrologueSec("0"), 0); assert.equal(normalizePrologueSec(45), 120); assert.equal(normalizePrologueSec(undefined), 120); assert.equal(formatPrologueTime(90000, "sv"), "1 minut och 30 sekunder"); assert.equal(formatPrologueTime(120000, "en"), "2 minutes"); assert.equal(formatPrologueTime(30000, "sv"), "30 sekunder"); }); test("latest personal clock start follows EFRA 9.3 / IFMAR 4.2.3", () => { const passings = [{ timestamp: 30000, lapMs: 20000, validLap: true }, { timestamp: 25000, lapMs: 3000, validLap: false }]; // Flying: 150 % of the first completed lap after the horn. assert.equal(getLatestClockStartTs({ startedAt: 10000, startMode: "flying" }, { passings }), 10000 + 30000); assert.equal(getLatestClockStartTs({ startedAt: 10000, startMode: "flying" }, { passings: [] }), null, "unknown until someone completes a lap"); // Staggered: when the first car completes a lap, or 1 minute after the start. assert.equal(getLatestClockStartTs({ startedAt: 10000, startMode: "staggered" }, { passings }), 30000); assert.equal(getLatestClockStartTs({ startedAt: 10000, startMode: "staggered" }, { passings: [] }), 70000); assert.equal(getLatestClockStartTs({ startedAt: 10000, startMode: "mass" }, { passings }), null); assert.equal(usesPersonalClock("flying"), true); assert.equal(usesPersonalClock("position"), false); });