// author: simon-pierre boucher import { describe, expect, it } from "vitest"; import { ERROR_TAXONOMY, httpStatusFor, isRetryable, tendrilError } from "./errors.js"; describe("error taxonomy", () => { it("maps every code to a valid HTTP status", () => { for (const [code, spec] of Object.entries(ERROR_TAXONOMY)) { expect(spec.http, code).toBeGreaterThanOrEqual(400); expect(spec.http, code).toBeLessThan(600); expect(spec.message.length, code).toBeGreaterThan(0); } }); it("marks non-retryable terminal codes correctly", () => { expect(isRetryable("ERR_ROBOTS_DENIED")).toBe(false); expect(isRetryable("ERR_SSRF_BLOCKED")).toBe(false); expect(isRetryable("ERR_UNSUPPORTED_TYPE")).toBe(false); expect(isRetryable("ERR_TARGET_4XX")).toBe(false); }); it("marks transient codes as retryable", () => { expect(isRetryable("ERR_TIER_TIMEOUT")).toBe(true); expect(isRetryable("ERR_POOL_EXHAUSTED")).toBe(true); expect(isRetryable("ERR_DAEMON_DOWN")).toBe(true); }); it("builds errors with default and custom messages", () => { expect(tendrilError("ERR_INVALID_URL").message).toBe(ERROR_TAXONOMY.ERR_INVALID_URL.message); const e = tendrilError("ERR_TOO_LARGE", { message: "custom", details: { size: 42 } }); expect(e.message).toBe("custom"); expect(e.details).toEqual({ size: 42 }); }); it("omits optional fields when not provided (exactOptionalPropertyTypes)", () => { const e = tendrilError("ERR_INTERNAL"); expect("details" in e).toBe(false); expect("cause" in e).toBe(false); }); it("exposes HTTP status via helper", () => { expect(httpStatusFor("ERR_UNAUTHORIZED")).toBe(401); expect(httpStatusFor("ERR_RATE_LIMITED")).toBe(429); }); });