test: testing overhaul -- CI e2e gates, parallel suites, generated matrices, mutation testing (#215)

Closes the "e2e never runs in CI" hole. Adds per-PR e2e smoke gate,
nightly full-suite workflows, parallel vitest forks (per-fork DBs),
Playwright parallel/serial/visual projects against production builds,
metadata-generated test suites (drift guards, hostile inputs, format
matrix, pairwise settings, property-based fuzz), Stryker mutation
testing, Schemathesis API fuzz, coverage ratchet, and fixes for three
session-poisoning bugs that caused 200+ serial-bucket failures.

Bug fix included: favicon/split/bulk-rename could hang clients forever
when ZIP streaming failed after reply.hijack().
This commit is contained in:
SnapOtter
2026-06-10 22:01:13 +08:00
committed by GitHub
parent 3b8d529b44
commit 4ec39c556f
62 changed files with 2888 additions and 298 deletions
+104
View File
@@ -0,0 +1,104 @@
import { TOOLS } from "@snapotter/shared";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { getRegisteredToolIds, getToolConfig } from "../../apps/api/src/routes/tool-factory.js";
import { buildTestApp, loginAsAdmin, type TestApp } from "./test-server.js";
/**
* Drift guards between the shared TOOLS catalog and the API.
*
* Two intentional asymmetries exist and are pinned exactly:
* - REGISTRY_EXEMPT: tools whose contract does not fit the single-buffer
* process fn (multi-file, ZIP/JSON output, no-input generators, custom AI
* routes). They expose an HTTP route but are not in the pipeline/batch
* registry. If one of these gains registry support, remove it here.
* - LEGACY_ALIASES: extra registered toolIds kept for backwards-compatible
* URLs (consolidated into adjust-colors).
*/
const REGISTRY_EXEMPT = new Set([
"barcode-read",
"bulk-rename",
"collage",
"color-palette",
"compare",
"compose",
"erase-object",
"favicon",
"find-duplicates",
"html-to-image",
"image-to-base64",
"image-to-pdf",
"info",
"ocr",
"pdf-to-image",
"qr-generate",
"stitch",
"svg-to-raster",
"watermark-image",
]);
const LEGACY_ALIASES = new Set([
"brightness-contrast",
"saturation",
"color-channels",
"color-effects",
]);
describe("tool route drift", () => {
let testApp: TestApp;
let adminToken: string;
beforeAll(async () => {
testApp = await buildTestApp();
adminToken = await loginAsAdmin(testApp.app);
}, 30_000);
afterAll(async () => {
await testApp.cleanup();
}, 10_000);
it("every non-exempt TOOLS entry has a registered process fn", () => {
const registered = new Set(getRegisteredToolIds());
const missing = TOOLS.filter((t) => !REGISTRY_EXEMPT.has(t.id) && !registered.has(t.id)).map(
(t) => t.id,
);
expect(missing, `tools not registered on the API: ${missing.join(", ")}`).toEqual([]);
});
it("registry-exempt list is not stale", () => {
const registered = new Set(getRegisteredToolIds());
for (const id of REGISTRY_EXEMPT) {
expect(
registered.has(id),
`"${id}" is in REGISTRY_EXEMPT but IS registered now; remove it from the exempt list`,
).toBe(false);
}
});
it("every registered tool exposes a settings schema and process fn", () => {
for (const id of getRegisteredToolIds()) {
const config = getToolConfig(id);
expect(config?.settingsSchema, `tool "${id}" has no settings schema`).toBeTruthy();
expect(typeof config?.process, `tool "${id}" has no process fn`).toBe("function");
}
});
it("no orphan registrations (registered but missing from TOOLS, excluding legacy aliases)", () => {
const ids = new Set(TOOLS.map((t) => t.id));
for (const id of getRegisteredToolIds()) {
if (LEGACY_ALIASES.has(id)) continue;
expect(ids.has(id), `registered tool "${id}" has no TOOLS definition`).toBe(true);
}
});
it("every TOOLS entry answers on POST /api/v1/tools/:toolId (no dead routes)", async () => {
for (const tool of TOOLS) {
const res = await testApp.app.inject({
method: "POST",
url: `/api/v1/tools/${tool.id}`,
headers: { authorization: `Bearer ${adminToken}`, "content-type": "application/json" },
payload: {},
});
expect(res.statusCode, `tool "${tool.id}" has no live POST route (got 404)`).not.toBe(404);
}
}, 60_000);
});