This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user