import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { createMotionIntervalWindow } from '../../src/modules/cockpit/domain/frameMotionWindow.js' const referenceStats = list => { const sorted = [...list].sort((a, b) => a - b) return { samples: sorted.length, p50Ms: sorted.length ? +sorted[Math.floor(sorted.length * .5)].toFixed(2) : null, p95Ms: sorted.length ? +sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * .95))].toFixed(2) : null, maxMs: sorted.length ? +sorted[sorted.length - 1].toFixed(2) : null, over50Ms: sorted.filter(n => n > 50).length, } } const emptyStats = { samples: 0, p50Ms: null, p95Ms: null, maxMs: null, over50Ms: 0 } // Continuous motion keeps the same percentile math the export already had. { const w = createMotionIntervalWindow() assert.equal(w.push(0, true), 'first', 'The first frame only seeds the previous frame') const frames = [16.7, 16.6, 33.4, 17.1, 16.9] let t = 0 for (const dt of frames) assert.equal(w.push(t += dt, true), 'sampled') const snapshot = w.snapshot() const { samples: count, p50Ms, p95Ms, maxMs, over50Ms } = snapshot assert.deepEqual({ samples: count, p50Ms, p95Ms, maxMs, over50Ms }, referenceStats(frames)) assert.equal(snapshot.restartGapsExcluded, 0) assert.equal(snapshot.longGapsExcluded, 0) assert.equal(snapshot.maxExcludedMs, null) } // Idle -> motion start must not enter the window, and the loss is counted. { const w = createMotionIntervalWindow() w.push(1000, false) assert.equal(w.push(1600, true), 'restart-gap', 'First moving frame after idle is a restart gap') assert.equal(w.push(1616.7, true), 'sampled', 'The next continuous moving frame is sampled') const snapshot = w.snapshot() assert.equal(snapshot.samples, 1) assert.equal(snapshot.maxMs, 16.7) assert.equal(snapshot.restartGapsExcluded, 1) assert.equal(snapshot.maxExcludedMs, 600, 'Excluded gap magnitude stays visible') } // Hidden pages mark a break instead of silently treating the gap as motion. { const w = createMotionIntervalWindow() w.push(500, true) w.markGap(900) assert.equal(w.push(940, true), 'restart-gap') assert.equal(w.snapshot().samples, 0) assert.equal(w.snapshot().restartGapsExcluded, 1) } // A genuine >=1000ms stall between two moving frames is excluded but reported. { const w = createMotionIntervalWindow() w.push(0, true) assert.equal(w.push(999, true), 'sampled', 'Just under the ceiling is still a real interval') assert.equal(w.push(1999, true), 'long-gap', 'Exactly 1000ms is a gap, not a frame interval') assert.equal(w.push(2999, true), 'long-gap') const snapshot = w.snapshot() assert.equal(snapshot.samples, 1) assert.equal(snapshot.longGapsExcluded, 2) assert.equal(snapshot.maxExcludedMs, 1000) } // Slow but continuous frames are real evidence and must not be hidden. { const w = createMotionIntervalWindow() w.push(0, true) assert.equal(w.push(300, true), 'sampled') w.push(600, true) assert.equal(w.snapshot().maxMs, 300) assert.equal(w.snapshot().over50Ms, 2) } // Non-monotonic or stationary observations never create samples. { const w = createMotionIntervalWindow() w.push(100, true) assert.equal(w.push(100, true), 'idle', 'Zero-length interval is not a frame') assert.equal(w.push(90, true), 'idle', 'Backwards clock is rejected') assert.equal(w.snapshot().samples, 0) assert.deepEqual(w.snapshot(), { ...emptyStats, restartGapsExcluded: 0, longGapsExcluded: 0, maxExcludedMs: null }) } // The rolling window keeps the newest 240 intervals. { const w = createMotionIntervalWindow() w.push(0, true) for (let i = 1; i <= 300; i++) w.push(i * 10, true) const snapshot = w.snapshot() assert.equal(snapshot.samples, 240, 'Only the newest 240 intervals are kept') assert.equal(snapshot.maxMs, 10, 'Every kept interval is the same 10ms step') assert.equal(snapshot.p50Ms, 10) } // Reset clears samples and counters so a new pilot window starts clean. { const w = createMotionIntervalWindow() w.push(0, true); w.push(2000, true); w.markGap(2100) w.reset() assert.deepEqual(w.snapshot(), { ...emptyStats, restartGapsExcluded: 0, longGapsExcluded: 0, maxExcludedMs: null }) assert.equal(w.push(5000, true), 'first', 'Reset also clears the previous frame') } // Wiring: the export must use this window instead of the old raw interval list. { const source = readFileSync('src/modules/cockpit/services/modelDiagnostics.js', 'utf8') assert.match(source, /createMotionIntervalWindow/, 'diagnostics must use the motion window') assert.match(source, /motion\.push\(/, 'diagnostics must feed the window') assert.match(source, /motion\.markGap\(/, 'hidden frames must mark a break') assert.doesNotMatch(source, /add\(intervals/, 'the raw interval list must be gone') } console.log('PASS motion interval window: continuity, restart/hidden gaps, stall reporting, rolling cap, reset, wiring')