The user pointed out that track layout changes matter most for Open Practice/training, not races - so the admin Training page and the driver's own mobile profile (server/public_training_profile.js, the actual thing most viewed after practice) needed the same layout scoping already applied to the live "track record" audio cue. Every stat now splits into two groups: lifetime/commitment numbers (total laps, sessions, training days, milestone badges) still read a driver's whole history, since a track rebuild doesn't erase past practice. Pace/record/consistency numbers (personal best, recent-pace trend, consistency spread, the "stable pace" control zone, stint achievements, "best stint") now read only laps tagged with whichever layout is currently active. Each ledger entry is tagged with trackLayoutId at the moment it's synced from a session, so it survives the source session later being deleted. Verified with new unit tests covering both implementations and a real headless-browser run confirming both the admin page and the actual public mobile profile show the current-layout best lap (never a faster but incomparable old-layout one) while keeping the correct full lifetime lap count in both places. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
467 lines
26 KiB
JavaScript
467 lines
26 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createRequire } from "node:module";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const {
|
|
buildPublicTrainingProfile,
|
|
findPublicTrainingShare,
|
|
hasPublicTrainingShare,
|
|
getPublicBestDurationResult,
|
|
normalizePublicTrainingSince,
|
|
normalizePublicTrainingWindow,
|
|
splitPublicTrainingStints,
|
|
addPublicTrainingMarker,
|
|
savePublicEvenLapsConfig,
|
|
savePublicStepTrainingConfig,
|
|
recordPublicExerciseSession,
|
|
} = require("../server/public_training_profile.js");
|
|
|
|
const DRIVER_ONE_TOKEN = "driver-one-token-12345678901234567890";
|
|
const DRIVER_TWO_TOKEN = "driver-two-token-12345678901234567890";
|
|
const DISABLED_TOKEN = "disabled-token-123456789012345678901";
|
|
|
|
function buildState(now = Date.now()) {
|
|
return {
|
|
drivers: [
|
|
{ id: "driver_1", profileId: "profile_1", name: "Driver One", brand: "Associated 2WD", classId: "class_1", transponder: "secret-transponder-one" },
|
|
{ id: "driver_1_4wd", profileId: "profile_1", name: "Driver One", brand: "Associated 4WD", classId: "class_2", transponder: "secret-transponder-four" },
|
|
{ id: "driver_2", profileId: "profile_2", name: "Driver Two", brand: "Xray", classId: "class_1", transponder: "secret-transponder-two" },
|
|
],
|
|
classes: [
|
|
{ id: "class_1", name: "2WD" },
|
|
{ id: "class_2", name: "4WD" },
|
|
],
|
|
settings: {
|
|
clubName: "Test Club",
|
|
clubTagline: "Practice",
|
|
logoDataUrl: "javascript:alert(1)",
|
|
wsUrl: "ws://secret-decoder:9000",
|
|
},
|
|
training: {
|
|
shares: [
|
|
{ driverId: "driver_1", token: DRIVER_ONE_TOKEN, enabled: true },
|
|
{ driverId: "driver_2", token: DRIVER_TWO_TOKEN, enabled: true },
|
|
{ driverId: "driver_1", token: DISABLED_TOKEN, enabled: false },
|
|
],
|
|
goals: [
|
|
{ driverId: "driver_1", title: "Bronze", reward: "Badge", targetLaps: 5 },
|
|
{ driverId: "driver_2", title: "Other goal", reward: "Other reward", targetLaps: 10 },
|
|
],
|
|
ledger: [
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: now - 25 * 60000, lapMs: 19000 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: now - 120000, lapMs: 18000 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: now - 12000, lapMs: 16500 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: now - 4000, lapMs: 16000 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: now - 2000, lapMs: 500 },
|
|
{ driverId: "driver_1_4wd", sessionId: "free", timestamp: now - 11000, lapMs: 14500 },
|
|
{ driverId: "driver_1_4wd", sessionId: "free", timestamp: now - 5000, lapMs: 14000 },
|
|
{ driverId: "driver_2", sessionId: "free", timestamp: now - 3000, lapMs: 12000 },
|
|
],
|
|
},
|
|
resultsBySession: {
|
|
free: {
|
|
splits: [
|
|
{ driverId: "driver_1", splitMs: 7200 },
|
|
{ driverId: "driver_1", splitMs: 7000 },
|
|
{ driverId: "driver_1_4wd", splitMs: 6400 },
|
|
{ driverId: "driver_2", splitMs: 6100 },
|
|
],
|
|
pitStops: [
|
|
{ driverId: "driver_1", pitStopMs: 12500, transponder: "secret-transponder-one" },
|
|
{ driverId: "driver_1_4wd", pitStopMs: 11000, transponder: "secret-transponder-four" },
|
|
{ driverId: "driver_2", pitStopMs: 9500, transponder: "secret-transponder-two" },
|
|
],
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
test("public share lookup rejects short, disabled, and unknown tokens", () => {
|
|
const state = buildState();
|
|
assert.equal(findPublicTrainingShare(state, "short"), null);
|
|
assert.equal(findPublicTrainingShare(state, DISABLED_TOKEN), null);
|
|
assert.equal(hasPublicTrainingShare(state, "unknown-token-123456789012345678"), false);
|
|
assert.equal(hasPublicTrainingShare(state, DRIVER_ONE_TOKEN), true);
|
|
});
|
|
|
|
test("public mobile marker is restricted to the shared profile and selected class", () => {
|
|
const state = buildState();
|
|
const marker = addPublicTrainingMarker(state, DRIVER_ONE_TOKEN, { classId: "class_2", type: "tires", note: " New rear tires " }, 123456);
|
|
|
|
assert.deepEqual({ driverId: marker.driverId, classId: marker.classId, type: marker.type, note: marker.note, timestamp: marker.timestamp }, { driverId: "driver_1_4wd", classId: "class_2", type: "tires", note: "New rear tires", timestamp: 123456 });
|
|
assert.equal(state.training.markers.length, 1);
|
|
assert.equal(addPublicTrainingMarker(state, DRIVER_TWO_TOKEN, { classId: "class_2" }, 123457), null);
|
|
assert.equal(addPublicTrainingMarker(state, DISABLED_TOKEN, { classId: "class_1" }, 123458), null);
|
|
});
|
|
|
|
test("a driver can save their own even-laps and step-training exercise from their mobile link", () => {
|
|
const state = buildState();
|
|
const evenLaps = savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "class_2", targetMs: 14000, toleranceMs: 300, lapCount: 6 });
|
|
assert.deepEqual({ driverId: evenLaps.driverId, classId: evenLaps.classId, targetMs: evenLaps.targetMs, toleranceMs: evenLaps.toleranceMs, lapCount: evenLaps.lapCount }, { driverId: "driver_1_4wd", classId: "class_2", targetMs: 14000, toleranceMs: 300, lapCount: 6 });
|
|
assert.equal(state.training.evenLaps.length, 1);
|
|
|
|
// Saving again for the same driver+class updates in place rather than adding a duplicate.
|
|
savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "class_2", targetMs: 13500, toleranceMs: 250, lapCount: 5 });
|
|
assert.equal(state.training.evenLaps.length, 1);
|
|
assert.equal(state.training.evenLaps[0].targetMs, 13500);
|
|
|
|
// A different driver's token, or a class the driver isn't in, must not be able to write it.
|
|
assert.equal(savePublicEvenLapsConfig(state, DRIVER_TWO_TOKEN, { classId: "class_2", targetMs: 10000, toleranceMs: 300, lapCount: 5 }), null);
|
|
assert.equal(savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "unknown_class", targetMs: 10000, toleranceMs: 300, lapCount: 5 }), null);
|
|
assert.equal(savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "class_2", targetMs: 0, toleranceMs: 300, lapCount: 5 }), null, "a non-positive target is rejected");
|
|
|
|
const stepTraining = savePublicStepTrainingConfig(state, DRIVER_ONE_TOKEN, { classId: "class_1", steps: [{ effort: "calm", lapCount: 3 }, { effort: "controlled", lapCount: 2 }] });
|
|
assert.equal(stepTraining.driverId, "driver_1");
|
|
assert.deepEqual(stepTraining.steps, [{ effort: "calm", lapCount: 3 }, { effort: "controlled", lapCount: 2 }]);
|
|
assert.equal(savePublicStepTrainingConfig(state, DRIVER_TWO_TOKEN, { classId: "class_2", steps: [{ effort: "calm", lapCount: 3 }] }), null, "driver_2 is not in class_2, unlike class_1 which they share with driver_1");
|
|
assert.equal(savePublicStepTrainingConfig(state, DRIVER_ONE_TOKEN, { classId: "class_1", steps: [] }), null, "a sequence with no laps is rejected");
|
|
});
|
|
|
|
test("public profile computes even-laps and step-training progress from their own since-marker, never leaking raw lap rows", () => {
|
|
const now = Date.now();
|
|
const state = buildState(now);
|
|
state.training.evenLaps = [{ id: "e1", driverId: "driver_1", classId: "class_1", targetMs: 16000, toleranceMs: 500, lapCount: 5 }];
|
|
state.training.stepTraining = [{ id: "s1", driverId: "driver_1", classId: "class_1", steps: [{ effort: "calm", lapCount: 1 }, { effort: "controlled", lapCount: 1 }] }];
|
|
// buildState's class_1 ledger for driver_1, oldest to newest: 19000, 18000, 16500, 16000
|
|
// (the 500ms lap is dropped by the ">500ms" pace filter applied before classLaps is built).
|
|
// A since-marker far enough back captures all four.
|
|
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "", 0, 0, now - 2_000_000, now - 2_000_000);
|
|
const classOne = profile.classProfiles.find((item) => item.classId === "class_1");
|
|
|
|
assert.deepEqual(classOne.evenLapsConfig, { targetMs: 16000, toleranceMs: 500, lapCount: 5 });
|
|
// 16000 and 16500 are both within 16000+/-500 of the target; 18000 before them is not.
|
|
assert.equal(classOne.evenLapsProgress.currentStreak, 2);
|
|
assert.equal(classOne.evenLapsProgress.active, true);
|
|
|
|
assert.deepEqual(classOne.stepTrainingConfig, { steps: [{ effort: "calm", lapCount: 1 }, { effort: "controlled", lapCount: 1 }] });
|
|
assert.equal(classOne.stepTrainingProgress.complete, true, "both configured laps (out of four recorded since the marker) are filled");
|
|
assert.equal(classOne.stepTrainingProgress.steps[0].averageLapMs, 19000);
|
|
assert.equal(classOne.stepTrainingProgress.steps[1].averageLapMs, 18000);
|
|
|
|
const serialized = JSON.stringify(profile);
|
|
assert.equal(serialized.includes("driver_1"), false, "the internal driver id must never reach the public payload, including in the new exercise fields");
|
|
});
|
|
|
|
test("public profile omits exercise progress and configuration when the driver has not configured or started one", () => {
|
|
const now = Date.now();
|
|
const profile = buildPublicTrainingProfile(buildState(now), DRIVER_ONE_TOKEN, "", 0, 0);
|
|
const classOne = profile.classProfiles.find((item) => item.classId === "class_1");
|
|
assert.equal(classOne.evenLapsConfig, null);
|
|
assert.equal(classOne.evenLapsProgress, null);
|
|
assert.equal(classOne.stepTrainingConfig, null);
|
|
assert.equal(classOne.stepTrainingProgress, null);
|
|
});
|
|
|
|
test("public profile contains only the selected driver's sanitized aggregates", () => {
|
|
const now = Date.now();
|
|
const profile = buildPublicTrainingProfile(buildState(now), DRIVER_ONE_TOKEN, "2026-08-24T12:00:00.000Z", now - 30000, 30000);
|
|
|
|
assert.equal(profile.profile.name, "Driver One");
|
|
assert.equal(profile.profile.brand, "Associated 2WD");
|
|
assert.equal(profile.profile.className, "2WD");
|
|
assert.equal("clubRank" in profile.profile, false);
|
|
assert.equal("rankedDrivers" in profile.profile, false);
|
|
assert.equal(profile.branding.clubName, "Test Club");
|
|
assert.equal(profile.branding.logoDataUrl, "");
|
|
assert.equal(profile.stats.totalLaps, 4);
|
|
assert.equal(profile.stats.bestLapMs, 16000);
|
|
assert.deepEqual(profile.currentSession, { laps: 2, bestLapMs: 16000, averageLapMs: 16250, consistencySpreadPct: 0, consistencySampleReady: false });
|
|
assert.equal(profile.goals.length, 1);
|
|
assert.equal(profile.goals[0].title, "Bronze");
|
|
assert.equal(profile.goals[0].percent, 80);
|
|
assert.equal(profile.classProfiles.length, 2);
|
|
assert.equal(profile.classProfiles[0].stats.totalLaps, 4);
|
|
assert.equal(profile.classProfiles[0].stats.achievements.find((item) => item.id === "first_session").unlocked, true);
|
|
assert.ok(Array.isArray(profile.classProfiles[0].stats.paceBuckets));
|
|
assert.deepEqual(profile.classProfiles[0].channelStats, {
|
|
splitCount: 2,
|
|
bestSplitMs: 7000,
|
|
averageSplitMs: 7100,
|
|
latestSplitGainMs: 200,
|
|
pitStopCount: 1,
|
|
totalPitMs: 12500,
|
|
bestPitStopMs: 12500,
|
|
averagePitStopMs: 12500,
|
|
});
|
|
assert.equal(profile.classProfiles[1].className, "4WD");
|
|
assert.equal(profile.classProfiles[1].stats.totalLaps, 2);
|
|
assert.equal(profile.classProfiles[1].stats.bestLapMs, 14000);
|
|
assert.equal(profile.classProfiles[1].channelStats.splitCount, 1);
|
|
assert.equal(profile.classProfiles[1].channelStats.pitStopCount, 1);
|
|
|
|
const serialized = JSON.stringify(profile);
|
|
assert.equal(serialized.includes("Driver Two"), false);
|
|
assert.equal(serialized.includes("driver_1"), false);
|
|
assert.equal(serialized.includes("secret-transponder"), false);
|
|
assert.equal(serialized.includes("secret-decoder"), false);
|
|
});
|
|
|
|
test("public profile's personal-best and lifetime totals are scoped correctly across a track layout change", () => {
|
|
const now = Date.now();
|
|
const oldLayoutDay = now - 60 * 24 * 60 * 60 * 1000;
|
|
const state = buildState(now);
|
|
state.trackLayouts = [
|
|
{ id: "layout_1", label: "Layout 1", startedAt: oldLayoutDay - 1000 },
|
|
{ id: "layout_2", label: "Layout 2", startedAt: now - 24 * 60 * 60 * 1000 },
|
|
];
|
|
// Old layout: a much faster time from a track that no longer exists.
|
|
state.training.ledger.push(
|
|
{ driverId: "driver_1", sessionId: "old_track", timestamp: oldLayoutDay, lapMs: 9000, trackLayoutId: "layout_1" },
|
|
{ driverId: "driver_1", sessionId: "old_track", timestamp: oldLayoutDay + 10000, lapMs: 9200, trackLayoutId: "layout_1" },
|
|
);
|
|
// Every lap already in buildState's fixture predates any layout tracking (no
|
|
// trackLayoutId at all) - it must not be conflated with "layout_2" either.
|
|
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "");
|
|
const classOne = profile.classProfiles.find((item) => item.classId === "class_1");
|
|
assert.equal(classOne.stats.totalLaps, 6, "lifetime total counts the old-layout laps too");
|
|
assert.equal(classOne.stats.bestLapMs, 0, "no lap is tagged with the currently active layout yet, so there is no current-layout best");
|
|
|
|
// Tag one of the existing (previously untagged) laps as run on the current layout.
|
|
state.training.ledger.find((lap) => lap.lapMs === 16000).trackLayoutId = "layout_2";
|
|
const updatedProfile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "");
|
|
const updatedClassOne = updatedProfile.classProfiles.find((item) => item.classId === "class_1");
|
|
assert.equal(updatedClassOne.stats.bestLapMs, 16000, "the current-layout lap is now the reported best, not the faster but incomparable old-layout lap");
|
|
assert.equal(updatedClassOne.stats.totalLaps, 6, "lifetime total is unaffected by which layout is current");
|
|
});
|
|
|
|
test("public profile visibility removes disabled sections from the API payload", () => {
|
|
const state = buildState();
|
|
state.training.shares[0].visibility = { progress: false, achievements: false, performance: false, goals: false, history: false };
|
|
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "");
|
|
const classProfile = profile.classProfiles[0];
|
|
|
|
assert.deepEqual(profile.visibility, { progress: false, achievements: false, performance: false, goals: false, history: false });
|
|
assert.equal(classProfile.goals.length, 0);
|
|
assert.deepEqual(classProfile.channelStats, {});
|
|
assert.equal("paceHistory" in classProfile.stats, false);
|
|
assert.equal("achievements" in classProfile.stats, false);
|
|
assert.equal("stintSummaries" in classProfile.stats, false);
|
|
assert.equal("dailySummaries" in classProfile.stats, false);
|
|
assert.equal(classProfile.stats.totalLaps, 4);
|
|
assert.equal(classProfile.stats.bestLapMs, 16000);
|
|
});
|
|
test("public profile exposes only class-scoped development trends and achievements", () => {
|
|
const now = Date.now();
|
|
const state = buildState(now);
|
|
state.training.ledger = Array.from({ length: 60 }, (_, index) => ({
|
|
driverId: "driver_1",
|
|
sessionId: "practice_2wd",
|
|
timestamp: now - (60 - index) * 18_000,
|
|
lapMs: index < 30 ? 18_000 : 17_000,
|
|
}));
|
|
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "");
|
|
const twoWheelDrive = profile.classProfiles.find((item) => item.classId === "class_1");
|
|
const fourWheelDrive = profile.classProfiles.find((item) => item.classId === "class_2");
|
|
|
|
assert.equal(twoWheelDrive.stats.trendSampleReady, true);
|
|
assert.equal(twoWheelDrive.stats.averageTrendMs, 1_000);
|
|
assert.equal(twoWheelDrive.stats.achievements.find((item) => item.id === "controlled_5").unlocked, true);
|
|
assert.equal(fourWheelDrive.stats.totalLaps, 0);
|
|
assert.equal(fourWheelDrive.stats.trendSampleReady, false);
|
|
});
|
|
|
|
test("public timer window freezes aggregates after the personal session", () => {
|
|
const now = Date.now();
|
|
const profile = buildPublicTrainingProfile(buildState(now), DRIVER_ONE_TOKEN, "", now - 30 * 60000, 20 * 60000);
|
|
|
|
assert.deepEqual(profile.currentSession, { laps: 1, bestLapMs: 19000, averageLapMs: 19000, consistencySpreadPct: 0, consistencySampleReady: false });
|
|
});
|
|
|
|
test("public timer window reports consistency once it has enough laps, and withholds it otherwise", () => {
|
|
const now = Date.now();
|
|
const state = buildState(now);
|
|
const sessionStart = now - 5 * 60000;
|
|
state.training.ledger = [
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 10000, lapMs: 16000 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 20000, lapMs: 16100 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 30000, lapMs: 16200 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 40000, lapMs: 16300 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 50000, lapMs: 16400 },
|
|
];
|
|
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "", sessionStart, 5 * 60000);
|
|
assert.equal(profile.currentSession.laps, 5);
|
|
assert.equal(profile.currentSession.consistencySampleReady, true);
|
|
// mean 16200, p10 (index 0) 16000, p90 (index 4) 16400: (16400-16000)/16200*100
|
|
assert.ok(Math.abs(profile.currentSession.consistencySpreadPct - ((16400 - 16000) / 16200) * 100) < 0.001);
|
|
|
|
const shortProfile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "", sessionStart + 35000, 5 * 60000);
|
|
assert.equal(shortProfile.currentSession.laps, 2, "only the last two laps fall inside this shorter window");
|
|
assert.equal(shortProfile.currentSession.consistencySampleReady, false, "fewer than 5 laps must not report a spread");
|
|
assert.equal(shortProfile.currentSession.consistencySpreadPct, 0);
|
|
});
|
|
|
|
test("public profile returns null when token or driver is unavailable", () => {
|
|
const state = buildState();
|
|
assert.equal(buildPublicTrainingProfile(state, DISABLED_TOKEN, ""), null);
|
|
state.drivers = state.drivers.filter((driver) => driver.id !== "driver_1");
|
|
assert.equal(buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, ""), null);
|
|
});
|
|
|
|
test("public timer query normalization bounds age and window values", () => {
|
|
const before = Date.now();
|
|
const since = normalizePublicTrainingSince(undefined, "30000");
|
|
const after = Date.now();
|
|
assert.ok(since >= before - 30000 && since <= after - 30000);
|
|
assert.equal(normalizePublicTrainingSince(undefined, String(25 * 60 * 60 * 1000)), 0);
|
|
assert.equal(normalizePublicTrainingWindow("-100"), 0);
|
|
assert.equal(normalizePublicTrainingWindow(String(25 * 60 * 60 * 1000)), 24 * 60 * 60 * 1000);
|
|
});
|
|
|
|
test("public profile counts a max-boundary lap but excludes it from pace", () => {
|
|
const now = Date.now();
|
|
const state = buildState(now);
|
|
state.training.ledger = [
|
|
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "practice", timestamp: now - 127_000, lapMs: 37_000 },
|
|
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "practice", timestamp: now - 90_000, lapMs: 37_000 },
|
|
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "practice", timestamp: now, lapMs: 90_000, excludedFromPace: true, stintBoundary: true },
|
|
];
|
|
|
|
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "");
|
|
assert.equal(profile.stats.totalLaps, 3);
|
|
assert.equal(profile.stats.bestLapMs, 37_000);
|
|
assert.equal(profile.stats.averageLapMs, 37_000);
|
|
assert.equal(profile.stats.stintCount, 2);
|
|
});
|
|
|
|
test("public five-minute result never combines simultaneous transponder sources", () => {
|
|
const laps = ["tp_a", "tp_b"].flatMap((sourceId) => Array.from({ length: 9 }, (_, index) => ({
|
|
sourceId,
|
|
sessionId: "practice_1",
|
|
timestamp: (index + 1) * 37_000,
|
|
lapMs: 37_000,
|
|
})));
|
|
const result = getPublicBestDurationResult(splitPublicTrainingStints(laps), 300_000);
|
|
|
|
assert.equal(result.lapCount, 9);
|
|
assert.equal(result.totalMs, 333_000);
|
|
});
|
|
|
|
test("public profile duration result includes the lap started before five minutes", () => {
|
|
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 = getPublicBestDurationResult(splitPublicTrainingStints(laps), 300_000);
|
|
|
|
assert.equal(result.lapCount, 17);
|
|
assert.equal(result.totalMs, 314_123);
|
|
assert.equal(result.averageLapMs, 18_478);
|
|
});
|
|
|
|
function buildExerciseSessionState(now) {
|
|
return {
|
|
drivers: [{ id: "driver_1", profileId: "profile_1", name: "Driver One", classId: "class_1", transponder: "secret-transponder-one" }],
|
|
classes: [{ id: "class_1", name: "2WD" }],
|
|
settings: {},
|
|
training: {
|
|
shares: [{ driverId: "driver_1", token: DRIVER_ONE_TOKEN, enabled: true }],
|
|
ledger: [],
|
|
},
|
|
resultsBySession: {},
|
|
};
|
|
}
|
|
|
|
test("recording a completed step-training session verifies it against real ledger laps, never trusting the client's claim", () => {
|
|
const now = Date.now();
|
|
const state = buildExerciseSessionState(now);
|
|
state.training.stepTraining = [{ driverId: "driver_1", classId: "class_1", steps: [{ effort: "calm", lapCount: 2 }, { effort: "controlled", lapCount: 2 }] }];
|
|
const sessionStart = now - 60000;
|
|
|
|
// A driver who has NOT actually completed the sequence (only 1 of the required 4 laps) cannot
|
|
// have a completion recorded just by asking for one.
|
|
state.training.ledger = [{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 5000, lapMs: 16000 }];
|
|
assert.equal(recordPublicExerciseSession(state, DRIVER_ONE_TOKEN, { classId: "class_1", kind: "stepTraining", startedAgoMs: now - sessionStart }, now), null);
|
|
assert.equal((state.training.exerciseSessions || []).length, 0);
|
|
|
|
// Once the real ledger actually has all 4 laps, the same request succeeds and the summary is
|
|
// computed from that real data, not anything the client sent.
|
|
state.training.ledger.push(
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 15000, lapMs: 16100 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 25000, lapMs: 20000 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 35000, lapMs: 20200 },
|
|
);
|
|
const record = recordPublicExerciseSession(state, DRIVER_ONE_TOKEN, { classId: "class_1", kind: "stepTraining", startedAgoMs: now - sessionStart }, now);
|
|
assert.ok(record);
|
|
assert.equal(record.kind, "stepTraining");
|
|
assert.equal(record.driverId, "driver_1");
|
|
assert.equal(record.summary.totalSteps, 2);
|
|
assert.equal(record.summary.totalLaps, 4);
|
|
assert.deepEqual(record.summary.controlledComparison, null, "only one controlled step exists, so there is nothing to compare");
|
|
|
|
// Calling it again for the same run must not create a second history entry.
|
|
const again = recordPublicExerciseSession(state, DRIVER_ONE_TOKEN, { classId: "class_1", kind: "stepTraining", startedAgoMs: now - sessionStart }, now);
|
|
assert.equal(again.id, record.id);
|
|
assert.equal(state.training.exerciseSessions.length, 1);
|
|
});
|
|
|
|
test("recording a completed even-laps session requires the target streak to have genuinely been reached", () => {
|
|
const now = Date.now();
|
|
const state = buildExerciseSessionState(now);
|
|
state.training.evenLaps = [{ driverId: "driver_1", classId: "class_1", targetMs: 16000, toleranceMs: 300, lapCount: 3 }];
|
|
const sessionStart = now - 60000;
|
|
|
|
// Only 2 laps within tolerance so far - not yet a streak of 3.
|
|
state.training.ledger = [
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 5000, lapMs: 16000 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 15000, lapMs: 16100 },
|
|
];
|
|
assert.equal(recordPublicExerciseSession(state, DRIVER_ONE_TOKEN, { classId: "class_1", kind: "evenLaps", startedAgoMs: now - sessionStart }, now), null);
|
|
|
|
state.training.ledger.push({ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 25000, lapMs: 15900 });
|
|
const record = recordPublicExerciseSession(state, DRIVER_ONE_TOKEN, { classId: "class_1", kind: "evenLaps", startedAgoMs: now - sessionStart }, now);
|
|
assert.ok(record);
|
|
assert.equal(record.summary.streak, 3);
|
|
assert.equal(record.summary.lapCount, 3);
|
|
});
|
|
|
|
test("recording a completed timer session requires real elapsed time and at least one lap", () => {
|
|
const now = Date.now();
|
|
const state = buildExerciseSessionState(now);
|
|
const sessionStart = now - 300000;
|
|
state.training.ledger = [{ driverId: "driver_1", sessionId: "free", timestamp: sessionStart + 10000, lapMs: 16000 }];
|
|
|
|
// Claiming a 5-minute session finished after only 1 minute of real elapsed time is rejected.
|
|
assert.equal(recordPublicExerciseSession(state, DRIVER_ONE_TOKEN, { classId: "class_1", kind: "timer", startedAgoMs: 60000, windowMs: 300000 }, now), null);
|
|
|
|
const record = recordPublicExerciseSession(state, DRIVER_ONE_TOKEN, { classId: "class_1", kind: "timer", startedAgoMs: now - sessionStart, windowMs: 300000 }, now);
|
|
assert.ok(record);
|
|
assert.equal(record.summary.laps, 1);
|
|
assert.equal(record.summary.bestLapMs, 16000);
|
|
});
|
|
|
|
test("recording an exercise session for an unknown class or invalid kind is rejected", () => {
|
|
const now = Date.now();
|
|
const state = buildExerciseSessionState(now);
|
|
assert.equal(recordPublicExerciseSession(state, DRIVER_ONE_TOKEN, { classId: "unknown_class", kind: "timer", startedAgoMs: 60000, windowMs: 60000 }, now), null);
|
|
assert.equal(recordPublicExerciseSession(state, DRIVER_ONE_TOKEN, { classId: "class_1", kind: "not_a_real_kind", startedAgoMs: 60000 }, now), null);
|
|
assert.equal(recordPublicExerciseSession(state, DISABLED_TOKEN, { classId: "class_1", kind: "timer", startedAgoMs: 60000, windowMs: 60000 }, now), null);
|
|
});
|
|
|
|
test("a goal targeting completed sessions computes progress from real exerciseSessions, and history/driverId stay out of the public response", () => {
|
|
const now = Date.now();
|
|
const state = buildExerciseSessionState(now);
|
|
state.training.goals = [{ driverId: "driver_1", classId: "class_1", metric: "sessions", exerciseKind: "stepTraining", targetSessions: 2, title: "Coffee mug", reward: "Mug" }];
|
|
state.training.exerciseSessions = [
|
|
{ id: "ex_1", driverId: "driver_1", classId: "class_1", kind: "stepTraining", startedAt: now - 100000, completedAt: new Date(now - 90000).toISOString(), summary: { totalSteps: 2, totalLaps: 4 } },
|
|
{ id: "ex_2", driverId: "driver_1", classId: "class_1", kind: "evenLaps", startedAt: now - 50000, completedAt: new Date(now - 40000).toISOString(), summary: { streak: 5 } },
|
|
];
|
|
|
|
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "");
|
|
const classOne = profile.classProfiles.find((item) => item.classId === "class_1");
|
|
|
|
assert.equal(classOne.goals[0].metric, "sessions");
|
|
assert.equal(classOne.goals[0].exerciseKind, "stepTraining");
|
|
assert.equal(classOne.goals[0].currentLaps, 1, "only the stepTraining session counts toward this goal, not the evenLaps one");
|
|
assert.equal(classOne.goals[0].targetLaps, 2);
|
|
assert.equal(classOne.goals[0].reached, false);
|
|
|
|
assert.equal(classOne.exerciseSessionHistory.length, 2);
|
|
assert.deepEqual(classOne.exerciseSessionHistory.map((entry) => entry.kind).sort(), ["evenLaps", "stepTraining"]);
|
|
const serialized = JSON.stringify(classOne.exerciseSessionHistory);
|
|
assert.ok(!serialized.includes("driver_1"), "exercise session history must never expose driverId");
|
|
assert.ok(!serialized.includes("class_1"), "exercise session history must never expose classId");
|
|
});
|