Public source validation / validate (push) Failing after 3m8s
76 lines
3.0 KiB
JavaScript
76 lines
3.0 KiB
JavaScript
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;
|