| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- 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')
|