import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { buildSoakSummary } from './wallboard-soak-analysis.mjs'; const directory = path.resolve(process.argv[2] ?? 'artifacts/evidence/M10-14/raw'); async function readJSON(name) { return JSON.parse(await readFile(path.join(directory, name), 'utf8')); } async function readNDJSON(name, required = true) { try { const content = await readFile(path.join(directory, name), 'utf8'); return content.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line)); } catch (error) { if (!required && error?.code === 'ENOENT') return []; throw error; } } const [metadata, recorded, samples, reconnects, events] = await Promise.all([ readJSON('metadata.json'), readJSON('result.json'), readNDJSON('samples.ndjson'), readNDJSON('reconnects.ndjson', false), readNDJSON('events.ndjson'), ]); const completedAt = new Date(recorded.completedAt); const completedMs = completedAt.getTime(); if (!Number.isFinite(completedMs)) throw new Error('result.json has no valid completedAt'); const relevantEvents = events.filter((event) => Date.parse(event.timestamp) <= completedMs); const openedSockets = relevantEvents.filter((event) => event.type === 'websocket-open').length; const closedSockets = relevantEvents.filter((event) => event.type === 'websocket-close').length; const recomputed = buildSoakSummary({ samples, reconnects, startedAt: new Date(metadata.startedAt), completedAt, durationHours: metadata.durationHours, sampleSeconds: metadata.sampleSeconds, counters: { maxActiveSockets: Math.max(0, ...relevantEvents.map((event) => Number(event.activeSockets) || 0)), openedSockets, closedSockets, websocketErrors: relevantEvents.filter((event) => event.type === 'websocket-error').length, pageErrors: relevantEvents.filter((event) => event.type === 'page-error').length, apiFailures: relevantEvents.filter((event) => event.type === 'api-failure').length, }, requireWallboardTitle: true, }); const comparison = { statusMatches: recomputed.status === recorded.status, sampleCountMatches: recomputed.sampleCount === recorded.sampleCount, reconnectCountMatches: recomputed.reconnectCount === recorded.reconnectCount, criticalMetricsMatch: [ 'expectedSampleCount', 'minimumSampleCount', 'maxSampleGapSeconds', 'maxAllowedSampleGapSeconds', 'reconnectFailures', 'retainedGrowthBytes', 'heapSlopeBytesPerHour', 'fpsP05', 'frameP95WorstMs', 'longestTaskMs', 'maxActiveSockets', 'openedSockets', 'closedSockets', 'websocketErrors', 'socketChurnBudget', 'pageErrors', 'apiFailures', 'maxDOMNodes', 'horizontalOverflowSamples', 'invalidWallboardSamples', ].every((field) => Object.is(recomputed[field], recorded[field])), }; const verification = { directory, recordedStatus: recorded.status, recomputedStatus: recomputed.status, comparison, recomputed, }; process.stdout.write(`${JSON.stringify(verification, null, 2)}\n`); if (!Object.values(comparison).every(Boolean) || recomputed.status !== 'pass') process.exitCode = 1;