import test from "node:test"; import assert from "node:assert/strict"; import { buildDriverTrainingStats, buildTrainingRecommendation, getDriverTrainingLaps, getNextTrainingMilestone, getTrainingGoalProgress, normalizeTrainingState, syncTrainingLedger, } from "../src/training_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 }, ], 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 }, { driverId: "driver_1", token: "b".repeat(24), enabled: true }, { driverId: "driver_2", token: "short", enabled: true }, ], }, { uid: createUid() }); assert.equal(normalized.goals.length, 1); assert.equal(normalized.goals[0].title, "Bronze"); assert.equal(normalized.goals[0].reward, "Badge"); assert.deepEqual(normalized.ledger.map((lap) => lap.id), ["lap_1"]); assert.deepEqual(normalized.shares.map((share) => share.token), ["a".repeat(24)]); }); 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 }, ], }; 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 }, { 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(synced.goals.length, 1); assert.equal(synced.shares.length, 1); }); 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); assert.equal(stats.controlledLapStreak, 2); assert.equal(stats.dailySummaries[0].laps, 1); assert.equal(stats.dailySummaries[1].laps, 2); }); 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); });