Fix Team Race duplicate-membership bug and two related team-roster gaps

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>
This commit is contained in:
larssand
2026-09-10 17:07:45 +02:00
parent 71b3451b13
commit 15b574a37d
16 changed files with 261 additions and 15 deletions

View File

@@ -8,7 +8,9 @@ import {
buildRaceSessionsFromWizard,
generateFinalsForRace,
generateQualifyingForRace,
getTeamDriverPool,
normalizeEvent,
reassignTeamMembers,
reseedUpcomingQualifying,
} from "../src/event_race_logic.js";
import {
@@ -135,6 +137,43 @@ test("event normalization preserves the practice-only marker", () => {
assert.equal(event.raceConfig.presetId, "custom");
});
test("reassignTeamMembers strips a moved driver/car from every other team, leaving the excluded team untouched", () => {
const teams = [
{ id: "team_a", name: "A", driverIds: ["d1", "d2"], carIds: ["c1"] },
{ id: "team_b", name: "B", driverIds: ["d3"], carIds: [] },
];
const result = reassignTeamMembers(teams, { excludeTeamId: "team_b", driverIds: ["d1"], carIds: ["c1"] });
const teamA = result.find((team) => team.id === "team_a");
const teamB = result.find((team) => team.id === "team_b");
assert.deepEqual(teamA.driverIds, ["d2"], "d1 was moved out of team A");
assert.deepEqual(teamA.carIds, [], "c1 was moved out of team A too");
assert.deepEqual(teamB.driverIds, ["d3"], "the team being saved is left for the caller to update, not touched here");
});
test("reassignTeamMembers returns unchanged team objects as-is when nothing overlaps", () => {
const teams = [{ id: "team_a", name: "A", driverIds: ["d1"], carIds: [] }];
const result = reassignTeamMembers(teams, { excludeTeamId: "team_b", driverIds: ["d9"], carIds: [] });
assert.strictEqual(result[0], teams[0]);
});
test("getTeamDriverPool scopes to the event's class/participants, only falling back to every driver when that list is empty", () => {
const state = {
drivers: [
{ id: "d1", name: "One", classId: "2wd" },
{ id: "d2", name: "Two", classId: "4wd" },
],
};
const getEventDrivers = (event) => state.drivers.filter((driver) => !event?.classId || driver.classId === event.classId);
const scoped = getTeamDriverPool({ classId: "2wd", raceConfig: {} }, { getEventDrivers, state });
assert.deepEqual(scoped.drivers.map((driver) => driver.id), ["d1"]);
assert.equal(scoped.fallback, false);
const noMatches = getTeamDriverPool({ classId: "unknown_class", raceConfig: {} }, { getEventDrivers, state });
assert.deepEqual(noMatches.drivers.map((driver) => driver.id), ["d1", "d2"], "falls back to every driver rather than leaving zero checkboxes");
assert.equal(noMatches.fallback, true);
});
test("race wizard creates configured practice, qualifying, and endurance sessions", () => {
const event = {
id: "event_1",