Fix staggered heat call-out order and extend its shared clock
getEventDrivers followed the drivers directory's internal creation order instead of the order participants were actually selected for the event, so the fallback qualifying/finals grid, the Start List display, and the staggered-release announcer all read out drivers in a scrambled order relative to how the event was actually entered. Fixed to preserve raceConfig.driverIds' own order. Also, a staggered heat's shared clock/auto-finish target only ever counted the flat session duration, not the staggered release schedule itself - so the last-released driver's own personal window could still be running when the session force-finished. The shared clock now widens by (driverCount - 1) * staggerGapSec on top of the base duration for staggered sessions; each driver's own personal quota is unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
buildRaceSessionsFromWizard,
|
||||
generateFinalsForRace,
|
||||
generateQualifyingForRace,
|
||||
getEventDrivers,
|
||||
getTeamDriverPool,
|
||||
moveDriversIntoQualifyingHeat,
|
||||
normalizeEvent,
|
||||
@@ -185,6 +186,31 @@ test("getTeamDriverPool scopes to the event's class/participants, only falling b
|
||||
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");
|
||||
});
|
||||
|
||||
test("race wizard creates configured practice, qualifying, and endurance sessions", () => {
|
||||
const event = {
|
||||
id: "event_1",
|
||||
@@ -494,6 +520,31 @@ test("session timing handles untimed practice, countdown, and follow-up", () =>
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user