import { describe, test, expect } from "bun:test"; import { classifyError } from "../src/lib/errors"; // Pure pattern-matching (M6 error taxonomy) — no browser needed. describe("classifyError", () => { test("decode-stage failures are always unsupported-file, regardless of message", () => { const result = classifyError(new Error("Unable to decode audio data"), { stage: "decode" }); expect(result.kind).toBe("unsupported-file"); expect(result.retryable).toBe(false); expect(result.suggestion).toMatch(/different file|convert/i); }); test("recognizes out-of-memory messages", () => { for (const message of [ "RuntimeError: Aborted(OOM)", "Cannot enlarge memory arrays to size 123456, (OOM)", "Failed to allocate memory for tensor", "could not allocate buffer", ]) { const result = classifyError(new Error(message)); expect(result.kind).toBe("out-of-memory"); expect(result.retryable).toBe(false); expect(result.suggestion).toMatch(/smaller model/i); } }); test("a worker crash with an empty message is treated as out-of-memory (D-027)", () => { const result = classifyError(new Error(""), { stage: "worker-crash" }); expect(result.kind).toBe("out-of-memory"); expect(result.message.length).toBeGreaterThan(0); }); test("a worker crash with a real message is not assumed to be OOM", () => { const result = classifyError(new Error("TypeError: x is not a function"), { stage: "worker-crash", }); expect(result.kind).toBe("generic"); }); test("recognizes WebGPU device-lost messages", () => { for (const message of [ "GPUDeviceLostInfo: device was lost", "WebGPU device was lost: reason=destroyed", "the GPU device has been lost", ]) { const result = classifyError(new Error(message)); expect(result.kind).toBe("device-lost"); expect(result.retryable).toBe(true); expect(result.suggestion).toMatch(/backend|try again/i); } }); test("recognizes network failures", () => { for (const message of [ "TypeError: Failed to fetch", "NetworkError when attempting to fetch resource.", "Load failed", ]) { const result = classifyError(new Error(message), { stage: "load" }); expect(result.kind).toBe("network"); expect(result.retryable).toBe(true); } }); test("offline context forces a network classification with a clear message", () => { const result = classifyError(new Error("something obscure"), { stage: "load", offline: true, }); expect(result.kind).toBe("network"); expect(result.retryable).toBe(true); expect(result.message).toMatch(/offline/i); expect(result.suggestion).toMatch(/restart/i); }); test("load-stage network suggestion mentions the download restarting", () => { const result = classifyError(new Error("Failed to fetch"), { stage: "load" }); expect(result.suggestion).toMatch(/restart/i); }); test("transcribe-stage network suggestion is stage-agnostic", () => { const result = classifyError(new Error("Failed to fetch"), { stage: "transcribe" }); expect(result.suggestion).not.toMatch(/restart/i); }); test("falls back to a generic, non-retryable classification", () => { const result = classifyError(new Error("Something went wrong doing the thing")); expect(result).toEqual({ kind: "generic", message: "Something went wrong doing the thing", retryable: false, }); }); test("accepts non-Error raw values", () => { expect(classifyError("plain string failure").message).toBe("plain string failure"); expect(classifyError(42).message).toBe("42"); }); }); describe("classifyError — transcript translation (D-WS4b)", () => { test("out of memory while translating suggests closing tabs, not a smaller Whisper model", () => { const classified = classifyError("RangeError: Out of memory", { stage: "translate" }); expect(classified.kind).toBe("out-of-memory"); expect(classified.suggestion).not.toContain("smaller model"); expect(classified.suggestion).toContain("other tabs"); }); test("a failed translation-model download is retryable and says the download restarts", () => { const classified = classifyError("Failed to fetch", { stage: "translate" }); expect(classified.kind).toBe("network"); expect(classified.retryable).toBe(true); expect(classified.suggestion).toContain("translation model"); }); });