Fix lap limits and five-minute stint statistics
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user