mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
AI sidecar failures reached Sentry as 'Error: Error': the scrubber type-onlys plain Errors and the tool wrappers threw them from result.error. The bridge now exports toSidecarError(), wrapping the sidecar reason in a SafeError (memory-allocation text classifies as operational, the rest as bug); all 14 wrappers use it, plus the dispatcher crash/stdin/spawn rejection paths and parseStdoutJson. toBgRemovalError from #535 delegates to the shared helper. On the web side, DOMExceptions report their specific name via err.name, so the NATIVE_ERRORS allowlist dropped the whole family's browser-authored messages. It now carries the full WebIDL DOMException name table; messages still pass through url/path redaction. Bridge-mocking test files switched to importOriginal passthrough mocks.
104 lines
3.9 KiB
TypeScript
104 lines
3.9 KiB
TypeScript
import { readFile, writeFile } from "node:fs/promises";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("sharp", () => {
|
|
const mockSharp = vi.fn(() => ({
|
|
png: vi.fn().mockReturnThis(),
|
|
toBuffer: vi.fn().mockResolvedValue(Buffer.from("mock-png-data")),
|
|
}));
|
|
return { default: mockSharp };
|
|
});
|
|
|
|
vi.mock("node:fs/promises", () => ({
|
|
readFile: vi.fn().mockResolvedValue(Buffer.from("mock-output-data")),
|
|
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
vi.mock("../../../packages/ai/src/bridge.js", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("../../../packages/ai/src/bridge.js")>()),
|
|
runPythonWithProgress: vi.fn(),
|
|
parseStdoutJson: vi.fn(),
|
|
}));
|
|
|
|
import { parseStdoutJson, runPythonWithProgress } from "../../../packages/ai/src/bridge.js";
|
|
import { inpaint } from "../../../packages/ai/src/inpainting.js";
|
|
|
|
const IMG = Buffer.from("fake-image");
|
|
const MASK = Buffer.from("fake-mask");
|
|
const DIR = "/tmp/test-inpaint";
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(readFile).mockResolvedValue(Buffer.from("mock-output-data"));
|
|
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
vi.mocked(runPythonWithProgress).mockResolvedValue({ stdout: '{"success": true}', stderr: "" });
|
|
vi.mocked(parseStdoutJson).mockReturnValue({ success: true, method: "lama-onnx" });
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("inpaint quality-mode script selection", () => {
|
|
const ARGS = [`${DIR}/input_inpaint.png`, `${DIR}/mask_inpaint.png`, `${DIR}/output_inpaint.png`];
|
|
|
|
it("runs the LaMa script (inpaint.py) by default", async () => {
|
|
await inpaint(IMG, MASK, DIR);
|
|
expect(runPythonWithProgress).toHaveBeenCalledWith("inpaint.py", ARGS, expect.any(Object));
|
|
});
|
|
|
|
it("runs the LaMa script when quality is explicitly 'fast'", async () => {
|
|
await inpaint(IMG, MASK, DIR, undefined, "fast");
|
|
expect(runPythonWithProgress).toHaveBeenCalledWith("inpaint.py", ARGS, expect.any(Object));
|
|
});
|
|
|
|
it("runs the diffusion script (inpaint_hq.py) when quality is 'hq'", async () => {
|
|
await inpaint(IMG, MASK, DIR, undefined, "hq");
|
|
// A regression here would silently run LaMa while the UI reported High Quality.
|
|
expect(runPythonWithProgress).toHaveBeenCalledWith("inpaint_hq.py", ARGS, expect.any(Object));
|
|
expect(runPythonWithProgress).not.toHaveBeenCalledWith(
|
|
"inpaint.py",
|
|
expect.anything(),
|
|
expect.anything(),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("inpaint contract", () => {
|
|
it("writes the input and mask as PNGs and returns the output buffer", async () => {
|
|
const out = await inpaint(IMG, MASK, DIR, undefined, "hq");
|
|
expect(writeFile).toHaveBeenCalledWith(
|
|
`${DIR}/input_inpaint.png`,
|
|
Buffer.from("mock-png-data"),
|
|
);
|
|
expect(writeFile).toHaveBeenCalledWith(`${DIR}/mask_inpaint.png`, Buffer.from("mock-png-data"));
|
|
expect(readFile).toHaveBeenCalledWith(`${DIR}/output_inpaint.png`);
|
|
expect(out).toEqual(Buffer.from("mock-output-data"));
|
|
});
|
|
|
|
it("forwards onProgress to the bridge", async () => {
|
|
const onProgress = vi.fn();
|
|
await inpaint(IMG, MASK, DIR, onProgress, "hq");
|
|
expect(runPythonWithProgress).toHaveBeenCalledWith(
|
|
"inpaint_hq.py",
|
|
expect.any(Array),
|
|
expect.objectContaining({ onProgress }),
|
|
);
|
|
});
|
|
|
|
it("throws the Python error (no silent fallback) when the script fails", async () => {
|
|
vi.mocked(parseStdoutJson).mockReturnValue({
|
|
success: false,
|
|
error: "High-quality inpainting model not found",
|
|
});
|
|
await expect(inpaint(IMG, MASK, DIR, undefined, "hq")).rejects.toThrow(
|
|
"High-quality inpainting model not found",
|
|
);
|
|
});
|
|
|
|
it("throws a fallback message when the script fails without an error string", async () => {
|
|
vi.mocked(parseStdoutJson).mockReturnValue({ success: false });
|
|
await expect(inpaint(IMG, MASK, DIR, undefined, "hq")).rejects.toThrow("Inpainting failed");
|
|
});
|
|
});
|