import test from "node:test"; import assert from "node:assert/strict"; import { buildDriverActivePracticeStats, buildEvenLapsProgress, buildStepTrainingProgress, buildDriverTrainingStats, buildDriverTimingChannelStats, buildTrainingAchievements, buildTrainingHighlights, buildTrainingMarkerComparisons, buildTrainingMissions, buildTrainingPaceBuckets, buildTrainingRecommendation, buildTrainingStintAnalysis, buildTrainingRaceSessions, buildTrainingWeekdayStats, countExerciseSessions, countDriverExerciseSessions, getBestConsecutiveTrainingLaps, getBestTrainingDurationResult, getDriverTrainingLaps, getDriverTrainingLapsForCurrentLayout, getDriverTrainingLapsForLayout, getNextTrainingMilestone, getClubTrainingGoalProgress, getTrainingGoalProgress, normalizeTrainingState, splitTrainingStints, syncTrainingLedger, } from "../src/training_logic.js"; import { getDriverProfileGroups } from "../src/driver_profile_logic.js"; function createUid() { let index = 0; return (prefix) => `${prefix}_${++index}`; } test("normalization removes invalid and duplicate training records", () => { const normalized = normalizeTrainingState({ goals: [ { id: "goal_1", driverId: "driver_1", targetLaps: 500, title: " Bronze ", reward: " Badge " }, { id: "goal_invalid", driverId: "", targetLaps: 100 }, { id: "goal_sessions", driverId: "driver_1", classId: "class_1", metric: "sessions", exerciseKind: "stepTraining", targetSessions: 10, title: " Coffee mug " }, { id: "goal_sessions_no_kind", driverId: "driver_1", metric: "sessions", targetSessions: 10 }, ], exerciseSessions: [ { id: "ex_1", driverId: "driver_1", classId: "class_1", kind: "stepTraining", startedAt: 1000, completedAt: "2026-09-09T10:00:00.000Z", summary: { stepsCompleted: 5 } }, { id: "ex_duplicate", driverId: "driver_1", classId: "class_1", kind: "stepTraining", startedAt: 1000, completedAt: "2026-09-09T10:05:00.000Z", summary: {} }, { id: "ex_invalid_kind", driverId: "driver_1", classId: "class_1", kind: "unknown", startedAt: 2000 }, { id: "ex_no_driver", driverId: "", classId: "class_1", kind: "evenLaps", startedAt: 3000 }, ], ledger: [ { id: "lap_1", sourceKey: "source_1", driverId: "driver_1", lapMs: 16000, timestamp: 1000 }, { id: "lap_duplicate", sourceKey: "source_1", driverId: "driver_1", lapMs: 17000, timestamp: 2000 }, { id: "lap_short", sourceKey: "source_2", driverId: "driver_1", lapMs: 500, timestamp: 3000 }, { id: "lap_anonymous", sourceKey: "source_3", driverId: "", lapMs: 18000, timestamp: 4000 }, ], shares: [ { driverId: "driver_1", token: "a".repeat(24), enabled: true, visibility: { history: false } }, { driverId: "driver_1", token: "b".repeat(24), enabled: true }, { driverId: "driver_2", token: "short", enabled: true }, ], markers: [ { id: "marker_1", driverId: "driver_1", classId: "class_1", timestamp: 5000, type: "tires", note: " New tires " }, { id: "marker_invalid", driverId: "", classId: "class_1", timestamp: 5000 }, ], clubGoal: { id: "club_goal_1", title: " Club to 10000 ", reward: " Pizza ", targetLaps: 10000, baselineLaps: 120, }, }, { uid: createUid() }); assert.equal(normalized.goals.length, 2, "the sessions goal without an exerciseKind is dropped, same as a laps goal with no target"); assert.equal(normalized.goals[0].title, "Bronze"); assert.equal(normalized.goals[0].reward, "Badge"); assert.equal(normalized.goals[0].metric, "laps"); const sessionsGoal = normalized.goals.find((goal) => goal.id === "goal_sessions"); assert.equal(sessionsGoal.metric, "sessions"); assert.equal(sessionsGoal.exerciseKind, "stepTraining"); assert.equal(sessionsGoal.targetSessions, 10); assert.equal(normalized.exerciseSessions.length, 1, "the duplicate same driver/class/kind/startedAt session, the invalid kind, and the driver-less entry are all dropped"); assert.equal(normalized.exerciseSessions[0].id, "ex_1"); assert.deepEqual(normalized.exerciseSessions[0].summary, { stepsCompleted: 5 }); assert.deepEqual(normalized.ledger.map((lap) => lap.id), ["lap_1"]); assert.deepEqual(normalized.shares.map((share) => share.token), ["a".repeat(24)]); assert.deepEqual(normalized.shares[0].visibility, { progress: true, achievements: true, performance: true, goals: true, history: false }); assert.equal(normalized.markers.length, 1); assert.equal(normalized.markers[0].note, "New tires"); assert.equal(normalized.clubGoal.title, "Club to 10000"); assert.equal(normalized.clubGoal.reward, "Pizza"); assert.equal(normalized.clubGoal.baselineLaps, 120); }); test("a club goal can also target completed sessions of one exercise kind, club-wide", () => { const withSessions = normalizeTrainingState({ clubGoal: { title: "Mug run", reward: "Mug", metric: "sessions", exerciseKind: "evenLaps", targetSessions: 50, baselineSessions: 5 }, }, { uid: createUid() }); assert.equal(withSessions.clubGoal.metric, "sessions"); assert.equal(withSessions.clubGoal.exerciseKind, "evenLaps"); assert.equal(withSessions.clubGoal.targetSessions, 50); assert.equal(withSessions.clubGoal.baselineSessions, 5); const missingKind = normalizeTrainingState({ clubGoal: { title: "Invalid", metric: "sessions", targetSessions: 50 }, }, { uid: createUid() }); assert.equal(missingKind.clubGoal, null, "a sessions club goal without an exerciseKind is dropped, same as a laps goal with no target"); }); test("ledger sync retains archived laps and rebuilds current session laps", () => { const training = { goals: [{ id: "goal_1", driverId: "driver_1", targetLaps: 100 }], shares: [{ driverId: "driver_1", token: "s".repeat(24), enabled: true }], ledger: [ { id: "archived", sourceKey: "old:1", sessionId: "old", driverId: "driver_1", lapMs: 19000, timestamp: 1000 }, { id: "stale", sourceKey: "live:old", sessionId: "live", driverId: "driver_1", lapMs: 30000, timestamp: 2000 }, ], markers: [{ id: "marker_1", driverId: "driver_1", classId: "class_1", timestamp: 2500, type: "setup" }], }; const state = { sessions: [{ id: "live", name: "Free Practice", eventId: "event_1" }], events: [{ id: "event_1", name: "Club practice" }], resultsBySession: { live: { passings: [ { driverId: "driver_1", competitorKey: "driver_1", lapMs: 16500, timestamp: 3000, validLap: true, validationWarning: "above_max_stint", excludedFromPace: true, stintBoundary: true }, { driverId: "driver_1", competitorKey: "driver_1", lapMs: 9000, timestamp: 4000, validLap: false }, { driverId: "", competitorKey: "anonymous", lapMs: 17000, timestamp: 5000, validLap: true }, { driverId: "driver_1", competitorKey: "driver_1", lapMs: 500, timestamp: 6000, validLap: true }, ], }, }, }; const synced = syncTrainingLedger(training, state, { uid: createUid() }); assert.equal(synced.ledger.length, 2); assert.equal(synced.ledger.some((lap) => lap.id === "archived"), true); assert.equal(synced.ledger.some((lap) => lap.id === "stale"), false); const liveLap = synced.ledger.find((lap) => lap.sessionId === "live"); assert.equal(liveLap.lapMs, 16500); assert.equal(liveLap.sessionName, "Free Practice"); assert.equal(liveLap.eventName, "Club practice"); assert.equal(liveLap.excludedFromPace, true); assert.equal(liveLap.stintBoundary, true); assert.equal(synced.goals.length, 1); assert.equal(synced.shares.length, 1); assert.equal(synced.markers.length, 1); }); test("resetting a running Open Practice keeps its earlier laps in the training stats", () => { const lap = (lapMs, timestamp) => ({ driverId: "driver_1", competitorKey: "driver_1", lapMs, timestamp, validLap: true }); const state = { sessions: [{ id: "op", name: "Open Practice", type: "open_practice", eventId: "event_1" }], events: [{ id: "event_1", name: "Club" }], resultsBySession: { op: { passings: [lap(20000, 1000), lap(19500, 2000), lap(19000, 3000)] } }, }; let training = syncTrainingLedger({}, state, { uid: createUid() }); assert.equal(training.ledger.length, 3); // "Reset data" on Timing: the session keeps running, its results are cleared. state.sessions[0].resultsResetAt = 5000; delete state.resultsBySession.op; training = syncTrainingLedger(training, state, { uid: createUid() }); assert.equal(training.ledger.length, 3, "nothing is lost right after the reset"); // The next day's first lap brings the session back into the results. This used to wipe // every lap from before the reset out of the training stats. state.resultsBySession.op = { passings: [lap(18500, 90000)] }; training = syncTrainingLedger(training, state, { uid: createUid() }); assert.deepEqual(training.ledger.map((entry) => entry.lapMs), [20000, 19500, 19000, 18500]); // A lap removed from the current results (judge invalidated it) still leaves the stats. state.resultsBySession.op.passings[0].validLap = false; training = syncTrainingLedger(training, state, { uid: createUid() }); assert.deepEqual(training.ledger.map((entry) => entry.lapMs), [20000, 19500, 19000]); }); test("driver statistics remain isolated and aggregate days and sessions", () => { const firstDay = new Date(2026, 0, 10, 12, 0, 0).getTime(); const secondDay = new Date(2026, 0, 11, 12, 0, 0).getTime(); const state = { training: { ledger: [ { driverId: "driver_1", sessionId: "practice_1", timestamp: firstDay, lapMs: 18000 }, { driverId: "driver_1", sessionId: "practice_1", timestamp: firstDay + 20000, lapMs: 16500 }, { driverId: "driver_1", sessionId: "practice_2", timestamp: secondDay, lapMs: 17000 }, { driverId: "driver_2", sessionId: "practice_2", timestamp: secondDay + 20000, lapMs: 12000 }, ], }, }; assert.equal(getDriverTrainingLaps(state, "driver_1").length, 3); const stats = buildDriverTrainingStats(state, "driver_1"); assert.equal(stats.totalLaps, 3); assert.equal(stats.totalSessions, 2); assert.equal(stats.trainingDays, 2); assert.equal(stats.bestLapMs, 16500); assert.equal(stats.averageLapMs, 17167); // A flow streak never carries over into another session or stint. assert.equal(stats.controlledLapStreak, 1); assert.equal(stats.dailySummaries[0].laps, 1); assert.equal(stats.dailySummaries[1].laps, 2); }); test("driver pace/PB/consistency stats only compare laps from the current track layout, but lifetime totals still count every lap", () => { const oldLayoutDay = new Date(2026, 0, 1, 12, 0, 0).getTime(); const newLayoutDay = new Date(2026, 2, 1, 12, 0, 0).getTime(); const state = { trackLayouts: [{ id: "layout_1", label: "Layout 1", startedAt: new Date(2026, 0, 1).getTime() }, { id: "layout_2", label: "Layout 2", startedAt: new Date(2026, 1, 1).getTime() }], training: { ledger: [ // Old layout: much faster (e.g. a shorter loop) - must not count as today's record. { driverId: "driver_1", sessionId: "old_session", timestamp: oldLayoutDay, lapMs: 10_000, trackLayoutId: "layout_1" }, { driverId: "driver_1", sessionId: "old_session", timestamp: oldLayoutDay + 11_000, lapMs: 10_500, trackLayoutId: "layout_1" }, // Current layout: slower, genuinely the best time achievable on today's track. { driverId: "driver_1", sessionId: "new_session", timestamp: newLayoutDay, lapMs: 18_000, trackLayoutId: "layout_2" }, { driverId: "driver_1", sessionId: "new_session", timestamp: newLayoutDay + 19_000, lapMs: 17_500, trackLayoutId: "layout_2" }, ], }, }; const layoutLaps = getDriverTrainingLapsForCurrentLayout(state, "driver_1"); assert.equal(layoutLaps.length, 2, "only the current layout's laps"); assert.ok(layoutLaps.every((lap) => lap.trackLayoutId === "layout_2")); const stats = buildDriverTrainingStats(state, "driver_1"); assert.equal(stats.totalLaps, 4, "lifetime total still counts every lap ever driven"); assert.equal(stats.totalSessions, 2, "lifetime session count is unaffected by layout scoping"); assert.equal(stats.trainingDays, 2, "training-day history is unaffected by layout scoping"); assert.equal(stats.bestLapMs, 17_500, "best lap only compares laps from today's layout, not the faster old-layout lap"); }); test("with no track layouts configured, layout scoping is a no-op (every lap has trackLayoutId \"\")", () => { const state = { training: { ledger: [ { driverId: "driver_1", sessionId: "s1", timestamp: 1000, lapMs: 15_000 }, { driverId: "driver_1", sessionId: "s1", timestamp: 16_000, lapMs: 14_000 }, ], }, }; assert.deepEqual(getDriverTrainingLapsForCurrentLayout(state, "driver_1"), getDriverTrainingLaps(state, "driver_1")); assert.equal(buildDriverTrainingStats(state, "driver_1").bestLapMs, 14_000); }); test("buildTrainingStintAnalysis's best-stint pick is scoped to the current track layout", () => { const state = { trackLayouts: [{ id: "layout_1", label: "Layout 1", startedAt: 0 }, { id: "layout_2", label: "Layout 2", startedAt: 50_000 }], training: { ledger: [ { driverId: "driver_1", sourceId: "tp_1", sessionId: "fast_old", timestamp: 10_000, lapMs: 10_000, trackLayoutId: "layout_1" }, { driverId: "driver_1", sourceId: "tp_1", sessionId: "fast_old", timestamp: 20_000, lapMs: 10_000, trackLayoutId: "layout_1" }, { driverId: "driver_1", sourceId: "tp_1", sessionId: "fast_old", timestamp: 30_000, lapMs: 10_000, trackLayoutId: "layout_1" }, { driverId: "driver_1", sourceId: "tp_1", sessionId: "slower_new", timestamp: 100_000, lapMs: 16_000, trackLayoutId: "layout_2" }, { driverId: "driver_1", sourceId: "tp_1", sessionId: "slower_new", timestamp: 116_000, lapMs: 16_000, trackLayoutId: "layout_2" }, { driverId: "driver_1", sourceId: "tp_1", sessionId: "slower_new", timestamp: 132_000, lapMs: 16_000, trackLayoutId: "layout_2" }, ], }, }; const { rows, best } = buildTrainingStintAnalysis(state, "driver_1"); assert.equal(rows.length, 1, "the old-layout stint is excluded entirely"); assert.equal(rows[0].sessionId, "slower_new"); assert.equal(best.sessionId, "slower_new", "the faster old-layout stint is never picked as \"best\""); }); test("counted stint-boundary laps do not distort driver pace averages", () => { const state = { training: { ledger: [ { driverId: "driver_1", sourceId: "tp_1", sessionId: "practice", timestamp: 37_000, lapMs: 37_000 }, { driverId: "driver_1", sourceId: "tp_1", sessionId: "practice", timestamp: 74_000, lapMs: 37_000 }, { driverId: "driver_1", sourceId: "tp_1", sessionId: "practice", timestamp: 164_000, lapMs: 90_000, excludedFromPace: true, stintBoundary: true }, ], }, }; const stats = buildDriverTrainingStats(state, "driver_1"); assert.equal(stats.totalLaps, 3); assert.equal(stats.bestLapMs, 37_000); assert.equal(stats.averageLapMs, 37_000); assert.equal(stats.stintCount, 2); }); test("split and pit statistics aggregate by personal profile and selected class", () => { const state = { drivers: [ { id: "driver_2wd_a", name: "Driver", profileId: "profile_1", classId: "class_2wd" }, { id: "driver_2wd_b", name: "Driver", profileId: "profile_1", classId: "class_2wd" }, { id: "driver_4wd", name: "Driver", profileId: "profile_1", classId: "class_4wd" }, ], sessions: [ { id: "session_1", name: "Practice", eventId: "event_1" }, { id: "session_2", name: "Endurance", eventId: "event_1" }, ], events: [{ id: "event_1", name: "Club day" }], resultsBySession: { session_1: { splits: [ { driverId: "driver_2wd_a", splitMs: 8100, timestamp: 1000 }, { driverId: "driver_2wd_b", splitMs: 7900, timestamp: 2000 }, { driverId: "driver_4wd", splitMs: 6500, timestamp: 3000 }, ], pitStops: [], }, session_2: { splits: [], pitStops: [ { driverId: "driver_2wd_a", pitStopMs: 12500, enteredAt: 4000 }, { driverId: "driver_2wd_b", pitStopMs: 9500, enteredAt: 5000 }, { driverId: "driver_4wd", pitStopMs: 7000, enteredAt: 6000 }, ], }, }, }; const stats = buildDriverTimingChannelStats(state, "profile_1", "class_2wd"); assert.equal(stats.splitCount, 2); assert.equal(stats.bestSplitMs, 7900); assert.equal(stats.averageSplitMs, 8000); assert.equal(stats.latestSplitGainMs, 200); assert.equal(stats.pitStopCount, 2); assert.equal(stats.totalPitMs, 22000); assert.equal(stats.bestPitStopMs, 9500); assert.equal(stats.recentPitStops[0].sessionName, "Endurance"); }); test("one driver profile combines transponders within a class but keeps 2WD and 4WD separate", () => { const state = { drivers: [ { id: "driver_2wd_a", name: "Same Driver", profileId: "profile_same", classId: "class_2wd" }, { id: "driver_2wd_b", name: "Same Driver", profileId: "profile_same", classId: "class_2wd" }, { id: "driver_4wd", name: "Same Driver", profileId: "profile_same", classId: "class_4wd" }, ], training: { ledger: [ { driverId: "driver_2wd_a", sessionId: "practice", timestamp: 10_000, lapMs: 18_000 }, { driverId: "driver_2wd_b", sessionId: "practice", timestamp: 30_000, lapMs: 17_500 }, { driverId: "driver_4wd", sessionId: "practice", timestamp: 50_000, lapMs: 15_000 }, ], }, }; const twoWheelDrive = buildDriverTrainingStats(state, "profile_same", "class_2wd"); const fourWheelDrive = buildDriverTrainingStats(state, "profile_same", "class_4wd"); assert.equal(twoWheelDrive.totalLaps, 2); assert.equal(twoWheelDrive.bestLapMs, 17_500); assert.equal(fourWheelDrive.totalLaps, 1); assert.equal(fourWheelDrive.bestLapMs, 15_000); }); test("legacy same-name entries share a profile while an explicit profile can remain separate", () => { const groups = getDriverProfileGroups({ drivers: [ { id: "legacy_2wd", name: "Test Driver", classId: "2wd", club: "JMK RB", country: "Sverige" }, { id: "legacy_4wd", name: " test driver ", classId: "4wd" }, { id: "separate", name: "Test Driver", profileId: "profile_other", classId: "2wd" }, ], }); assert.equal(groups.length, 2); assert.equal(groups.find((group) => group.id.startsWith("legacy:"))?.drivers.length, 2); assert.equal(groups.find((group) => group.id.startsWith("legacy:"))?.club, "JMK RB"); assert.equal(groups.find((group) => group.id.startsWith("legacy:"))?.country, "Sverige"); assert.equal(groups.find((group) => group.id === "profile_other")?.drivers.length, 1); }); test("active practice profile statistics include only linked entries in the selected class", () => { const state = { activeSessionId: "practice", drivers: [ { id: "two_a", name: "Driver", profileId: "profile", classId: "2wd" }, { id: "two_b", name: "Driver", profileId: "profile", classId: "2wd" }, { id: "four", name: "Driver", profileId: "profile", classId: "4wd" }, ], sessions: [{ id: "practice", name: "Open", type: "open_practice", status: "running", startedAt: 1_000 }], resultsBySession: { practice: { passings: [ { driverId: "two_a", timestamp: 20_000, lapMs: 18_000, validLap: true }, { driverId: "two_b", timestamp: 39_000, lapMs: 17_500, validLap: true }, { driverId: "four", timestamp: 55_000, lapMs: 15_000, validLap: true }, ], }, }, }; const stats = buildDriverActivePracticeStats(state, "profile", 60_000, "2wd"); assert.equal(stats.laps, 2); assert.equal(stats.bestLapMs, 17_500); }); test("active practice stats include only the selected driver's current valid laps", () => { const startedAt = 10_000; const state = { activeSessionId: "practice_1", sessions: [{ id: "practice_1", name: "Wednesday Open Practice", type: "open_practice", status: "running", startedAt }], resultsBySession: { practice_1: { passings: [ { driverId: "driver_1", timestamp: 9_000, lapMs: 15_000, validLap: true }, { driverId: "driver_1", timestamp: 20_000, lapMs: 16_000, validLap: true }, { driverId: "driver_1", timestamp: 37_000, lapMs: 17_000, validLap: true }, { driverId: "driver_1", timestamp: 45_000, lapMs: 8_000, validLap: false }, { driverId: "driver_2", timestamp: 54_000, lapMs: 16_500, validLap: true }, ], }, }, }; assert.deepEqual(buildDriverActivePracticeStats(state, "driver_1", 70_000), { sessionId: "practice_1", sessionName: "Wednesday Open Practice", startedAt, elapsedMs: 60_000, laps: 2, bestLapMs: 16_000, averageLapMs: 16_500, lastLapMs: 17_000, controlZoneMinMs: 16_000, controlZoneMaxMs: 17_280, controlledLapStreak: 2, }); }); test("active practice stats ignore races and stopped practice sessions", () => { const base = { activeSessionId: "session_1", resultsBySession: {} }; assert.equal(buildDriverActivePracticeStats({ ...base, sessions: [{ id: "session_1", type: "final", status: "running" }] }, "driver_1"), null); assert.equal(buildDriverActivePracticeStats({ ...base, sessions: [{ id: "session_1", type: "free_practice", status: "finished" }] }, "driver_1"), null); }); test("even laps progress tracks a live streak within tolerance and keeps today's best after a miss", () => { const startedAt = 10_000; const state = { activeSessionId: "practice_1", sessions: [{ id: "practice_1", name: "Evening Practice", type: "open_practice", status: "running", startedAt }], resultsBySession: { practice_1: { passings: [ { driverId: "driver_1", timestamp: 11_000, lapMs: 20_000, validLap: true }, // within target 20000 +/-500 { driverId: "driver_1", timestamp: 32_000, lapMs: 20_300, validLap: true }, // within { driverId: "driver_1", timestamp: 53_000, lapMs: 21_500, validLap: true }, // outside -> breaks streak { driverId: "driver_1", timestamp: 75_000, lapMs: 19_800, validLap: true }, // within -> new streak ], }, }, }; const config = { targetMs: 20_000, toleranceMs: 500, lapCount: 5 }; const progress = buildEvenLapsProgress(state, "driver_1", "", config, 90_000); assert.equal(progress.active, true); assert.equal(progress.currentStreak, 1, "the streak reset after the 21.5s lap and rebuilt to one lap"); assert.equal(progress.bestStreakToday, 2, "the earlier two-lap streak remains the best even after the reset"); assert.equal(progress.lastLapMs, 19_800); assert.equal(progress.lastLapWithinTolerance, true); }); test("even laps progress breaks the streak on a lap excluded from pace", () => { const startedAt = 10_000; const state = { activeSessionId: "practice_1", sessions: [{ id: "practice_1", type: "open_practice", status: "running", startedAt }], resultsBySession: { practice_1: { passings: [ { driverId: "driver_1", timestamp: 11_000, lapMs: 20_000, validLap: true }, { driverId: "driver_1", timestamp: 32_000, lapMs: 20_000, validationWarning: "above_max_stint", validLap: true }, { driverId: "driver_1", timestamp: 53_000, lapMs: 20_100, validLap: true }, ], }, }, }; const config = { targetMs: 20_000, toleranceMs: 500, lapCount: 5 }; const progress = buildEvenLapsProgress(state, "driver_1", "", config, 90_000); assert.equal(progress.currentStreak, 1, "an excluded lap in the middle still resets the streak even though its own time is within tolerance"); }); test("even laps progress without a config or an active session", () => { assert.equal(buildEvenLapsProgress({}, "driver_1", "", null), null); assert.equal(buildEvenLapsProgress({}, "driver_1", "", { targetMs: 0, toleranceMs: 500 }), null); const progress = buildEvenLapsProgress({ activeSessionId: "x", sessions: [], resultsBySession: {}, training: { ledger: [] } }, "driver_1", "", { targetMs: 20_000, toleranceMs: 500, lapCount: 5 }); assert.equal(progress.active, false); assert.equal(progress.currentStreak, 0); assert.equal(progress.bestStreakToday, 0); assert.equal(progress.lastLapMs, null); }); test("step training progress advances through steps and has nothing to compare until two controlled steps have laps", () => { const startedAt = 10_000; const state = { activeSessionId: "practice_1", sessions: [{ id: "practice_1", type: "open_practice", status: "running", startedAt }], resultsBySession: { practice_1: { passings: [ { driverId: "driver_1", timestamp: 11_000, lapMs: 25_000, validLap: true }, { driverId: "driver_1", timestamp: 37_000, lapMs: 25_500, validLap: true }, { driverId: "driver_1", timestamp: 59_000, lapMs: 21_000, validLap: true }, { driverId: "driver_1", timestamp: 81_200, lapMs: 21_200, validLap: true }, { driverId: "driver_1", timestamp: 99_200, lapMs: 18_000, validLap: true }, ], }, }, }; const config = { steps: [ { effort: "calm", lapCount: 2 }, { effort: "controlled", lapCount: 2 }, { effort: "fast", lapCount: 2 }, { effort: "controlled", lapCount: 2 }, ] }; const progress = buildStepTrainingProgress(state, "driver_1", "", config, 120_000); assert.equal(progress.active, true); assert.equal(progress.totalSteps, 4); assert.equal(progress.totalLaps, 8); assert.equal(progress.completedLaps, 5); assert.equal(progress.complete, false); assert.equal(progress.currentStepIndex, 2, "the third step (fast) is current after two calm and two controlled laps"); assert.deepEqual(progress.currentStep, { index: 2, effort: "fast", lapCount: 2, lapsCompleted: 1, lapsRemaining: 1 }); assert.equal(progress.steps[1].averageLapMs, 21_100, "the first controlled step's pace is averaged"); assert.equal(progress.controlledComparison, null, "only one controlled step has laps so far, nothing to compare yet"); }); test("step training progress compares the first and last controlled steps as a neutral pace difference", () => { const startedAt = 10_000; const state = { activeSessionId: "practice_1", sessions: [{ id: "practice_1", type: "open_practice", status: "running", startedAt }], resultsBySession: { practice_1: { passings: [ { driverId: "driver_1", timestamp: 11_000, lapMs: 25_000, validLap: true }, { driverId: "driver_1", timestamp: 37_000, lapMs: 25_500, validLap: true }, { driverId: "driver_1", timestamp: 59_000, lapMs: 21_000, validLap: true }, { driverId: "driver_1", timestamp: 81_200, lapMs: 21_200, validLap: true }, { driverId: "driver_1", timestamp: 99_200, lapMs: 18_000, validLap: true }, { driverId: "driver_1", timestamp: 117_000, lapMs: 17_800, validLap: true }, { driverId: "driver_1", timestamp: 138_800, lapMs: 21_800, validLap: true }, { driverId: "driver_1", timestamp: 161_000, lapMs: 22_200, validLap: true }, ], }, }, }; const config = { steps: [ { effort: "calm", lapCount: 2 }, { effort: "controlled", lapCount: 2 }, { effort: "fast", lapCount: 2 }, { effort: "controlled", lapCount: 2 }, ] }; const progress = buildStepTrainingProgress(state, "driver_1", "", config, 200_000); assert.equal(progress.complete, true); assert.equal(progress.currentStepIndex, 3); assert.deepEqual(progress.controlledComparison, { firstAvgMs: 21_100, lastAvgMs: 22_000, diffMs: 900 }); }); test("step training progress counts an excluded lap toward the step but not its pace average", () => { const startedAt = 10_000; const state = { activeSessionId: "practice_1", sessions: [{ id: "practice_1", type: "open_practice", status: "running", startedAt }], resultsBySession: { practice_1: { passings: [ { driverId: "driver_1", timestamp: 11_000, lapMs: 21_000, validLap: true }, { driverId: "driver_1", timestamp: 200_000, lapMs: 180_000, validLap: true, validationWarning: "above_max_stint" }, ], }, }, }; const config = { steps: [{ effort: "controlled", lapCount: 2 }] }; const progress = buildStepTrainingProgress(state, "driver_1", "", config, 400_000); assert.equal(progress.steps[0].lapsCompleted, 2, "both laps count toward the step's lap count"); assert.equal(progress.steps[0].averageLapMs, 21_000, "the excluded lap is left out of the pace average"); assert.equal(progress.complete, true); }); test("step training progress without a config or an active session", () => { assert.equal(buildStepTrainingProgress({}, "driver_1", "", null), null); assert.equal(buildStepTrainingProgress({}, "driver_1", "", { steps: [] }), null); const progress = buildStepTrainingProgress({ activeSessionId: "x", sessions: [], resultsBySession: {} }, "driver_1", "", { steps: [{ effort: "calm", lapCount: 3 }] }); assert.equal(progress.active, false); assert.equal(progress.completedLaps, 0); assert.equal(progress.complete, false); }); test("training stints split after a break over two minutes or a session change", () => { const laps = [ { sessionId: "practice_1", timestamp: 18_000, lapMs: 18_000 }, { sessionId: "practice_1", timestamp: 36_000, lapMs: 18_000 }, { sessionId: "practice_1", timestamp: 200_001, lapMs: 164_001 }, { sessionId: "practice_2", timestamp: 218_001, lapMs: 18_000 }, ]; const stints = splitTrainingStints(laps); assert.equal(stints.length, 3); assert.equal(stints[0].laps.length, 2); assert.equal(stints[1].laps.length, 1); assert.equal(stints[1].laps[0].excludedFromPace, true); assert.equal(stints[1].durationMs, 0); assert.equal(stints[1].startedAt, 200_001); assert.equal(stints[2].sessionId, "practice_2"); }); test("configured maximum lap time creates a new pace stint below the fallback break", () => { const stints = splitTrainingStints([ { sessionId: "practice_1", sourceId: "tp_1", timestamp: 37_000, lapMs: 37_000 }, { sessionId: "practice_1", sourceId: "tp_1", timestamp: 74_000, lapMs: 37_000 }, { sessionId: "practice_1", sourceId: "tp_1", timestamp: 164_000, lapMs: 90_000, excludedFromPace: true, stintBoundary: true }, { sessionId: "practice_1", sourceId: "tp_1", timestamp: 201_000, lapMs: 37_000 }, ]); assert.equal(stints.length, 2); assert.equal(stints[1].startedAt, 164_000); assert.equal(stints[1].durationMs, 37_000); assert.equal(stints[1].laps.filter((lap) => !lap.excludedFromPace).length, 1); }); test("best consecutive laps never cross a stint boundary", () => { const stints = splitTrainingStints([ { sessionId: "practice_1", timestamp: 18_000, lapMs: 18_000 }, { sessionId: "practice_1", timestamp: 35_000, lapMs: 17_000 }, { sessionId: "practice_1", timestamp: 51_000, lapMs: 16_000 }, { sessionId: "practice_1", timestamp: 66_000, lapMs: 15_000 }, { sessionId: "practice_1", timestamp: 201_001, lapMs: 15_000 }, ]); const best = getBestConsecutiveTrainingLaps(stints, 3); assert.equal(best.totalMs, 48_000); assert.equal(best.averageLapMs, 16_000); assert.equal(best.startedAt, 18_000); }); test("best five-minute result counts a final lap started before the cutoff and reports its pace", () => { const laps = Array.from({ length: 16 }, (_, index) => ({ sessionId: "practice_1", timestamp: (index + 1) * 18_000, lapMs: 18_000, })); laps.push({ sessionId: "practice_1", timestamp: 314_123, lapMs: 26_123 }); const result = getBestTrainingDurationResult(splitTrainingStints(laps), 300_000); assert.equal(result.lapCount, 17); assert.equal(result.totalMs, 314_123); assert.equal(result.averageLapMs, 18_478); assert.equal(result.startedAt, 0); assert.equal(result.endedAt, 314_123); }); test("best five-minute result never combines simultaneous transponder sources", () => { const laps = ["transponder_a", "transponder_b"].flatMap((sourceId) => ( Array.from({ length: 9 }, (_, index) => ({ driverId: sourceId, sourceId, sessionId: "practice_1", timestamp: (index + 1) * 37_000, lapMs: 37_000, })) )); const stints = splitTrainingStints(laps); const result = getBestTrainingDurationResult(stints, 300_000); assert.equal(stints.length, 2); assert.equal(result.lapCount, 9); assert.equal(result.totalMs, 333_000); assert.ok(result.totalMs <= result.targetMs + 37_000); }); test("a stint shorter than the target duration has no five-minute result", () => { const stints = splitTrainingStints([ { sessionId: "practice_1", timestamp: 18_000, lapMs: 18_000 }, { sessionId: "practice_1", timestamp: 36_000, lapMs: 18_000 }, ]); assert.equal(getBestTrainingDurationResult(stints, 300_000), null); }); test("training progress recognizes a clean five-minute stint and strong finish", () => { const ledger = Array.from({ length: 16 }, (_, index) => ({ driverId: "driver_1", sourceId: "transponder_1", sessionId: "practice_1", timestamp: (index + 1) * 20_000, lapMs: 20_000, })); const state = { training: { ledger } }; const stats = buildDriverTrainingStats(state, "driver_1"); const analysis = buildTrainingStintAnalysis(state, "driver_1"); assert.equal(stats.controlZoneMinMs, 20_000); assert.equal(stats.controlZoneMaxMs, 21_600); assert.equal(stats.controlledLapStreak, 16); assert.equal(stats.outlierLaps, 0); assert.equal(stats.strongFinishStints, 1); assert.equal(stats.cleanFiveMinuteStints, 1); assert.equal(stats.achievements.find((item) => item.id === "strong_finish").unlocked, true); assert.equal(stats.achievements.find((item) => item.id === "clean_five_minute").unlocked, true); assert.equal(analysis.rows[0].strongFinish, true); assert.equal(analysis.rows[0].fiveMinuteResult.lapCount, 15); assert.equal(analysis.rows[0].fiveMinuteResult.totalMs, 300_000); }); test("recommendations select baseline, consistency, and progression", () => { assert.equal(buildTrainingRecommendation({ totalLaps: 9 }).level, "baseline"); assert.deepEqual( buildTrainingRecommendation({ totalLaps: 30, recentLapCount: 30, consistencySpreadPct: 9, bestLapMs: 16000 }), { level: "consistency", titleKey: "training.plan_consistency_title", bodyKey: "training.plan_consistency_body", targetLapMs: 17280, } ); assert.equal(buildTrainingRecommendation({ totalLaps: 30, recentLapCount: 30, consistencySpreadPct: 4, bestLapMs: 16000 }).level, "progression"); }); test("goal and automatic milestone progress are bounded", () => { assert.deepEqual(getTrainingGoalProgress({ targetLaps: 500 }, { totalLaps: 125 }), { currentLaps: 125, targetLaps: 500, remainingLaps: 375, percent: 25, reached: false, }); assert.equal(getTrainingGoalProgress({ targetLaps: 100 }, { totalLaps: 150 }).percent, 100); assert.equal(getTrainingGoalProgress({ targetLaps: 100 }, { totalLaps: 150 }).reached, true); assert.equal(getNextTrainingMilestone({ totalLaps: 250 }).targetLaps, 500); assert.equal(getNextTrainingMilestone({ totalLaps: 5200 }).targetLaps, 6000); }); test("a goal can target completed guided-exercise sessions instead of laps", () => { const goal = { metric: "sessions", exerciseKind: "stepTraining", targetSessions: 10 }; assert.deepEqual(getTrainingGoalProgress(goal, { totalLaps: 999999 }, 4), { currentLaps: 4, targetLaps: 10, remainingLaps: 6, percent: 40, reached: false, }); assert.equal(getTrainingGoalProgress(goal, {}, 10).reached, true); assert.equal(getTrainingGoalProgress(goal, {}, 15).percent, 100, "percent is bounded at 100 past the target"); }); test("countExerciseSessions scopes by driver, class and kind independently", () => { const sessions = [ { driverId: "driver_1", classId: "class_1", kind: "stepTraining" }, { driverId: "driver_1", classId: "class_1", kind: "stepTraining" }, { driverId: "driver_1", classId: "class_1", kind: "evenLaps" }, { driverId: "driver_1", classId: "class_2", kind: "stepTraining" }, { driverId: "driver_2", classId: "class_1", kind: "stepTraining" }, ]; assert.equal(countExerciseSessions(sessions, { driverId: "driver_1", classId: "class_1", kind: "stepTraining" }), 2); assert.equal(countExerciseSessions(sessions, { driverId: "driver_1", classId: "class_1", kind: "evenLaps" }), 1); assert.equal(countExerciseSessions(sessions, { driverId: "driver_1", classId: "class_2", kind: "stepTraining" }), 1); assert.equal(countExerciseSessions(sessions, { kind: "stepTraining" }), 4, "an unscoped driver/class counts club-wide"); assert.equal(countExerciseSessions(undefined, { kind: "stepTraining" }), 0); }); test("countDriverExerciseSessions bridges a profile id (how admin goals store driverId) to the specific driver row id (how exerciseSessions store it)", () => { const state = { drivers: [{ id: "driver_1", profileId: "profile_1", name: "Driver One", classId: "class_1" }], training: { exerciseSessions: [ { driverId: "driver_1", classId: "class_1", kind: "stepTraining" }, { driverId: "driver_1", classId: "class_1", kind: "stepTraining" }, { driverId: "driver_1", classId: "class_1", kind: "evenLaps" }, { driverId: "driver_2", classId: "class_1", kind: "stepTraining" }, ], }, }; assert.equal(countDriverExerciseSessions(state, "profile_1", "class_1", "stepTraining"), 2, "resolves through the profile id, the same id an admin-created goal stores"); assert.equal(countDriverExerciseSessions(state, "profile_1", "class_1", "evenLaps"), 1); assert.equal(countDriverExerciseSessions(state, "profile_1", "class_2", "stepTraining"), 0, "wrong class"); assert.equal(countDriverExerciseSessions(state, "driver_2", "class_1", "stepTraining"), 1, "a driver with no profile entry falls back to matching its own id directly"); }); test("development trend compares the latest 30 laps with the preceding 30", () => { const start = new Date(2026, 0, 10, 12, 0, 0).getTime(); const ledger = Array.from({ length: 60 }, (_, index) => ({ driverId: "driver_1", sessionId: "practice_1", timestamp: start + ((index + 1) * 18_000), lapMs: index < 30 ? 18_000 : 17_000, })); const stats = buildDriverTrainingStats({ training: { ledger } }, "driver_1"); assert.equal(stats.trendSampleReady, true); assert.equal(stats.previousRecentAverageMs, 18_000); assert.equal(stats.recentAverageMs, 17_000); assert.equal(stats.averageTrendMs, 1_000); assert.equal(stats.consistencyTrendPct, 0); }); test("automatic achievements expose unlocked and upcoming personal goals", () => { const achievements = buildTrainingAchievements({ totalSessions: 2, totalLaps: 520, trainingDays: 4, controlledLapStreak: 7, bestFiveMinuteResult: { lapCount: 17 }, }); assert.equal(achievements.find((item) => item.id === "first_session").unlocked, true); assert.equal(achievements.find((item) => item.id === "laps_500").unlocked, true); assert.equal(achievements.find((item) => item.id === "laps_1000").percent, 52); assert.equal(achievements.find((item) => item.id === "days_10").unlocked, false); assert.equal(achievements.find((item) => item.id === "five_minute").unlocked, true); assert.equal(achievements.find((item) => item.id === "laps_250").unlocked, true); assert.equal(achievements.find((item) => item.id === "controlled_10").unlocked, false); }); test("daily missions recognize volume, control, and completed five-minute stints", () => { const missions = buildTrainingMissions({ todayLaps: 12, todayControlledLapStreak: 5, todayCompletedFiveMinuteStints: 1, }); assert.equal(missions.find((item) => item.id === "laps_today").percent, 60); assert.equal(missions.find((item) => item.id === "controlled_today").completed, true); assert.equal(missions.find((item) => item.id === "five_minute_today").completed, true); }); test("training highlights prioritize current personal progress", () => { const highlights = buildTrainingHighlights({ totalLaps: 600, todayLaps: 24, personalBestGainMs: 320, trendSampleReady: true, averageTrendMs: 180, consistencyTrendPct: 1.4, todayControlledLapStreak: 6, }); assert.deepEqual(highlights.map((item) => item.id), ["today_laps", "personal_best", "pace_gain"]); }); test("club goal progress starts at its saved baseline and is bounded", () => { const progress = getClubTrainingGoalProgress({ ledger: Array.from({ length: 175 }, (_, index) => ({ id: `lap_${index}` })), clubGoal: { targetLaps: 100, baselineLaps: 125 }, }); assert.equal(progress.currentLaps, 50); assert.equal(progress.remainingLaps, 50); assert.equal(progress.percent, 50); assert.equal(progress.reached, false); assert.equal(getClubTrainingGoalProgress({ ledger: [] }), null); }); test("club goal progress can also target completed guided-exercise sessions, club-wide", () => { const exerciseSessions = [ { driverId: "driver_1", classId: "class_1", kind: "evenLaps" }, { driverId: "driver_2", classId: "class_2", kind: "evenLaps" }, { driverId: "driver_1", classId: "class_1", kind: "stepTraining" }, ]; const progress = getClubTrainingGoalProgress({ exerciseSessions, clubGoal: { metric: "sessions", exerciseKind: "evenLaps", targetSessions: 5, baselineSessions: 1 }, }); assert.equal(progress.currentLaps, 1, "2 evenLaps sessions club-wide minus baseline 1"); assert.equal(progress.targetLaps, 5); assert.equal(progress.reached, false); assert.equal(getClubTrainingGoalProgress({ exerciseSessions: [], clubGoal: { metric: "sessions", exerciseKind: "evenLaps", targetSessions: 5 } }).currentLaps, 0, "no matching sessions yet still computes normally, just at zero"); assert.equal(getClubTrainingGoalProgress({ exerciseSessions, clubGoal: { metric: "sessions", targetSessions: 5 } }), null, "a sessions goal without an exerciseKind is invalid"); }); test("pace buckets use a trimmed average and keep classes isolated", () => { const start = new Date(2026, 7, 23, 12, 0, 0).getTime(); const twoWheelLaps = [10_000, ...Array.from({ length: 10 }, () => 20_000), 60_000]; const state = { drivers: [ { id: "driver_2wd", profileId: "profile", classId: "2wd" }, { id: "driver_4wd", profileId: "profile", classId: "4wd" }, ], training: { ledger: [ ...twoWheelLaps.map((lapMs, index) => ({ driverId: "driver_2wd", sessionId: "practice", timestamp: start + index * 60_000, lapMs })), { driverId: "driver_4wd", sessionId: "practice", timestamp: start + 60_000, lapMs: 14_000 }, ], markers: [], }, }; const buckets = buildTrainingPaceBuckets(state, "profile", "2wd", 30); const weekdays = buildTrainingWeekdayStats(state, "profile", "2wd"); assert.equal(buckets.length, 1); assert.equal(buckets[0].lapCount, 12); assert.equal(buckets[0].paceMs, 20_000); assert.equal(weekdays.find((day) => day.weekday === 0).lapCount, 12); }); test("setup markers compare only the selected class before and after the change", () => { const markerAt = new Date(2026, 7, 23, 13, 0, 0).getTime(); const state = { drivers: [ { id: "driver_2wd", profileId: "profile", classId: "2wd" }, { id: "driver_4wd", profileId: "profile", classId: "4wd" }, ], training: { ledger: [ ...[25, 20, 15].map((minutes, index) => ({ driverId: "driver_2wd", timestamp: markerAt - minutes * 60_000, lapMs: 20_000 + index * 100 })), ...[5, 10, 15].map((minutes, index) => ({ driverId: "driver_2wd", timestamp: markerAt + minutes * 60_000, lapMs: 18_000 + index * 100 })), { driverId: "driver_4wd", timestamp: markerAt + 60_000, lapMs: 12_000 }, ], markers: [{ id: "marker", driverId: "driver_2wd", classId: "2wd", timestamp: markerAt, type: "tires" }], }, }; const [comparison] = buildTrainingMarkerComparisons(state, "profile", "2wd", 30); assert.equal(comparison.beforeLapCount, 3); assert.equal(comparison.afterLapCount, 3); assert.equal(comparison.beforePaceMs, 20_100); assert.equal(comparison.afterPaceMs, 18_100); assert.equal(comparison.improvementMs, 2_000); }); test("stint analysis hides race pace 10 for short stints and flags slow laps", () => { const firstStart = new Date(2026, 7, 23, 12, 0, 0).getTime(); const firstLaps = [...Array.from({ length: 10 }, () => 18_000), 30_000]; let timestamp = firstStart; const ledger = firstLaps.map((lapMs) => { timestamp += lapMs; return { driverId: "driver", sessionId: "practice", timestamp, lapMs }; }); ledger.push({ driverId: "driver", sessionId: "practice", timestamp: timestamp + 180_001, lapMs: 18_000 }); ledger.push({ driverId: "driver", sessionId: "practice", timestamp: timestamp + 198_001, lapMs: 18_000 }); const analysis = buildTrainingStintAnalysis({ training: { ledger } }, "driver"); assert.equal(analysis.rows.length, 2); const longStint = analysis.rows.find((row) => row.lapCount === 11); const shortStint = analysis.rows.find((row) => row.lapCount === 2); assert.equal(longStint.racePace10Ms, 18_000); assert.equal(longStint.mistakes, 1); assert.equal(shortStint.racePace10Ms, 0); assert.equal(shortStint.consistencyPct, null); assert.equal(shortStint.insufficient, true); }); test("one Open Practice run across a layout switch: each lap lands on the layout active when it was driven", () => { const switchAt = 10_000; const state = { trackLayouts: [{ id: "layout_1", label: "Layout 1", startedAt: 1 }, { id: "layout_2", label: "Layout 2", startedAt: switchAt }], // The session itself was stamped with layout_1 when it started - that must not decide the laps. sessions: [{ id: "season", name: "Open Practice", eventId: "event_1", type: "open_practice", trackLayoutId: "layout_1" }], events: [{ id: "event_1", name: "Season practice" }], resultsBySession: { season: { passings: [ { driverId: "driver_1", competitorKey: "driver_1", lapMs: 12_000, timestamp: 5_000, validLap: true }, { driverId: "driver_1", competitorKey: "driver_1", lapMs: 11_800, timestamp: 8_000, validLap: true }, { driverId: "driver_1", competitorKey: "driver_1", lapMs: 15_000, timestamp: 20_000, validLap: true }, { driverId: "driver_1", competitorKey: "driver_1", lapMs: 14_600, timestamp: 35_000, validLap: true }, ], }, }, }; const training = syncTrainingLedger({}, state, { uid: createUid() }); assert.deepEqual(training.ledger.map((lap) => lap.trackLayoutId), ["layout_1", "layout_1", "layout_2", "layout_2"]); const withLedger = { ...state, training }; assert.equal(buildDriverTrainingStats(withLedger, "driver_1").bestLapMs, 14_600, "default = current layout (layout_2)"); assert.equal(buildDriverTrainingStats(withLedger, "driver_1", "", { layout: "layout_1" }).bestLapMs, 11_800, "an older layout can be picked"); const overall = buildDriverTrainingStats(withLedger, "driver_1", "", { layout: "all" }); assert.equal(overall.bestLapMs, 11_800, "all layouts = overall best"); assert.equal(overall.totalLaps, 4); assert.equal(buildDriverTrainingStats(withLedger, "driver_1", "", { layout: "layout_1" }).totalLaps, 4, "lifetime totals never depend on the layout choice"); assert.equal(buildTrainingStintAnalysis(withLedger, "driver_1", "", { layout: "layout_2" }).rows.reduce((sum, row) => sum + row.lapCount, 0), 2, "stint analysis follows the layout choice"); }); test("layout choice \"none\" picks the laps driven before any layout existed", () => { const state = { trackLayouts: [{ id: "layout_1", label: "Layout 1", startedAt: 50_000 }], training: { ledger: [ { driverId: "driver_1", sessionId: "s0", timestamp: 1000, lapMs: 13_000, trackLayoutId: "" }, { driverId: "driver_1", sessionId: "s1", timestamp: 60_000, lapMs: 16_000, trackLayoutId: "layout_1" }, ], }, }; assert.deepEqual(getDriverTrainingLapsForLayout(state, "driver_1", "", "none").map((lap) => lap.lapMs), [13_000]); assert.deepEqual(getDriverTrainingLapsForLayout(state, "driver_1", "", "current").map((lap) => lap.lapMs), [16_000]); assert.equal(getDriverTrainingLapsForLayout(state, "driver_1", "", "all").length, 2); }); test("training stats count practice by default; race laps are kept, marked and shown per race session, never as stints", () => { let n = 0; const uid = (prefix) => `${prefix}_${++n}`; const lap = (sessionId, timestamp, lapMs) => ({ driverId: "d1", competitorKey: "d1", sessionId, lapMs, timestamp, validLap: true }); const state = { drivers: [{ id: "d1", name: "D1", classId: "c1" }], classes: [{ id: "c1", name: "2WD" }], events: [{ id: "e_practice", name: "Club practice" }, { id: "e_race", name: "Winter #3 2WD" }], sessions: [ { id: "op", name: "Open Practice", type: "open_practice", eventId: "e_practice" }, { id: "q2", name: "Kval 2", type: "qualification", eventId: "e_race" }, ], series: [{ id: "s", name: "Winter Cup", eventIds: ["e_race"] }], resultsBySession: { op: { passings: [lap("op", 1000, 20000), lap("op", 21000, 20000), lap("op", 41000, 19500), lap("op", 60500, 19500)] }, q2: { passings: [lap("q2", 500000, 18000), lap("q2", 518000, 18200), lap("q2", 536200, 18400)] }, }, }; state.training = syncTrainingLedger({}, state, { uid }); assert.deepEqual(state.training.ledger.map((entry) => entry.sessionType), ["open_practice", "open_practice", "open_practice", "open_practice", "qualification", "qualification", "qualification"]); assert.equal(getDriverTrainingLaps(state, "d1").length, 4, "practice by default"); assert.equal(getDriverTrainingLaps(state, "d1", "", { source: "race" }).length, 3); assert.equal(getDriverTrainingLaps(state, "d1", "", { source: "all" }).length, 7); assert.equal(buildDriverTrainingStats(state, "d1").bestLapMs, 19500, "a race lap doesn't become the training best"); assert.equal(buildDriverTrainingStats(state, "d1", "", { source: "all" }).bestLapMs, 18000); // Stints come from practice laps only, even with "all"; with "race" there are none. const allStints = buildTrainingStintAnalysis(state, "d1", "", { source: "all" }); assert.equal(allStints.rows.reduce((sum, row) => sum + row.lapCount, 0), 4); assert.equal(buildTrainingStintAnalysis(state, "d1", "", { source: "race" }).rows.length, 0); const races = buildTrainingRaceSessions(state, "d1"); assert.equal(races.length, 1); assert.deepEqual([races[0].sessionName, races[0].eventName, races[0].eventId, races[0].lapCount, races[0].bestLapMs, races[0].averageLapMs], ["Kval 2", "Winter #3 2WD", "e_race", 3, 18000, 18200]); // A lap saved before session types were recorded gets it from its (still existing) session. const old = { ...state, training: { ...state.training, ledger: state.training.ledger.map(({ sessionType, ...entry }) => entry) } }; assert.equal(getDriverTrainingLaps(old, "d1", "", { source: "race" }).length, 3); delete old.resultsBySession; const resynced = syncTrainingLedger(old.training, old, { uid }); assert.equal(resynced.ledger.filter((entry) => entry.sessionType === "qualification").length, 3, "back-filled on the next sync"); });