fix: reject malformed stability bundles

This commit is contained in:
Gustavo Madeira Santana
2026-04-22 17:47:22 -04:00
parent 10f128413d
commit f561d7f0ef
2 changed files with 104 additions and 4 deletions

View File

@@ -187,4 +187,64 @@ describe("diagnostic stability bundles", () => {
"Unsupported stability bundle version",
);
});
it("rejects malformed bundle snapshots before returning them", () => {
const baseBundle = {
version: 1,
generatedAt: "2026-04-22T12:00:00.000Z",
reason: "gateway.restart_startup_failed",
process: {
pid: 123,
platform: "darwin",
arch: "arm64",
node: "24.14.1",
uptimeMs: 1000,
},
host: {
hostname: "<redacted-hostname>",
},
snapshot: {
generatedAt: "2026-04-22T12:00:00.000Z",
capacity: 1000,
count: 1,
dropped: 0,
events: [{ seq: 1, ts: 1, type: "webhook.received" }],
summary: { byType: { "webhook.received": 1 } },
},
};
const cases = [
{
name: "malformed-event",
bundle: {
...baseBundle,
snapshot: {
...baseBundle.snapshot,
events: [{ type: "webhook.received", ts: 1 }],
},
},
error: "snapshot.events[0].seq",
},
{
name: "null-summary",
bundle: {
...baseBundle,
snapshot: {
...baseBundle.snapshot,
summary: null,
},
},
error: "snapshot.summary",
},
];
for (const testCase of cases) {
const file = path.join(tempDir, `${testCase.name}.json`);
fs.writeFileSync(file, `${JSON.stringify(testCase.bundle, null, 2)}\n`, "utf8");
const result = readDiagnosticStabilityBundleFileSync(file);
expect(result.status).toBe("failed");
expect(result.status === "failed" ? String(result.error) : "").toContain(testCase.error);
}
});
});

View File

@@ -161,6 +161,49 @@ function readObject(value: unknown, label: string): Record<string, unknown> {
return value as Record<string, unknown>;
}
function readNumber(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`Invalid stability bundle: ${label} must be a finite number`);
}
return value;
}
function readOptionalNumber(value: unknown, label: string): number | undefined {
if (value === undefined) {
return undefined;
}
return readNumber(value, label);
}
function readString(value: unknown, label: string): string {
if (typeof value !== "string") {
throw new Error(`Invalid stability bundle: ${label} must be a string`);
}
return value;
}
function readStabilitySnapshot(value: unknown): DiagnosticStabilitySnapshot {
const snapshot = readObject(value, "snapshot");
readString(snapshot.generatedAt, "snapshot.generatedAt");
readNumber(snapshot.capacity, "snapshot.capacity");
readNumber(snapshot.count, "snapshot.count");
readNumber(snapshot.dropped, "snapshot.dropped");
readOptionalNumber(snapshot.firstSeq, "snapshot.firstSeq");
readOptionalNumber(snapshot.lastSeq, "snapshot.lastSeq");
if (!Array.isArray(snapshot.events)) {
throw new Error("Invalid stability bundle: snapshot.events must be an array");
}
for (const [index, event] of snapshot.events.entries()) {
const record = readObject(event, `snapshot.events[${index}]`);
readNumber(record.seq, `snapshot.events[${index}].seq`);
readNumber(record.ts, `snapshot.events[${index}].ts`);
readString(record.type, `snapshot.events[${index}].type`);
}
const summary = readObject(snapshot.summary, "snapshot.summary");
readObject(summary.byType, "snapshot.summary.byType");
return snapshot as DiagnosticStabilitySnapshot;
}
function parseDiagnosticStabilityBundle(value: unknown): DiagnosticStabilityBundle {
const bundle = readObject(value, "bundle");
if (bundle.version !== DIAGNOSTIC_STABILITY_BUNDLE_VERSION) {
@@ -171,10 +214,7 @@ function parseDiagnosticStabilityBundle(value: unknown): DiagnosticStabilityBund
}
readObject(bundle.process, "process");
readObject(bundle.host, "host");
const snapshot = readObject(bundle.snapshot, "snapshot");
if (!Array.isArray(snapshot.events) || typeof snapshot.summary !== "object") {
throw new Error("Invalid stability bundle: snapshot is malformed");
}
readStabilitySnapshot(bundle.snapshot);
return bundle as DiagnosticStabilityBundle;
}