mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: coverage campaign and mutation testing across five packages (#628)
Coverage 83.6 to 87.36% lines, 81.63 to 84.14% branches. Mutation testing across five packages: image-engine 85, media-engine 92, doc-engine 87, shared+enterprise 86, apps/api security and jobs slice. Runs all five lanes weekly. Fixes the silently-broken mutation CI (babel pin), a redact-pdf envelope-shape test bug, an untested enterprise license valid-signature path, and an audit test that only exercised a hand-copied reproduction. Test and config only, no product code changes beyond the babel pin and one test-only oidc export. Full suite: 16,712 pass, 0 fail.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { AiJobOutput, AiPathJobInput } from "../../../apps/api/src/jobs/ai-handlers.js";
|
||||
import {
|
||||
hasAiJobHandler,
|
||||
hasAiPathJobHandler,
|
||||
registerAiJobHandler,
|
||||
registerAiPathJobHandler,
|
||||
runAiPathToolJob,
|
||||
runAiToolJob,
|
||||
} from "../../../apps/api/src/jobs/ai-handlers.js";
|
||||
import type { ToolJobData } from "../../../apps/api/src/jobs/types.js";
|
||||
import type { ToolProcessCtx } from "../../../apps/api/src/routes/tool-factory.js";
|
||||
|
||||
const ctx: ToolProcessCtx = {
|
||||
signal: new AbortController().signal,
|
||||
scratchDir: "/tmp/ai-handler-registry-test",
|
||||
report: vi.fn(),
|
||||
};
|
||||
|
||||
function job(toolId: string): ToolJobData {
|
||||
return {
|
||||
jobId: `job-${toolId}`,
|
||||
toolId,
|
||||
userId: null,
|
||||
pool: "ai",
|
||||
inputRefs: [`uploads/job-${toolId}/input.png`],
|
||||
filename: "input.png",
|
||||
settings: {},
|
||||
kind: "ai-tool",
|
||||
};
|
||||
}
|
||||
|
||||
const output: AiJobOutput = {
|
||||
buffer: Buffer.from("out"),
|
||||
filename: "input_done.png",
|
||||
contentType: "image/png",
|
||||
};
|
||||
|
||||
// Unique per-test tool ids keep the module-level registry Maps isolated between
|
||||
// cases so registration in one test never leaks into another.
|
||||
describe("AI job handler registry", () => {
|
||||
describe("buffer-backed handlers", () => {
|
||||
it("reports no handler for a tool id that was never registered", () => {
|
||||
expect(hasAiJobHandler("registry-buffer-unregistered")).toBe(false);
|
||||
});
|
||||
|
||||
it("registers a handler and dispatches the job to it", async () => {
|
||||
const handler = vi.fn(async () => output);
|
||||
registerAiJobHandler("registry-buffer-dispatch", handler);
|
||||
|
||||
expect(hasAiJobHandler("registry-buffer-dispatch")).toBe(true);
|
||||
|
||||
const input = Buffer.from("in");
|
||||
const data = job("registry-buffer-dispatch");
|
||||
const result = await runAiToolJob(data, input, ctx);
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith(input, data, ctx);
|
||||
expect(result).toBe(output);
|
||||
});
|
||||
|
||||
it("throws a descriptive error when running a job with no registered handler", async () => {
|
||||
expect(hasAiJobHandler("registry-buffer-missing")).toBe(false);
|
||||
|
||||
await expect(
|
||||
runAiToolJob(job("registry-buffer-missing"), Buffer.from("x"), ctx),
|
||||
).rejects.toThrow("No AI job handler for registry-buffer-missing");
|
||||
});
|
||||
|
||||
it("overwrites an existing handler when the same tool id is registered twice", async () => {
|
||||
const first = vi.fn(async () => output);
|
||||
const second = vi.fn(async () => ({ ...output, filename: "second.png" }));
|
||||
registerAiJobHandler("registry-buffer-overwrite", first);
|
||||
registerAiJobHandler("registry-buffer-overwrite", second);
|
||||
|
||||
const result = await runAiToolJob(job("registry-buffer-overwrite"), Buffer.from("x"), ctx);
|
||||
|
||||
expect(first).not.toHaveBeenCalled();
|
||||
expect(second).toHaveBeenCalledTimes(1);
|
||||
expect(result.filename).toBe("second.png");
|
||||
});
|
||||
});
|
||||
|
||||
describe("path-backed handlers", () => {
|
||||
const pathInput: AiPathJobInput = { path: "/tmp/scan.pdf", size: 42 };
|
||||
|
||||
it("reports no path handler for a tool id that was never registered", () => {
|
||||
expect(hasAiPathJobHandler("registry-path-unregistered")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the buffer and path registries independent", () => {
|
||||
registerAiJobHandler(
|
||||
"registry-buffer-only",
|
||||
vi.fn(async () => output),
|
||||
);
|
||||
|
||||
// A buffer registration must not register the id as a path handler.
|
||||
expect(hasAiJobHandler("registry-buffer-only")).toBe(true);
|
||||
expect(hasAiPathJobHandler("registry-buffer-only")).toBe(false);
|
||||
});
|
||||
|
||||
it("registers a path handler and dispatches the job to it", async () => {
|
||||
const handler = vi.fn(async () => output);
|
||||
registerAiPathJobHandler("registry-path-dispatch", handler);
|
||||
|
||||
expect(hasAiPathJobHandler("registry-path-dispatch")).toBe(true);
|
||||
// The path registration must not leak into the buffer registry.
|
||||
expect(hasAiJobHandler("registry-path-dispatch")).toBe(false);
|
||||
|
||||
const data = job("registry-path-dispatch");
|
||||
const result = await runAiPathToolJob(data, pathInput, ctx);
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith(pathInput, data, ctx);
|
||||
expect(result).toBe(output);
|
||||
});
|
||||
|
||||
it("throws a descriptive error when running a path job with no registered handler", async () => {
|
||||
expect(hasAiPathJobHandler("registry-path-missing")).toBe(false);
|
||||
|
||||
await expect(runAiPathToolJob(job("registry-path-missing"), pathInput, ctx)).rejects.toThrow(
|
||||
"No path-backed AI job handler for registry-path-missing",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not fall back to a buffer handler for a path job dispatch", async () => {
|
||||
// Only a buffer handler exists for this id; the path dispatch must still fail.
|
||||
registerAiJobHandler(
|
||||
"registry-path-no-fallback",
|
||||
vi.fn(async () => output),
|
||||
);
|
||||
|
||||
expect(hasAiJobHandler("registry-path-no-fallback")).toBe(true);
|
||||
expect(hasAiPathJobHandler("registry-path-no-fallback")).toBe(false);
|
||||
|
||||
await expect(
|
||||
runAiPathToolJob(job("registry-path-no-fallback"), pathInput, ctx),
|
||||
).rejects.toThrow("No path-backed AI job handler for registry-path-no-fallback");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user