feat: normalize failures through a stable registry

This commit is contained in:
donghyeon-ka
2026-07-25 20:52:58 +09:00
parent a0ca15da65
commit 7f3569ce3c
4 changed files with 315 additions and 57 deletions
+70
View File
@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import {
ERROR_REGISTRY,
createFailure,
kindForStatus,
normalizeUnknownFailure,
} from "../../src/contracts/errors.js";
describe("frontend failure classification", () => {
it("defines all 26 stable error kinds with the seven contract fields", () => {
expect(Object.keys(ERROR_REGISTRY)).toHaveLength(26);
for (const definition of Object.values(ERROR_REGISTRY)) {
expect(definition).toEqual(
expect.objectContaining({
kind: expect.any(String),
defaultRetryable: expect.any(Boolean),
severity: expect.any(String),
userMessageKey: expect.any(String),
action: expect.any(String),
telemetryEvent: expect.any(String),
redaction: expect.any(Array),
}),
);
}
});
it.each([
[401, "AUTH_REQUIRED"],
[403, "FORBIDDEN"],
[404, "NOT_FOUND"],
[409, "CONFLICT"],
[422, "VALIDATION_REJECTED"],
[418, "UNKNOWN_CLIENT_FAILURE"],
[429, "RATE_LIMITED"],
[503, "SERVER_FAILURE"],
])("maps HTTP %i to %s", (status, kind) => {
expect(kindForStatus(status)).toBe(kind);
});
it("projects only allowlisted safe fields", () => {
const result = createFailure("SERVER_FAILURE", "LIST_SAMPLE_RESOURCES", 0, {
code: "TEMPORARY",
httpStatus: 503,
requestId: "request-1",
stack: "must not leak",
body: "must not leak",
authorization: "Bearer secret",
});
expect(result).toMatchObject({
kind: "SERVER_FAILURE",
code: "TEMPORARY",
httpStatus: 503,
requestId: "request-1",
});
expect(JSON.stringify(result)).not.toMatch(/stack|body|Bearer|secret/);
});
it("normalizes any thrown value without leaking it", () => {
const secret = { token: "sensitive", nested: { rawBody: "private" } };
const result = normalizeUnknownFailure(secret);
expect(result).toMatchObject({
kind: "UNKNOWN_FAILURE",
causeClass: "object",
});
expect(JSON.stringify(result)).not.toMatch(/sensitive|private|token|rawBody/);
});
});