diff --git a/src/logging/diagnostic-stability-bundle.test.ts b/src/logging/diagnostic-stability-bundle.test.ts index e044f268297..f5ac2d52a0e 100644 --- a/src/logging/diagnostic-stability-bundle.test.ts +++ b/src/logging/diagnostic-stability-bundle.test.ts @@ -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: "", + }, + 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); + } + }); }); diff --git a/src/logging/diagnostic-stability-bundle.ts b/src/logging/diagnostic-stability-bundle.ts index d25a5e9f792..d91bedadc59 100644 --- a/src/logging/diagnostic-stability-bundle.ts +++ b/src/logging/diagnostic-stability-bundle.ts @@ -161,6 +161,49 @@ function readObject(value: unknown, label: string): Record { return value as Record; } +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; }