test: add comprehensive AI feature install/uninstall test coverage

- Add 55 unit tests for feature-status.ts (installed.json CRUD, cache
  behavior, install lock, model verification, crash recovery, composite
  state) using real temp directories
- Add 36 integration tests for full install/uninstall lifecycle against
  Docker containers (face-detection bundle, SSE progress, tool gates,
  shared model protection, concurrent install prevention, auth guards,
  container restart recovery)
- Fix noise-removal CPU timeout by adding megapixel-based timeout
  calculation (120s/MP, min 5 minutes)
- Fix Playwright auth storage state race condition (mkdirSync before
  saving analytics-user.json)
- Fix 2 skipped tests in fixes-verification.spec.ts by replacing
  external ~/Downloads/sample dependency with existing test fixtures
- Enable skipped analytics-consent settings toggle test
- Restructure features.spec.ts to manage bundle state (uninstall/
  reinstall OCR) so 501 guard tests run instead of skipping
- Update noise-removal test mock to include sharp metadata() method
This commit is contained in:
SnapOtter
2026-04-28 13:27:54 +08:00
parent 60eb3abaa7
commit 4acec0846c
8 changed files with 1183 additions and 107 deletions
+83 -76
View File
@@ -31,15 +31,6 @@ async function fetchBundleStatuses(
return data.bundles as BundleInfo[];
}
async function isBundleInstalled(
request: import("@playwright/test").APIRequestContext,
bundleId: string,
): Promise<boolean> {
const bundles = await fetchBundleStatuses(request);
const bundle = bundles.find((b) => b.id === bundleId);
return bundle?.status === "installed";
}
// ─── Feature API tests ─────────────────────────────────────────────
test.describe("Feature API", () => {
@@ -102,16 +93,6 @@ test.describe("Feature API", () => {
expect(response.status()).toBe(404);
});
test("POST uninstall returns 409 for not-installed bundle", async ({ request }) => {
const installed = await isBundleInstalled(request, "background-removal");
if (installed) {
test.skip();
return;
}
const response = await request.post("/api/v1/admin/features/background-removal/uninstall");
expect(response.status()).toBe(409);
});
test("GET disk-usage returns totalBytes", async ({ request }) => {
const token = await getToken(request);
const response = await request.get("/api/v1/admin/features/disk-usage", {
@@ -123,20 +104,70 @@ test.describe("Feature API", () => {
});
});
// ─── Tool route guard tests ─────────────────────────────────────────
// ─── Tool route guard tests (serial: manages ocr bundle lifecycle) ──
test.describe("Tool route guards", () => {
test.describe.configure({ mode: "serial" });
const pngBuffer = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
"base64",
);
const aiTools = [
test.beforeAll(async ({ request }) => {
const token = await getToken(request);
const bundles = await fetchBundleStatuses(request);
const ocr = bundles.find((b) => b.id === "ocr");
if (ocr?.status === "installed") {
await request.post("/api/v1/admin/features/ocr/uninstall", {
headers: { Authorization: `Bearer ${token}` },
});
for (let i = 0; i < 30; i++) {
const updated = await fetchBundleStatuses(request);
if (updated.find((b) => b.id === "ocr")?.status === "not_installed") break;
await new Promise((r) => setTimeout(r, 2000));
}
}
});
test.afterAll(async ({ request }) => {
const token = await getToken(request);
await request.post("/api/v1/admin/features/ocr/install", {
headers: { Authorization: `Bearer ${token}` },
});
for (let i = 0; i < 60; i++) {
const updated = await fetchBundleStatuses(request);
if (updated.find((b) => b.id === "ocr")?.status === "installed") break;
await new Promise((r) => setTimeout(r, 5000));
}
});
// ocr bundle is uninstalled -- expect 501
test("ocr returns 501 FEATURE_NOT_INSTALLED with correct bundle", async ({ request }) => {
const response = await request.post("/api/v1/tools/ocr", {
multipart: {
file: {
name: "test.png",
mimeType: "image/png",
buffer: pngBuffer,
},
settings: JSON.stringify({}),
},
});
expect(response.status()).toBe(501);
const body = await response.json();
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
expect(body.feature).toBe("ocr");
expect(body.featureName).toBeTruthy();
expect(body.estimatedSize).toBeTruthy();
});
// All other AI tools have their bundles installed -- expect 200 (guard allows through)
const installedAiTools = [
{ tool: "remove-background", bundle: "background-removal" },
{ tool: "upscale", bundle: "upscale-enhance" },
{ tool: "blur-faces", bundle: "face-detection" },
{ tool: "erase-object", bundle: "object-eraser-colorize" },
{ tool: "ocr", bundle: "ocr" },
{ tool: "colorize", bundle: "object-eraser-colorize" },
{ tool: "enhance-faces", bundle: "upscale-enhance" },
{ tool: "noise-removal", bundle: "upscale-enhance" },
@@ -145,13 +176,8 @@ test.describe("Tool route guards", () => {
{ tool: "passport-photo", bundle: "background-removal" },
];
for (const { tool, bundle } of aiTools) {
test(`${tool} returns 501 FEATURE_NOT_INSTALLED with correct bundle`, async ({ request }) => {
const installed = await isBundleInstalled(request, bundle);
if (installed) {
test.skip();
return;
}
for (const { tool } of installedAiTools) {
test(`${tool} returns 200 when bundle is installed`, async ({ request }) => {
const response = await request.post(`/api/v1/tools/${tool}`, {
multipart: {
file: {
@@ -162,12 +188,7 @@ test.describe("Tool route guards", () => {
settings: JSON.stringify({}),
},
});
expect(response.status()).toBe(501);
const body = await response.json();
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
expect(body.feature).toBe(bundle);
expect(body.featureName).toBeTruthy();
expect(body.estimatedSize).toBeTruthy();
expect(response.status()).toBe(200);
});
}
@@ -185,23 +206,18 @@ test.describe("Tool route guards", () => {
// Should succeed or fail with a processing error, NOT 501
expect(response.status()).not.toBe(501);
});
});
// ─── Batch and pipeline guard tests ─────────────────────────────────
// ─── Uninstall conflict (ocr is not installed at this point) ──────
test.describe("Batch and pipeline guards", () => {
const pngBuffer = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
"base64",
);
test("POST uninstall returns 409 for not-installed bundle", async ({ request }) => {
const response = await request.post("/api/v1/admin/features/ocr/uninstall");
expect(response.status()).toBe(409);
});
// ─── Batch guard (ocr is not installed at this point) ─────────────
test("batch endpoint returns 501 for uninstalled AI tool", async ({ request }) => {
const installed = await isBundleInstalled(request, "background-removal");
if (installed) {
test.skip();
return;
}
const response = await request.post("/api/v1/tools/remove-background/batch", {
const response = await request.post("/api/v1/tools/ocr/batch", {
multipart: {
"files[]": {
name: "test.png",
@@ -215,25 +231,33 @@ test.describe("Batch and pipeline guards", () => {
const body = await response.json();
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
});
});
// ─── GUI tests (Playwright page interactions) ───────────────────────
// ─── GUI tests (ocr is not installed at this point) ───────────────
test.describe("Feature install UI", () => {
test("uninstalled AI tool page shows install prompt", async ({ page, request }) => {
const installed = await isBundleInstalled(request, "background-removal");
if (installed) {
test.skip();
return;
}
await page.goto("/remove-background");
await expect(page.getByText("Background Removal")).toBeVisible({
test("uninstalled AI tool page shows install prompt", async ({ page }) => {
await page.goto("/ocr");
await expect(page.getByText("OCR")).toBeVisible({
timeout: 10000,
});
await expect(page.getByText("additional download")).toBeVisible();
await expect(page.getByRole("button", { name: /enable/i })).toBeVisible();
});
test("AI tools show download badge in sidebar", async ({ page }) => {
await page.goto("/resize");
// Wait for the sidebar to load
await expect(page.locator("[data-testid='tool-panel']").or(page.locator("nav"))).toBeVisible({
timeout: 10000,
});
// The download icon should be visible near the OCR tool
const ocrLink = page.locator("a[href='/ocr']");
await expect(ocrLink).toBeVisible();
});
});
// ─── GUI tests (no bundle state dependency) ─────────────────────────
test.describe("Feature install UI", () => {
test("non-AI tool page loads normally", async ({ page }) => {
await page.goto("/resize");
// Should show the normal tool UI, not an install prompt
@@ -242,23 +266,6 @@ test.describe("Feature install UI", () => {
});
});
test("AI tools show download badge in sidebar", async ({ page, request }) => {
const installed = await isBundleInstalled(request, "background-removal");
if (installed) {
test.skip();
return;
}
await page.goto("/resize");
// Wait for the sidebar to load
await expect(page.locator("[data-testid='tool-panel']").or(page.locator("nav"))).toBeVisible({
timeout: 10000,
});
// The download icon should be visible near AI tools
// Note: we can't easily test for the exact Download icon, but we can check the tool links exist
const removeBackgroundLink = page.locator("a[href='/remove-background']");
await expect(removeBackgroundLink).toBeVisible();
});
test("settings dialog has AI Features section", async ({ page }) => {
await page.goto("/resize");
// Open settings - look for a settings button/gear icon