lägg till jämna varv-övning + v1.2.0

Ny guidad träningsövning i Open Practice: sätt måltid, tolerans
och antal varv, se aktuell serie och dagens bästa live på
Training-sidan. Ett missat varv startar en ny serie utan att
radera bästa resultatet.

Hittade och fixade två buggar under vägen:
- syncTrainingLedger byggde om training-state vid varje sparning
  utan att föra vidare det nya evenLaps-fältet, så sparade
  konfigurationer försvann direkt
- Training-sidan kraschade för alla förare med färre än 3 varv i
  varje stint (t.ex. de första varven i ett pass) eftersom en
  dedupe-funktion läste en egenskap på ett null-värde

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-09 08:51:50 +02:00
parent d18cd7fb5e
commit 9122e91f2e
9 changed files with 341 additions and 18 deletions

View File

@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
import {
buildDriverActivePracticeStats,
buildEvenLapsProgress,
buildDriverTrainingStats,
buildDriverTimingChannelStats,
buildTrainingAchievements,
@@ -307,6 +308,61 @@ test("active practice stats ignore races and stopped practice sessions", () => {
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("training stints split after a break over two minutes or a session change", () => {
const laps = [
{ sessionId: "practice_1", timestamp: 18_000, lapMs: 18_000 },