Files

117 lines
4.4 KiB
TypeScript

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-045)", () => {
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");
});
});