import { describe, it, expect } from "vitest"; import { explainError, categorize, severityOf, subjectOf, attemptOf, retryNarrative, maxSeverity, } from "../error-friendly"; import type { TelemetryRecord } from "../telemetry"; const rec = (over: Partial & { kind: string }): TelemetryRecord => over; describe("subjectOf", () => { it("reads the failing unit off whichever id field is present", () => { expect(subjectOf(rec({ kind: "action.failed", action: "orders.charge" }))).toEqual({ kind: "action", name: "orders.charge", }); expect(subjectOf(rec({ kind: "projection.failed", projection: "balances" }))).toEqual({ kind: "projection", name: "balances", }); expect(subjectOf(rec({ kind: "external.call.failed", call: "stripe.charge" }))).toEqual({ kind: "external call", name: "stripe.charge", }); }); it("falls back to the source event, then a generic label", () => { expect(subjectOf(rec({ kind: "reaction.failed", sourceEvent: "OrderPlaced" }))).toEqual({ kind: "operation", name: "OrderPlaced", }); expect(subjectOf(rec({ kind: "x" })).name).toBe("an operation"); }); }); describe("categorize", () => { it("classifies by error name / message / code first", () => { expect( categorize(rec({ kind: "action.failed", error: { name: "ZodError", message: "x" } })), ).toBe("validation"); expect( categorize( rec({ kind: "action.failed", error: { name: "E", message: "field is required" } }), ), ).toBe("validation"); expect( categorize( rec({ kind: "action.failed", error: { name: "E", message: "card declined", code: 402 } }), ), ).toBe("declined"); expect( categorize( rec({ kind: "action.failed", error: { name: "Forbidden", message: "x", code: 403 } }), ), ).toBe("unauthorized"); expect( categorize( rec({ kind: "action.failed", error: { name: "E", message: "user not found", code: 404 } }), ), ).toBe("not-found"); expect( categorize( rec({ kind: "action.failed", error: { name: "E", message: "already exists", code: 409 } }), ), ).toBe("conflict"); expect( categorize( rec({ kind: "action.failed", error: { name: "TimeoutError", message: "timed out" } }), ), ).toBe("timeout"); }); it("falls back to the record kind for the forge terminal shapes", () => { expect(categorize(rec({ kind: "dlq.recorded", error: { name: "E", message: "boom" } }))).toBe( "dead-letter", ); expect( categorize(rec({ kind: "reaction.exhausted", error: { name: "E", message: "boom" } })), ).toBe("exhausted"); expect( categorize(rec({ kind: "external.call.failed", error: { name: "E", message: "boom" } })), ).toBe("external"); expect( categorize(rec({ kind: "projection.failed", error: { name: "E", message: "boom" } })), ).toBe("downstream"); }); it("is 'unknown' when nothing matches", () => { expect( categorize(rec({ kind: "action.failed", error: { name: "WeirdError", message: "?" } })), ).toBe("unknown"); }); }); describe("attemptOf / retryNarrative", () => { it("normalises the retrying shape", () => { const r = rec({ kind: "action.failed", attempt: 2, maxAttempts: 3, willRetry: true, error: { name: "E", message: "x" }, }); expect(attemptOf(r)).toMatchObject({ attempt: 2, maxAttempts: 3, willRetry: true, terminal: false, }); expect(retryNarrative(r)).toBe("attempt 2 of 3, retrying"); }); it("marks terminal shapes and reports total attempts", () => { const r = rec({ kind: "dlq.recorded", attempts: 3, error: { name: "E", message: "x" } }); expect(attemptOf(r)).toMatchObject({ attempts: 3, willRetry: false, terminal: true }); expect(retryNarrative(r)).toBe("3 attempts — gave up"); }); it("returns null when there's nothing to say", () => { expect(retryNarrative(rec({ kind: "action.failed", attempt: 1, maxAttempts: 1 }))).toBe( "attempt 1 of 1", ); expect(retryNarrative(rec({ kind: "projection.failed" }))).toBeNull(); }); }); describe("severityOf", () => { it("terminal shapes are critical", () => { expect(severityOf(rec({ kind: "dlq.recorded" }))).toBe("critical"); expect(severityOf(rec({ kind: "reaction.exhausted" }))).toBe("critical"); }); it("a failure that will retry is only a warning", () => { expect( severityOf( rec({ kind: "action.failed", willRetry: true, error: { name: "E", message: "x" } }), ), ).toBe("warning"); }); it("validation / declined are warnings (expected control-flow)", () => { expect( severityOf(rec({ kind: "action.failed", error: { name: "ZodError", message: "x" } })), ).toBe("warning"); expect( severityOf(rec({ kind: "action.failed", error: { name: "E", message: "declined" } })), ).toBe("warning"); }); it("an unrecoverable runtime failure is high", () => { expect( severityOf( rec({ kind: "action.failed", willRetry: false, error: { name: "TypeError", message: "boom" }, }), ), ).toBe("high"); }); }); describe("maxSeverity", () => { it("picks the louder", () => { expect(maxSeverity("warning", "critical")).toBe("critical"); expect(maxSeverity("high", "warning")).toBe("high"); expect(maxSeverity("info", "info")).toBe("info"); }); }); describe("explainError", () => { it("produces a friendly title/summary/suggestion naming the subject", () => { const f = explainError( rec({ kind: "action.failed", action: "orders.charge", willRetry: false, error: { name: "PaymentDeclined", message: "card declined", code: 402 }, }), ); expect(f.category).toBe("declined"); expect(f.title).toBe("Declined"); expect(f.summary).toContain("orders.charge"); expect(f.suggestion.length).toBeGreaterThan(0); expect(f.severity).toBe("warning"); expect(f.rawMessage).toBe("card declined"); }); it("explains a dead-lettered action loudly", () => { const f = explainError( rec({ kind: "dlq.recorded", action: "orders.ship", attempts: 5, error: { name: "TypeError", message: "boom" }, }), ); expect(f.severity).toBe("critical"); expect(f.title).toBe("Dead-lettered"); expect(f.summary).toContain("orders.ship"); expect(f.attempt.terminal).toBe(true); }); });