Reported by the user: creating a team, adding the wrong driver, then moving that driver to another team by ticking them on the new team's edit form left the driver on both teams at once. Root cause: there was never a "move" action - the edit form only ever added a driver to the team being saved, never removed them from wherever they already were. Live laps from a driver on two teams were then silently credited to only the first-matching team, with no clear error. reassignTeamMembers (event_race_logic.js) now strips a driver/car being saved onto one team out of every other team in the same event, and the team checkbox list shows which team a driver is already on before you move them. Also fixed two related gaps found while investigating, at the user's request to fix everything: getTeamDriverPool had regressed to ignoring the event's class/participant scoping entirely (showing every driver in the club), and deleting a driver or car left them behind in team rosters forever. Added a duplicate_team_members Race Day preflight check as defense in depth for any already-corrupted saved state. Verified with new unit tests for reassignTeamMembers, getTeamDriverPool and the new preflight check, plus a new permanent browser test reproducing the user's exact reported scenario end to end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
114 lines
6.1 KiB
JavaScript
114 lines
6.1 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { buildRaceDayPreflight, buildRaceDayQueue } from "../src/race_day_logic.js";
|
|
|
|
function createState() {
|
|
return {
|
|
activeSessionId: "session_1",
|
|
decoder: { connected: true, splitConnected: true, pitEntryConnected: true, pitExitConnected: true },
|
|
settings: { splitDecoderEnabled: false, pitDecoderEnabled: false, raceAudio: { enabled: true } },
|
|
classes: [{ id: "class_1", name: "2WD" }],
|
|
drivers: [
|
|
{ id: "driver_1", classId: "class_1", name: "One", transponder: "111" },
|
|
{ id: "driver_2", classId: "class_1", name: "Two", transponder: "222" },
|
|
],
|
|
cars: [],
|
|
events: [{
|
|
id: "event_1",
|
|
mode: "race",
|
|
classId: "class_1",
|
|
raceConfig: { participantsConfigured: true, driverIds: ["driver_1", "driver_2"], minLapMs: 10000, maxLapMs: 60000 },
|
|
}],
|
|
sessions: [
|
|
{ id: "session_1", eventId: "event_1", name: "Q1", mode: "race", type: "qualification", status: "ready", startMode: "mass", driverIds: ["driver_1", "driver_2"] },
|
|
{ id: "session_2", eventId: "event_1", name: "Q2", mode: "race", type: "qualification", status: "ready", startMode: "mass", driverIds: ["driver_1", "driver_2"] },
|
|
{ id: "session_3", eventId: "event_1", name: "A final", mode: "race", type: "final", status: "ready", startMode: "position", driverIds: ["driver_1", "driver_2"], manualGridIds: ["driver_1", "driver_2"] },
|
|
],
|
|
};
|
|
}
|
|
|
|
test("Race Day queue follows the selected event session order", () => {
|
|
const queue = buildRaceDayQueue(createState());
|
|
assert.equal(queue.current.id, "session_1");
|
|
assert.equal(queue.next.id, "session_2");
|
|
assert.equal(queue.afterNext.id, "session_3");
|
|
});
|
|
|
|
test("Race Day preflight accepts a complete race session", () => {
|
|
const state = createState();
|
|
const result = buildRaceDayPreflight(state, state.sessions[0], { backendAvailable: true });
|
|
assert.equal(result.ready, true);
|
|
assert.ok(result.checks.every((item) => item.level === "ok"));
|
|
});
|
|
|
|
test("Race Day preflight blocks decoder, transponder, duplicate and grid errors", () => {
|
|
const state = createState();
|
|
state.decoder.connected = false;
|
|
state.drivers[1].transponder = "111";
|
|
state.sessions[2].manualGridIds = ["driver_1"];
|
|
const result = buildRaceDayPreflight(state, state.sessions[2], { backendAvailable: true });
|
|
assert.equal(result.ready, false);
|
|
const levels = new Map(result.checks.map((item) => [item.id, item.level]));
|
|
assert.equal(levels.get("decoder"), "error");
|
|
assert.equal(levels.get("duplicate_transponders"), "error");
|
|
assert.equal(levels.get("grid"), "error");
|
|
});
|
|
|
|
test("open practice allows an unset lap window but still requires the main decoder", () => {
|
|
const state = createState();
|
|
state.events[0].raceConfig.minLapMs = 0;
|
|
state.events[0].raceConfig.maxLapMs = 0;
|
|
const practice = { ...state.sessions[0], type: "open_practice", driverIds: [] };
|
|
let result = buildRaceDayPreflight(state, practice, { backendAvailable: true });
|
|
assert.equal(result.ready, true);
|
|
assert.equal(result.checks.find((item) => item.id === "lap_window").level, "warning");
|
|
state.decoder.connected = false;
|
|
result = buildRaceDayPreflight(state, practice, { backendAvailable: true });
|
|
assert.equal(result.ready, false);
|
|
});
|
|
|
|
test("team_race preflight blocks a driver saved onto two teams at once, naming them instead of a generic transponder error", () => {
|
|
const state = createState();
|
|
state.sessions.push({ id: "session_team", eventId: "event_1", name: "Team Race", mode: "race", type: "team_race", status: "ready", startMode: "mass" });
|
|
state.events[0].raceConfig.teams = [
|
|
{ id: "team_a", name: "Team A", driverIds: ["driver_1"], carIds: [] },
|
|
{ id: "team_b", name: "Team B", driverIds: ["driver_1", "driver_2"], carIds: [] },
|
|
];
|
|
const result = buildRaceDayPreflight(state, state.sessions.find((s) => s.id === "session_team"), { backendAvailable: true });
|
|
assert.equal(result.ready, false);
|
|
const duplicateCheck = result.checks.find((item) => item.id === "duplicate_team_members");
|
|
assert.equal(duplicateCheck.level, "error");
|
|
assert.deepEqual(duplicateCheck.duplicateTeamMemberNames, ["One"]);
|
|
});
|
|
|
|
test("team_race preflight accepts teams with no members in common", () => {
|
|
const state = createState();
|
|
state.sessions.push({ id: "session_team", eventId: "event_1", name: "Team Race", mode: "race", type: "team_race", status: "ready", startMode: "mass" });
|
|
state.events[0].raceConfig.teams = [
|
|
{ id: "team_a", name: "Team A", driverIds: ["driver_1"], carIds: [] },
|
|
{ id: "team_b", name: "Team B", driverIds: ["driver_2"], carIds: [] },
|
|
];
|
|
const result = buildRaceDayPreflight(state, state.sessions.find((s) => s.id === "session_team"), { backendAvailable: true });
|
|
assert.equal(result.checks.find((item) => item.id === "duplicate_team_members").level, "ok");
|
|
});
|
|
|
|
test("preflight presents blockers before warnings without changing the original checks", async () => {
|
|
const { sortRaceDayChecks } = await import("../src/race_day_view.js");
|
|
const checks = [{ id: "audio", level: "warning" }, { id: "session", level: "ok" }, { id: "decoder", level: "error" }];
|
|
assert.deepEqual(sortRaceDayChecks(checks).map((item) => item.id), ["decoder", "audio", "session"]);
|
|
assert.equal(checks[0].id, "audio");
|
|
});
|
|
|
|
test("preflight shortcuts respect driver, shared-car and team setup", async () => {
|
|
const { getRaceDayCheckAction: action } = await import("../src/race_day_view.js");
|
|
const missing = { id: "transponders", level: "error" };
|
|
assert.equal(action(missing, { mode: "race", type: "qualification" }).view, "drivers");
|
|
assert.equal(action(missing, { mode: "track", type: "heat" }).view, "cars");
|
|
assert.equal(action(missing, { mode: "race", type: "team_race" }).view, "race_setup");
|
|
assert.equal(action({ id: "participants", level: "error" }, { mode: "track" }).view, "events");
|
|
assert.equal(action({ id: "decoder", level: "error" }).view, "settings");
|
|
assert.equal(action({ id: "session", level: "error" }).view, "timing");
|
|
assert.equal(action({ id: "lap_window", level: "warning" }, { mode: "race" }).field, "minLapSec");
|
|
assert.equal(action({ id: "decoder", level: "ok" }), null);
|
|
});
|