Fix lap limits and five-minute stint statistics

This commit is contained in:
larssand
2026-09-02 19:36:25 +02:00
parent f561dd8feb
commit 483a9dbd5a
14 changed files with 356 additions and 110 deletions

View File

@@ -4,12 +4,12 @@ import assert from "node:assert/strict";
import { connectDecoderHelper, processDecoderMessageHelper } from "../src/decoder_runtime.js";
import { ensureSessionResult } from "../src/timing_logic.js";
function createHarness() {
function createHarness(options = {}) {
const session = {
id: "session_1",
eventId: "event_1",
mode: "race",
type: "team_race",
type: options.sessionType || "team_race",
status: "running",
startMode: "mass",
startedAt: 1_000,
@@ -36,7 +36,10 @@ function createHarness() {
getActiveSession: () => session,
ensureSessionResult: (sessionId) => ensureSessionResult(sessionId, { state }),
normalizeStartMode: () => "mass",
getSessionLapWindow: () => ({ minLapMs: 0, maxLapMs: Number.POSITIVE_INFINITY }),
getSessionLapWindow: () => ({
minLapMs: Number(options.minLapMs || 0),
maxLapMs: options.maxLapMs == null ? Number.POSITIVE_INFINITY : Number(options.maxLapMs),
}),
findEventTeamForPassing: (event, driverId) => event.teams.find((team) => team.driverIds.includes(driverId)) || null,
persistPassingToBackend: () => {},
pushOverlayEvent: (type, label) => overlayEvents.push({ type, label }),
@@ -74,6 +77,28 @@ test("split decoder records intermediate time without changing main lap state",
assert.equal(result.passings.length, 1);
});
test("minimum lap time rejects a shortcut without adding a lap", () => {
const harness = createHarness({ minLapMs: 10_000, maxLapMs: 120_000 });
harness.passing(6_000, "main");
const result = harness.state.resultsBySession.session_1;
assert.equal(result.competitors.team_team_1.laps, 0);
assert.equal(result.passings[0].validLap, false);
assert.equal(result.passings[0].invalidReason, "below_min");
});
test("maximum lap time starts a statistics stint but still counts the race lap", () => {
const harness = createHarness({ minLapMs: 10_000, maxLapMs: 60_000 });
harness.passing(121_001, "main");
const result = harness.state.resultsBySession.session_1;
assert.equal(result.competitors.team_team_1.laps, 1);
assert.equal(result.passings[0].validLap, true);
assert.equal(result.passings[0].validationWarning, "above_max_stint");
assert.equal(result.passings[0].excludedFromPace, true);
assert.equal(result.passings[0].stintBoundary, true);
});
test("pit entry and exit create one timed stop without adding laps", () => {
const harness = createHarness();
harness.passing(17_000, "main");

View File

@@ -167,6 +167,35 @@ test("public timer query normalization bounds age and window values", () => {
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",

View File

@@ -88,7 +88,7 @@ test("ledger sync retains archived laps and rebuilds current session laps", () =
resultsBySession: {
live: {
passings: [
{ driverId: "driver_1", competitorKey: "driver_1", lapMs: 16500, timestamp: 3000, validLap: true },
{ 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 },
@@ -106,6 +106,8 @@ test("ledger sync retains archived laps and rebuilds current session laps", () =
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);
@@ -137,6 +139,24 @@ test("driver statistics remain isolated and aggregate days and sessions", () =>
assert.equal(stats.dailySummaries[1].laps, 2);
});
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: [
@@ -294,9 +314,25 @@ test("training stints split after a break over two minutes or a session change",
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 },
@@ -328,6 +364,25 @@ test("best five-minute result counts a final lap started before the cutoff and r
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 },