Files
SnapOtter/tests/e2e-docker/ai-tools.spec.ts
SnapOtterandGitHub d10d0f544f fix: release QA hardening across processing, media, security, and CI gates (#649)
A release-readiness QA pass over the whole product. The commits split into
defects a user would hit and gates that were reporting green while measuring
nothing.

## Fixes that change behaviour

Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so
request.ip came from a client-set header and a forged X-Forwarded-For got past
the login limiter. The default is now a private-network trust list.

A transient Postgres outage stranded in-flight jobs, leaving finished output on
disk with no row pointing at it. A reconciler now resolves those rows and adopts
the bytes rather than dropping the work.

A Redis connection that moved to a new address wedged every read-blocked
consumer, so completions stopped signalling while health still answered 200.
Socket timeouts plus subscriber pings recover it.

Installing more than one AI bundle left the shared venv multi-versioned and
silently broke three tools. The installer now reconciles distributions to one
version each.

Converting an image to JXL at quality 1 through 4 returned a 500, because
libjxl 0.7 rejects the distance those values compute. The quality is floored at
what the encoder honours. A missing ffmpeg was also reported to the user as a
corrupt upload; it now says the engine is unavailable.

RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at
0.22.2, and the release scan was split so it can fail on an unfixed critical
instead of hiding it behind ignore-unfixed.

## Gates that could not fail

Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs
build; coverage discarded its whole report on any failing test; the lint gate
skipped root tests, scripts, and two workspaces; and several generated matrices
counted a host missing ffmpeg as a passing tool. Each now measures what it
claims.

Full evidence and the outstanding release items are tracked locally and are not
part of this branch.
2026-07-27 15:37:30 +08:00

1135 lines
36 KiB
TypeScript

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { expect, test } from "@playwright/test";
import { apiToolPath } from "@snapotter/shared";
// ─── AI Tools ───────────────────────────────────────────────────────
// Tests for: remove-background, upscale, ocr, blur-faces, smart-crop,
// enhance-faces, colorize, noise-removal, red-eye-removal, restore-photo,
// passport-photo, erase-object
//
// Most of these tools require the AI sidecar (Python bridge). OCR Fast is
// built in; only its signed Balanced/Best runtime is optional. Each test detects
// whether the required feature bundle is installed:
// - 200 or terminal 202 = tool works, verify output
// - 501 FEATURE_NOT_INSTALLED = skip gracefully (expected when bundle missing)
// - Other errors = genuine failures
const FIXTURES = join(process.cwd(), "tests", "fixtures", "image", "valid");
const EDGE_FIXTURES = join(process.cwd(), "tests", "fixtures", "image", "edge");
const CONTENT = FIXTURES;
let token: string;
test.beforeAll(async ({ request }) => {
const res = await request.post("/api/auth/login", {
data: { username: "admin", password: "admin" },
});
const body = await res.json();
token = body.token;
});
function fixture(name: string): Buffer {
return readFileSync(join(FIXTURES, name));
}
function contentFixture(name: string): Buffer {
return readFileSync(join(CONTENT, name));
}
function edgeFixture(name: string): Buffer {
return readFileSync(join(EDGE_FIXTURES, name));
}
const JPG_100x100 = fixture("test-100x100.jpg");
const BLANK_PNG = edgeFixture("test-blank.png");
const HEIC_PORTRAIT = fixture("test-portrait.heic");
/** Minimal valid 1x1 PNG for quick feature-detection probes. */
const TINY_PNG = edgeFixture("test-1x1.png");
/**
* Determine whether a given AI tool's feature bundle is installed by
* querying the features API and checking the status of the bundle that
* enables the tool.
*/
async function isFeatureInstalled(
request: import("@playwright/test").APIRequestContext,
toolId: string,
): Promise<boolean> {
const res = await request.get("/api/v1/features", {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok()) return false;
const data = await res.json();
// Map tool IDs to their bundle IDs
const toolBundleMap: Record<string, string> = {
"remove-background": "background-removal",
"passport-photo": "background-removal",
"blur-faces": "face-detection",
"red-eye-removal": "face-detection",
"smart-crop": "face-detection",
"erase-object": "object-eraser-colorize",
colorize: "object-eraser-colorize",
"ai-canvas-expand": "object-eraser-colorize",
upscale: "upscale-enhance",
"enhance-faces": "upscale-enhance",
"noise-removal": "upscale-enhance",
"restore-photo": "photo-restoration",
};
const bundleId = toolBundleMap[toolId];
if (!bundleId) return false;
const bundle = data.bundles?.find((b: { id: string }) => b.id === bundleId);
return bundle?.status === "installed";
}
interface OcrFeatureState {
status: string;
compatibility?: "compatible" | "incompatible" | "invalid";
availableQualities?: Array<"fast" | "balanced" | "best">;
}
async function getOcrFeatureState(
request: import("@playwright/test").APIRequestContext,
): Promise<OcrFeatureState> {
const res = await request.get("/api/v1/features", {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.ok()).toBe(true);
const data = await res.json();
const state = data.bundles?.find((bundle: { id: string }) => bundle.id === "ocr");
expect(state).toBeDefined();
return state as OcrFeatureState;
}
/**
* Post a file to an AI tool and return the response.
* Handles the 501 FEATURE_NOT_INSTALLED case gracefully.
*/
async function callAiTool(
request: import("@playwright/test").APIRequestContext,
toolId: string,
imageBuffer: Buffer,
settings: Record<string, unknown> = {},
filename = "test.png",
mimeType = "image/png",
): Promise<{ installed: boolean; ok: boolean; status: number; body: Record<string, unknown> }> {
const res = await request.post(apiToolPath(toolId), {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: filename, mimeType, buffer: imageBuffer },
settings: JSON.stringify(settings),
},
});
const status = res.status();
const body = await res.json();
if (status === 501 && body.code === "FEATURE_NOT_INSTALLED") {
return { installed: false, ok: false, status, body };
}
// Image OCR is a long-running API contract and always returns 202 once
// validation/enqueueing succeeds. Follow its SSE stream so black-box Docker
// assertions still inspect the authoritative terminal metadata.
if (toolId === "ocr" && status === 202 && typeof body.jobId === "string") {
const progress = await request.get(
`/api/v1/jobs/${encodeURIComponent(body.jobId as string)}/progress`,
{
headers: { Authorization: `Bearer ${token}` },
timeout: 120_000,
},
);
expect(progress.ok()).toBe(true);
const frames = (await progress.text())
.split(/\n\n/u)
.map((frame) => frame.match(/^data:\s*(.+)$/mu)?.[1])
.filter((data): data is string => data !== undefined)
.map((data) => JSON.parse(data) as Record<string, unknown>);
const terminal = [...frames]
.reverse()
.find((frame) => frame.phase === "complete" || frame.phase === "failed");
expect(terminal).toBeDefined();
if (terminal?.phase === "failed") {
return {
installed: true,
ok: false,
status: 422,
body: { error: terminal.error ?? "OCR failed" },
};
}
expect(terminal?.result).toBeDefined();
return {
installed: true,
ok: true,
status: 200,
body: terminal?.result as Record<string, unknown>,
};
}
return { installed: true, ok: res.ok(), status, body };
}
// ─── Remove Background ──────────────────────────────────────────────
test.describe("Remove Background", () => {
test("remove background returns download URL or 501", async ({ request }) => {
const result = await callAiTool(request, "remove-background", JPG_100x100, {});
if (!result.installed) {
expect(result.body.feature).toBe("background-removal");
expect(result.body.featureName).toBeTruthy();
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
expect(result.body.processedSize).toBeGreaterThan(0);
});
test("remove background with specific model", async ({ request }) => {
const result = await callAiTool(request, "remove-background", JPG_100x100, {
model: "u2net",
});
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("remove background on HEIC image", async ({ request }) => {
const result = await callAiTool(
request,
"remove-background",
HEIC_PORTRAIT,
{},
"portrait.heic",
"image/heic",
);
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
});
// ─── Upscale ────────────────────────────────────────────────────────
test.describe("Upscale", () => {
test("upscale 2x returns larger image or 501", async ({ request }) => {
const result = await callAiTool(request, "upscale", JPG_100x100, {
scale: 2,
model: "auto",
});
if (!result.installed) {
expect(result.body.feature).toBe("upscale-enhance");
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
expect(result.body.processedSize).toBeGreaterThan(0);
});
test("upscale 4x", async ({ request }) => {
const result = await callAiTool(request, "upscale", TINY_PNG, {
scale: 4,
model: "auto",
});
if (!result.installed) {
test.skip();
return;
}
// 4x upscale on a tiny image may fail with processing error — accept both
if (result.ok) {
expect(result.body.downloadUrl).toBeTruthy();
} else {
expect(result.body.error).toBeDefined();
}
});
test("upscale with lanczos (CPU fallback)", async ({ request }) => {
const result = await callAiTool(request, "upscale", JPG_100x100, {
scale: 2,
model: "lanczos",
});
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("upscale with realesrgan model", async ({ request }) => {
const result = await callAiTool(request, "upscale", TINY_PNG, {
scale: 2,
model: "realesrgan",
});
if (!result.installed) {
test.skip();
return;
}
// RealESRGAN may fail on very small images — accept both success and error
if (result.ok) {
expect(result.body.downloadUrl).toBeTruthy();
} else {
expect(result.body.error).toBeDefined();
}
});
});
// ─── OCR ────────────────────────────────────────────────────────────
test.describe("OCR", () => {
test("default OCR works with or without the accurate pack", async ({ request }) => {
const state = await getOcrFeatureState(request);
const result = await callAiTool(request, "ocr", JPG_100x100, {});
expect(result.ok).toBe(true);
expect(typeof result.body.text).toBe("string");
if (state.availableQualities?.includes("best")) {
expect(result.body).toMatchObject({
engine: "rapidocr-onnx",
provider: "CPUExecutionProvider",
device: "cpu",
requestedQuality: "best",
actualQuality: "best",
degraded: false,
});
} else {
expect(result.body).toMatchObject({
engine: "tesseract",
provider: "native",
device: "cpu",
requestedQuality: "fast",
actualQuality: "fast",
degraded: false,
});
}
});
test("Fast OCR is always the built-in native CPU engine", async ({ request }) => {
const result = await callAiTool(request, "ocr", JPG_100x100, { quality: "fast" });
expect(result.ok).toBe(true);
expect(result.body).toMatchObject({
engine: "tesseract",
provider: "native",
device: "cpu",
requestedQuality: "fast",
actualQuality: "fast",
degraded: false,
});
});
for (const quality of ["balanced", "best"] as const) {
test(`${quality} OCR requires the signed accurate runtime`, async ({ request }) => {
const state = await getOcrFeatureState(request);
const result = await callAiTool(request, "ocr", JPG_100x100, { quality });
if (state.availableQualities?.includes(quality)) {
expect(result.ok).toBe(true);
expect(result.body).toMatchObject({
engine: "rapidocr-onnx",
provider: "CPUExecutionProvider",
device: "cpu",
requestedQuality: quality,
actualQuality: quality,
degraded: false,
});
} else {
expect(result.status).toBe(501);
expect(["FEATURE_NOT_INSTALLED", "FEATURE_INCOMPATIBLE"]).toContain(result.body.code);
expect(result.body).toMatchObject({ feature: "ocr", requestedQuality: quality });
}
});
}
test("OCR on Japanese text image", async ({ request }) => {
const ocrJapanese = contentFixture("ocr-japanese.png");
const result = await callAiTool(
request,
"ocr",
ocrJapanese,
{ quality: "fast" },
"ocr-japanese.png",
"image/png",
);
expect(result.ok).toBe(true);
expect(result.body).toMatchObject({ engine: "tesseract", actualQuality: "fast" });
});
test("OCR on chat screenshot", async ({ request }) => {
const ocrChat = contentFixture("ocr-chat.jpeg");
const result = await callAiTool(
request,
"ocr",
ocrChat,
{ quality: "fast" },
"ocr-chat.jpeg",
"image/jpeg",
);
expect(result.ok).toBe(true);
expect(result.body).toMatchObject({ engine: "tesseract", actualQuality: "fast" });
// Chat screenshot should contain some readable text
if (result.body.text) {
expect((result.body.text as string).length).toBeGreaterThan(0);
}
});
});
// ─── Blur Faces ─────────────────────────────────────────────────────
test.describe("Blur Faces", () => {
test("blur faces processes image or returns 501", async ({ request }) => {
const portrait = contentFixture("portrait-color.jpg");
const result = await callAiTool(
request,
"blur-faces",
portrait,
{ blurRadius: 30, sensitivity: 0.5 },
"portrait.jpg",
"image/jpeg",
);
if (!result.installed) {
expect(result.body.feature).toBe("face-detection");
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("blur faces on HEIC portrait", async ({ request }) => {
const result = await callAiTool(
request,
"blur-faces",
HEIC_PORTRAIT,
{ blurRadius: 30 },
"portrait.heic",
"image/heic",
);
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("blur faces on image with no faces returns warning", async ({ request }) => {
const result = await callAiTool(request, "blur-faces", BLANK_PNG, { blurRadius: 30 });
if (!result.installed) {
test.skip();
return;
}
// Should succeed but indicate no faces were found
expect(result.ok).toBe(true);
// facesDetected should be 0 or a warning should be present
if (result.body.facesDetected !== undefined) {
expect(result.body.facesDetected).toBe(0);
}
});
test("blur faces on multi-face image", async ({ request }) => {
const multiFace = contentFixture("multi-face.webp");
const result = await callAiTool(
request,
"blur-faces",
multiFace,
{ blurRadius: 20 },
"multi-face.webp",
"image/webp",
);
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
});
// ─── Smart Crop ─────────────────────────────────────────────────────
test.describe("Smart Crop", () => {
test("smart crop to portrait dimensions or returns 501", async ({ request }) => {
const portrait = contentFixture("portrait-color.jpg");
const result = await callAiTool(
request,
"smart-crop",
portrait,
{ width: 400, height: 400 },
"portrait.jpg",
"image/jpeg",
);
if (!result.installed) {
expect(result.body.feature).toBe("face-detection");
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("smart crop with landscape aspect ratio", async ({ request }) => {
const portrait = contentFixture("portrait-color.jpg");
const result = await callAiTool(
request,
"smart-crop",
portrait,
{ width: 800, height: 400 },
"portrait.jpg",
"image/jpeg",
);
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
});
// ─── Enhance Faces ──────────────────────────────────────────────────
test.describe("Enhance Faces", () => {
test("enhance faces with auto model or returns 501", async ({ request }) => {
const portrait = contentFixture("portrait-color.jpg");
const result = await callAiTool(
request,
"enhance-faces",
portrait,
{ model: "auto" },
"portrait.jpg",
"image/jpeg",
);
if (!result.installed) {
expect(result.body.feature).toBe("upscale-enhance");
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("enhance faces with gfpgan model", async ({ request }) => {
const portrait = contentFixture("portrait-color.jpg");
const result = await callAiTool(
request,
"enhance-faces",
portrait,
{ model: "gfpgan" },
"portrait.jpg",
"image/jpeg",
);
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("enhance faces with codeformer model", async ({ request }) => {
const portrait = contentFixture("portrait-color.jpg");
const result = await callAiTool(
request,
"enhance-faces",
portrait,
{ model: "codeformer" },
"portrait.jpg",
"image/jpeg",
);
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
});
// ─── Colorize ───────────────────────────────────────────────────────
test.describe("Colorize", () => {
test("colorize B&W image or returns 501", async ({ request }) => {
const bwPortrait = contentFixture("portrait-bw.jpeg");
const result = await callAiTool(
request,
"colorize",
bwPortrait,
{ model: "auto" },
"portrait-bw.jpeg",
"image/jpeg",
);
if (!result.installed) {
expect(result.body.feature).toBe("object-eraser-colorize");
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("colorize with ddcolor model", async ({ request }) => {
const bwPortrait = contentFixture("portrait-bw.jpeg");
const result = await callAiTool(
request,
"colorize",
bwPortrait,
{ model: "ddcolor" },
"portrait-bw.jpeg",
"image/jpeg",
);
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
});
// ─── Noise Removal ──────────────────────────────────────────────────
test.describe("Noise Removal", () => {
test("noise removal with quick tier or returns 501", async ({ request }) => {
const result = await callAiTool(request, "noise-removal", JPG_100x100, {
tier: "quick",
});
if (!result.installed) {
expect(result.body.feature).toBe("upscale-enhance");
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("noise removal with balanced tier", async ({ request }) => {
const result = await callAiTool(request, "noise-removal", JPG_100x100, {
tier: "balanced",
});
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("noise removal with quality tier (SCUNet)", async ({ request }) => {
const result = await callAiTool(request, "noise-removal", JPG_100x100, {
tier: "quality",
});
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("noise removal with maximum tier (NAFNet)", async ({ request }) => {
const result = await callAiTool(request, "noise-removal", TINY_PNG, {
tier: "maximum",
});
if (!result.installed) {
test.skip();
return;
}
// NAFNet may fail on very small images — accept both success and error
if (result.ok) {
expect(result.body.downloadUrl).toBeTruthy();
} else {
expect(result.body.error).toBeDefined();
}
});
});
// ─── Red-Eye Removal ────────────────────────────────────────────────
test.describe("Red-Eye Removal", () => {
test("red-eye removal processes image or returns 501", async ({ request }) => {
const redEye = contentFixture("red-eye.jpg");
const result = await callAiTool(
request,
"red-eye-removal",
redEye,
{},
"red-eye.jpg",
"image/jpeg",
);
if (!result.installed) {
expect(result.body.feature).toBe("face-detection");
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("red-eye removal on image without red eyes succeeds gracefully", async ({ request }) => {
const portrait = contentFixture("portrait-color.jpg");
const result = await callAiTool(
request,
"red-eye-removal",
portrait,
{},
"portrait.jpg",
"image/jpeg",
);
if (!result.installed) {
test.skip();
return;
}
// Should succeed even if no red eyes found
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
});
// ─── Restore Photo ──────────────────────────────────────────────────
test.describe("Restore Photo", () => {
test("restore photo processes image or returns 501", async ({ request }) => {
const bwPortrait = contentFixture("portrait-bw.jpeg");
const result = await callAiTool(
request,
"restore-photo",
bwPortrait,
{},
"old-photo.jpeg",
"image/jpeg",
);
if (!result.installed) {
expect(result.body.feature).toBe("photo-restoration");
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
expect(result.body.processedSize).toBeGreaterThan(0);
});
});
// ─── Passport Photo ─────────────────────────────────────────────────
test.describe("Passport Photo", () => {
test("passport photo processes portrait or returns 501", async ({ request }) => {
const portrait = contentFixture("portrait-color.jpg");
const result = await callAiTool(
request,
"passport-photo",
portrait,
{},
"portrait.jpg",
"image/jpeg",
);
if (!result.installed) {
expect(result.body.feature).toBe("background-removal");
test.skip();
return;
}
// Passport photo may succeed or fail with "no face detected"
if (result.ok) {
expect(result.body.downloadUrl).toBeTruthy();
} else {
// Acceptable failure if face detection didn't find a face
expect(result.body.error).toBeDefined();
expect(typeof result.body.error).toBe("string");
expect(result.body.error).not.toContain("[object Object]");
}
});
test("passport photo error is readable for non-face image", async ({ request }) => {
const result = await callAiTool(request, "passport-photo", BLANK_PNG, {});
if (!result.installed) {
test.skip();
return;
}
// Should fail gracefully with a readable error, not [object Object]
if (!result.ok) {
expect(typeof result.body.error).toBe("string");
expect(result.body.error).not.toContain("[object Object]");
if (result.body.details) {
expect(typeof result.body.details).toBe("string");
}
}
});
test("passport photo on headshot portrait", async ({ request }) => {
const headshot = contentFixture("portrait-headshot.heic");
const result = await callAiTool(
request,
"passport-photo",
headshot,
{},
"headshot.heic",
"image/heic",
);
if (!result.installed) {
test.skip();
return;
}
if (result.ok) {
expect(result.body.downloadUrl).toBeTruthy();
}
});
});
// ─── Erase Object ───────────────────────────────────────────────────
test.describe("Erase Object", () => {
test("erase object returns 501 when feature not installed", async ({ request }) => {
const result = await callAiTool(request, "erase-object", JPG_100x100, {});
if (!result.installed) {
expect(result.body.feature).toBe("object-eraser-colorize");
expect(result.body.code).toBe("FEATURE_NOT_INSTALLED");
// This is the expected state — erase-object needs a mask and LaMa model
return;
}
// If installed, it may fail because no mask was provided
// That's still a valid test — we're checking the tool doesn't crash
expect(typeof result.body.error === "string" || result.body.downloadUrl).toBeTruthy();
});
});
// ─── AI Canvas Expand ──────────────────────────────────────────────
test.describe("AI Canvas Expand", () => {
test("expand canvas or returns 501", async ({ request }) => {
const result = await callAiTool(request, "ai-canvas-expand", JPG_100x100, {
extendTop: 50,
extendRight: 50,
extendBottom: 50,
extendLeft: 50,
tier: "fast",
});
if (!result.installed) {
expect(result.body.feature).toBe("object-eraser-colorize");
expect(result.body.code).toBe("FEATURE_NOT_INSTALLED");
test.skip();
return;
}
// AI canvas expand is async (returns 202 with jobId)
if (result.status === 202) {
expect(result.body.jobId).toBeTruthy();
expect(result.body.async).toBe(true);
} else {
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
expect(result.body.processedSize).toBeGreaterThan(0);
}
});
test("expand canvas with zero extend returns same-size image or error", async ({ request }) => {
const result = await callAiTool(request, "ai-canvas-expand", TINY_PNG, {
extendTop: 0,
extendRight: 0,
extendBottom: 0,
extendLeft: 0,
tier: "fast",
});
if (!result.installed) {
test.skip();
return;
}
// Zero expansion may succeed with unchanged image or may return validation error
if (result.ok || result.status === 202) {
expect(result.body.downloadUrl || result.body.jobId).toBeTruthy();
} else {
expect(result.body.error).toBeDefined();
}
});
test("expand canvas with HEIC input", async ({ request }) => {
const result = await callAiTool(
request,
"ai-canvas-expand",
HEIC_PORTRAIT,
{ extendRight: 100, tier: "fast" },
"portrait.heic",
"image/heic",
);
if (!result.installed) {
test.skip();
return;
}
if (result.status === 202) {
expect(result.body.jobId).toBeTruthy();
} else if (result.ok) {
expect(result.body.downloadUrl).toBeTruthy();
} else {
// May fail on edge cases -- acceptable
expect(result.body.error).toBeDefined();
}
});
});
// ─── Feature Bundle Status ──────────────────────────────────────────
test.describe("AI Feature Bundle Status", () => {
test("all AI tools return correct 501 response when uninstalled", async ({ request }) => {
// Map from tool ID used for the API POST to the lookup key in isFeatureInstalled
const aiTools = [
{ tool: "remove-background", featureKey: "remove-background", bundle: "background-removal" },
{ tool: "upscale", featureKey: "upscale", bundle: "upscale-enhance" },
{ tool: "blur-faces", featureKey: "blur-faces", bundle: "face-detection" },
{ tool: "erase-object", featureKey: "erase-object", bundle: "object-eraser-colorize" },
{ tool: "colorize", featureKey: "colorize", bundle: "object-eraser-colorize" },
{
tool: "ai-canvas-expand",
featureKey: "ai-canvas-expand",
bundle: "object-eraser-colorize",
},
{ tool: "enhance-faces", featureKey: "enhance-faces", bundle: "upscale-enhance" },
{ tool: "noise-removal", featureKey: "noise-removal", bundle: "upscale-enhance" },
{ tool: "red-eye-removal", featureKey: "red-eye-removal", bundle: "face-detection" },
{ tool: "restore-photo", featureKey: "restore-photo", bundle: "photo-restoration" },
{ tool: "passport-photo", featureKey: "passport-photo", bundle: "background-removal" },
];
let testedCount = 0;
for (const { tool, featureKey, bundle } of aiTools) {
const installed = await isFeatureInstalled(request, featureKey);
if (installed) continue;
testedCount++;
const res = await request.post(apiToolPath(tool), {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: TINY_PNG },
settings: JSON.stringify({}),
},
});
expect(res.status(), `${tool} should return 501`).toBe(501);
const body = await res.json();
expect(body.code, `${tool} missing code`).toBe("FEATURE_NOT_INSTALLED");
expect(body.feature, `${tool} wrong bundle`).toBe(bundle);
expect(body.featureName, `${tool} missing featureName`).toBeTruthy();
expect(
typeof body.estimatedSize === "string",
`${tool} estimatedSize should be a string`,
).toBe(true);
}
// If all bundles are installed, skip the test gracefully
if (testedCount === 0) {
test.skip();
}
});
});
// ─── Transparency Fixer ────────────────────────────────────────────
test.describe("Transparency Fixer", () => {
test("transparency fixer returns 202 or 501", async ({ request }) => {
const portrait = contentFixture("portrait-color.jpg");
const res = await request.post("/api/v1/tools/image/transparency-fixer", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "portrait.jpg", mimeType: "image/jpeg", buffer: portrait },
settings: JSON.stringify({}),
},
});
if (res.status() === 501) {
const body = await res.json();
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
expect(body.feature).toBe("background-removal");
} else {
// Async tool returns 202
expect(res.status()).toBe(202);
const body = await res.json();
expect(body.jobId).toBeTruthy();
expect(body.async).toBe(true);
}
});
test("transparency fixer with custom defringe", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/transparency-fixer", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: TINY_PNG },
settings: JSON.stringify({ defringe: 50, outputFormat: "png" }),
},
});
if (res.status() === 501) {
const body = await res.json();
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
} else {
expect(res.status()).toBe(202);
const body = await res.json();
expect(body.jobId).toBeTruthy();
}
});
test("transparency fixer with webp output", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/transparency-fixer", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
settings: JSON.stringify({ outputFormat: "webp" }),
},
});
if (res.status() === 501) {
const body = await res.json();
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
} else {
expect(res.status()).toBe(202);
const body = await res.json();
expect(body.jobId).toBeTruthy();
}
});
test("transparency fixer rejects empty file", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/transparency-fixer", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
settings: JSON.stringify({}),
},
});
expect(res.ok()).toBe(false);
const body = await res.json();
expect(body.error).toBeDefined();
});
});
// ─── Smart Crop — Additional ──────────────────────────────────────
test.describe("Smart Crop — additional", () => {
test("smart crop with square output", async ({ request }) => {
const portrait = contentFixture("portrait-color.jpg");
const result = await callAiTool(
request,
"smart-crop",
portrait,
{ width: 500, height: 500 },
"portrait.jpg",
"image/jpeg",
);
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
test("smart crop on multi-face image", async ({ request }) => {
const multiFace = contentFixture("multi-face.webp");
const result = await callAiTool(
request,
"smart-crop",
multiFace,
{ width: 300, height: 300 },
"multi-face.webp",
"image/webp",
);
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
});
});
// ─── Colorize — Additional ───────────────────────────────────────
test.describe("Colorize — additional", () => {
test("colorize with auto model", async ({ request }) => {
const bwPortrait = contentFixture("portrait-bw.jpeg");
const result = await callAiTool(
request,
"colorize",
bwPortrait,
{ model: "auto" },
"bw.jpeg",
"image/jpeg",
);
if (!result.installed) {
test.skip();
return;
}
expect(result.ok).toBe(true);
expect(result.body.downloadUrl).toBeTruthy();
expect(result.body.processedSize).toBeGreaterThan(0);
});
});
// ─── Auth Failure ──────────────────────────────────────────────────
test.describe("Auth failure", () => {
test("remove-background without token returns 401", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/remove-background", {
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: TINY_PNG },
settings: JSON.stringify({}),
},
});
expect(res.status()).toBe(401);
});
test("upscale without token returns 401", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/upscale", {
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: TINY_PNG },
settings: JSON.stringify({ scale: 2, model: "auto" }),
},
});
expect(res.status()).toBe(401);
});
test("ocr without token returns 401", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/ocr", {
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: TINY_PNG },
settings: JSON.stringify({}),
},
});
expect(res.status()).toBe(401);
});
test("transparency-fixer without token returns 401", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/transparency-fixer", {
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: TINY_PNG },
settings: JSON.stringify({}),
},
});
expect(res.status()).toBe(401);
});
test("blur-faces without token returns 401", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/blur-faces", {
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: TINY_PNG },
settings: JSON.stringify({ blurRadius: 30 }),
},
});
expect(res.status()).toBe(401);
});
test("colorize without token returns 401", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/colorize", {
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: TINY_PNG },
settings: JSON.stringify({}),
},
});
expect(res.status()).toBe(401);
});
});