Files
Live_RC/tests/training_logic.test.js
2026-08-24 18:35:22 +02:00

135 lines
5.7 KiB
JavaScript

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);
});