import { describe, it, expect } from "vitest"; import { groupIncidents, buildTimeline, computeImpact } from "../rca"; import type { TelemetryRecord } from "../telemetry"; const env = ( messageId: string, causationId: string, correlationId: string, extra?: Record, ) => ({ messageId, causationId, correlationId, ...extra, }); // A failing chain: dispatch → published → action.failed (retry) → dlq.recorded, // all on correlation c1, plus an unrelated success on c2 and a solo failure. const RECORDS: TelemetryRecord[] = [ { kind: "action.dispatched", action: "orders.charge", appName: "billing", ts: "2026-01-01T00:00:00.000Z", envelope: env("m1", "m1", "c1", { tenant: "acme", userId: "u1" }), }, { kind: "event.published", event: "ChargeStarted", appName: "billing", ts: "2026-01-01T00:00:00.010Z", envelope: env("m2", "m1", "c1"), }, { kind: "action.failed", action: "orders.charge", appName: "billing", attempt: 1, maxAttempts: 3, willRetry: true, ts: "2026-01-01T00:00:00.050Z", error: { name: "TimeoutError", message: "timed out" }, envelope: env("m3", "m1", "c1"), }, { kind: "dlq.recorded", action: "orders.charge", appName: "billing", attempts: 3, ts: "2026-01-01T00:00:00.300Z", error: { name: "TimeoutError", message: "timed out" }, envelope: env("m4", "m1", "c1"), }, { kind: "action.completed", action: "orders.ok", appName: "billing", ts: "2026-01-01T00:00:01.000Z", envelope: env("n1", "n1", "c2"), }, { kind: "external.call.failed", call: "stripe.charge", target: "https://stripe", attempt: 1, willRetry: false, ts: "2026-01-01T00:00:02.000Z", error: { name: "FetchError", message: "ECONNREFUSED" }, }, ]; describe("groupIncidents", () => { it("groups failures by correlation; the terminal outcome is the headline", () => { const incidents = groupIncidents(RECORDS); const c1 = incidents.find((i) => i.id === "c1")!; expect(c1).toBeTruthy(); expect(c1.failures).toHaveLength(2); // action.failed + dlq.recorded expect(c1.root.kind).toBe("dlq.recorded"); // terminal outcome represents the incident expect(c1.related).toHaveLength(4); // whole chain, not just failures expect(c1.severity).toBe("critical"); // dlq.recorded dominates expect(c1.count).toBe(2); }); it("treats an envelope-less failure as a solo incident", () => { const incidents = groupIncidents(RECORDS); const solo = incidents.find((i) => i.id.startsWith("solo:"))!; expect(solo).toBeTruthy(); expect(solo.root.kind).toBe("external.call.failed"); expect(solo.related).toHaveLength(1); }); it("ignores correlations with no failures", () => { const incidents = groupIncidents(RECORDS); expect(incidents.find((i) => i.id === "c2")).toBeUndefined(); }); it("sorts newest first", () => { const incidents = groupIncidents(RECORDS); // the solo external failure is the latest record → first expect(incidents[0]!.id).toMatch(/^solo:/); }); }); describe("buildTimeline", () => { it("orders the chain with ms offsets from the first record", () => { const c1 = groupIncidents(RECORDS).find((i) => i.id === "c1")!; const tl = buildTimeline(c1); expect(tl).toHaveLength(4); expect(tl[0]!.offsetMs).toBe(0); expect(tl[3]!.offsetMs).toBe(300); expect(tl.filter((e) => e.failure)).toHaveLength(2); }); }); describe("computeImpact", () => { it("derives the blast radius from the chain", () => { const c1 = groupIncidents(RECORDS).find((i) => i.id === "c1")!; const impact = computeImpact(c1); expect(impact.tenant).toBe("acme"); expect(impact.userId).toBe("u1"); expect(impact.apps).toEqual(["billing"]); expect(impact.records).toBe(4); expect(impact.failures).toBe(2); expect(impact.retries).toBe(2); // 3 attempts → 2 retries expect(impact.deadLettered).toBe(true); }); });