Give scheduled "Practice" sessions the real start ceremony too

A "practice" session used as a scheduled slot in a structured
race/sponsor event's session plan silently skipped the entire
countdown/lights/start-tone ceremony and jumped straight to running,
unlike qualifying/heat/final. Narrowed the ceremony-skip check to
only the standalone "Quick Open Practice" shortcut and free_practice
(both rolling/best-lap ranked with no fixed roster) - "practice" is
already ranked like a real timed session in every other respect and
now gets the same start sequence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-10 23:00:31 +02:00
parent cf89086dff
commit 85e18db0ed
6 changed files with 54 additions and 10 deletions

View File

@@ -8,6 +8,12 @@ The application version is maintained in `package.json` and displayed in the adm
No changes recorded yet.
## 1.16.1 - 2026-09-10
### Fixed
- A scheduled "Practice" session inside a structured race/sponsor event (as opposed to the standalone Quick Open Practice shortcut) silently skipped the entire start ceremony - no countdown, no lights, no start tone - jumping straight to running instead of matching qualifying/heat/final. It now gets the same countdown/lights/start-tone sequence; only Quick Open Practice and free practice still start instantly.
## 1.16.0 - 2026-09-10
### Added

View File

@@ -384,6 +384,19 @@ Found and fixed a real, previously-existing bug while building this, unrelated t
Verified 2026-09-10: new unit tests cover `getSessionSortWeight`'s fix directly, and `buildEventSchedule`'s cascading math, the running-order sort, track-mode sessions being excluded, and the re-anchoring behavior for both a late-but-still-running session and a finished one whose real duration (including a pause) differed from the plan. A full headless-browser run against a real, isolated server reproduced the exact reported workflow: set a 12:34 start time with a 3-minute gap on a real Open Practice + Q1 H1, confirmed the schedule panel showed practice at 12:34-12:39 and qualifying at 12:42-12:47, confirmed the printed start list included "12:34" next to Open Practice, then made practice actually start 2 minutes late and confirmed the panel flagged it "Delayed" and qualifying's planned time shifted forward to 12:44 automatically.
## Scheduled "Practice" sessions silently skipped the start ceremony
Reported 2026-09-10 by the user: starting a session in a sponsor event didn't count down like a normal race does. Investigation took a few rounds since the initial description ("says '10 seconds to start' and 'starting on the sound of the tone' but doesn't visibly count down") didn't match either of the two things actually tested:
- A real headless-browser test of a sponsor-event heat (mass start, 10s countdown) showed the visible countdown ticking correctly (10, 9, 8...) - already working.
- A real test of a session with type "practice" (the user's own guess at the cause - "träning", Swedish for practice) showed the *opposite* of the original complaint: the entire ceremony, audio included, was skipped in ~25ms and the session jumped straight to "running" - not "audio plays but visual doesn't move", but "nothing at all happens, instantly."
That second finding was real and worth fixing on its own even though it didn't match the original wording: confirmed with the user that a "Practice" session used as a scheduled slot in a structured race/sponsor event's session plan should get the same countdown/lights/start-tone ceremony as qualifying/heat/final - only the standalone "Quick Open Practice" shortcut (and "free_practice", both rolling/best-lap-ranked with no fixed roster) should still start instantly.
- [x] `startSessionWithAudio` (`src/app.js`) narrowed its ceremony-skip check from `["open_practice", "free_practice", "practice"]` to just `["open_practice", "free_practice"]`. "practice" sessions (built by the Race Setup wizard's own practice-session step, already ranked like a real timed session rather than rolling/best-lap like the other two practice types) now run the full `runStartSequence`.
Verified 2026-09-10: extended the existing permanent browser test (`tests/browser_race_day_start_sequences.js`) with a "practice"-type session run through the same full-ceremony assertions already used for qualification/final/team_race (real audio events logged, pause/resume/finish all work), plus a new explicit check that "open_practice" (the Quick Open Practice shortcut) still jumps straight to running with zero new audio events logged - locking in both sides of the distinction so this doesn't regress either direction. Full unit and browser regression suites re-run clean.
## Documentation maintenance
- [ ] Update the architecture's module/dependency inventory as modules change.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "rc-racecore",
"version": "1.16.0",
"version": "1.16.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rc-racecore",
"version": "1.16.0",
"version": "1.16.1",
"hasInstallScript": true,
"dependencies": {
"better-sqlite3": "^12.11.1",

View File

@@ -1,6 +1,6 @@
{
"name": "rc-racecore",
"version": "1.16.0",
"version": "1.16.1",
"private": true,
"description": "RC RaceCore live timing and race control with AMMC ingest and SQLite persistence",
"engines": {

View File

@@ -199,11 +199,17 @@ const announceRaceClockEvent = (eventKey, session) => triggerRaceAudio(eventKey,
const handleSessionTimerTick = () => handleSessionTimerTickHelper({ getActiveSession, isUntimedSession, getSessionTiming, saveState, getLastFinishAnnouncementSessionId: () => lastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId: (value) => { lastFinishAnnouncementSessionId = value; }, announceRaceFinished, announceRaceClockEvent });
const cancelRaceStart = () => raceAudioEngine.cancelStartSequence();
const startSessionWithAudio = (session, callbacks) => {
const practice = ["open_practice", "free_practice", "practice"].includes(String(session?.type || ""));
// Only the casual, come-and-go practice modes (open_practice - the standalone "Quick
// Open Practice" shortcut - and free_practice, both rolling/best-lap ranked with no
// fixed roster) skip the race-start ceremony. "practice" is a proper scheduled slot in
// a structured event's session plan (built by the Race Setup wizard alongside
// qualifying/finals, and already ranked like a real timed session, not rolling) and
// gets the same countdown/lights/start-tone ceremony as qualifying/heat/final.
const skipCeremony = ["open_practice", "free_practice"].includes(String(session?.type || ""));
const staggerOrder = getSessionGridOrder(session)
.map((driverId) => state.drivers.find((driver) => driver.id === driverId)?.transponder)
.filter(Boolean);
return raceAudioEngine.runStartSequence(session, { ...callbacks, skipAudio: practice, staggerOrder });
return raceAudioEngine.runStartSequence(session, { ...callbacks, skipAudio: skipCeremony, staggerOrder });
};
const tickClock = () => tickClockHelper({ dom, currentLanguage, handleSessionTimerTick, getActiveSession, getCurrentView: () => currentView, getOverlayMode: () => overlayMode, renderView, refreshTimingDynamics, updateHeaderState });
const buildTeamRaceResultsHtml = (event) => buildTeamRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, buildTeamRaceStandings, renderTable, formatLap, renderTeamStintLog, renderSessionChannelAnalysis });

View File

@@ -2,11 +2,13 @@ import assert from "node:assert/strict";
import { startFakeDecoder } from "./browser_fake_decoder.js";
import { RACE_AUDIO_EVENT_DEFINITIONS } from "../src/race_audio.js";
// Practice types skip the countdown/light/audio sequence entirely (see
// startSessionWithAudio in src/app.js) - already covered by browser_race_day.js.
// heat/qualification/final/team_race all run the real sequence in src/race_audio.js's
// runStartSequence, gating the session's transition to "running" until it completes -
// this file exercises that for each of those four types, plus pause/resume/finish.
// Only the casual, come-and-go practice modes (open_practice - the standalone "Quick Open
// Practice" shortcut - and free_practice) skip the countdown/light/audio sequence entirely
// (see startSessionWithAudio in src/app.js). "practice" is a proper scheduled slot in a
// structured event's session plan and, like heat/qualification/final/team_race, runs the
// real sequence in src/race_audio.js's runStartSequence, gating the session's transition
// to "running" until it completes - this file exercises that for each of those five types,
// plus pause/resume/finish, and separately confirms open_practice still skips it.
//
// Every effect/voice is silenced via raceAudio.events so the sequence's own audio
// playback (real <audio> elements, speechSynthesis) never has to actually complete in a
@@ -51,6 +53,8 @@ export async function runRaceDayStartSequencesSmoke({ evaluate, navigate, waitFo
{ id: "rdts_quali", eventId: event.id, name: "RDTS Qualification", type: "qualification", durationMin: 5, status: "ready" },
{ id: "rdts_final", eventId: event.id, name: "RDTS Final", type: "final", durationMin: 5, status: "ready" },
{ id: "rdts_team_race", eventId: event.id, name: "RDTS Team Race", type: "team_race", durationMin: 5, status: "ready" },
{ id: "rdts_practice", eventId: event.id, name: "RDTS Practice", type: "practice", durationMin: 5, status: "ready" },
{ id: "rdts_open_practice", eventId: event.id, name: "RDTS Open Practice", type: "open_practice", durationMin: 5, status: "ready" },
];
try {
@@ -112,6 +116,7 @@ export async function runRaceDayStartSequencesSmoke({ evaluate, navigate, waitFo
[sessions[1], ["start_sequence", "qualifying_start", "drivers_ready"], "qualification"],
[sessions[2], ["start_sequence", "drivers_ready"], "final"],
[sessions[3], ["start_sequence", "drivers_ready"], "team_race"],
[sessions[4], ["start_sequence", "drivers_ready"], "practice"],
]) {
await patchState({ activeSessionId: session.id });
await navigate("race_day", "raceDayStart");
@@ -129,6 +134,20 @@ export async function runRaceDayStartSequencesSmoke({ evaluate, navigate, waitFo
console.log(`PASS ${label} runs the real start sequence (not the practice shortcut) and supports pause/resume/finish`);
}
// --- open_practice: the standalone "Quick Open Practice" shortcut still skips the
// ceremony entirely and jumps straight to running, unlike the scheduled "practice"
// type just proven above. ---
const beforeOpenPracticeLog = (await savedState()).audioEventLog.length;
await patchState({ activeSessionId: sessions[5].id });
await navigate("race_day", "raceDayStart");
await waitFor(async () => (await buttonDisabled("raceDayStart")) === false, "open_practice preflight ready");
await click("raceDayStart");
await waitFor(async () => (await findSession(sessions[5].id)).status === "running", "open_practice jumps straight to running", 5000);
const afterOpenPracticeLog = (await savedState()).audioEventLog;
const newOpenPracticeEntries = afterOpenPracticeLog.slice(0, afterOpenPracticeLog.length - beforeOpenPracticeLog);
assert.deepEqual(newOpenPracticeEntries.map((entry) => entry.event), [], "open_practice still skips the whole ceremony - no new audio events logged: " + JSON.stringify(newOpenPracticeEntries.map((e) => e.event)));
console.log("PASS open_practice (Quick Open Practice shortcut) still skips the countdown/lights/audio ceremony entirely");
assert.deepEqual(dialogs, [], "no unexpected native dialog during the start-sequence flow");
} finally {
const cleanup = await savedState();