mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
#276's overhauled integration tests + QA sweep scripts hit the old sectionless /api/v1/tools/<id> routes, which 404 after #280 made routes section-prefixed. Ran scripts/rewrite-tool-paths.ts for literal URLs and converted dynamic /api/v1/tools/${id} constructions to apiToolPath(). Negative-test paths (nonexistent/fake tools) intentionally left raw.
150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
/**
|
|
* Catalog integrity launch gate.
|
|
*
|
|
* Asserts that EVERY TOOLS catalog entry is fully wired end to end:
|
|
* - Has a live API route (POST returns non-404)
|
|
* - Has a frontend tool-registry entry with a Settings component
|
|
* - Has a display mode in TOOL_DISPLAY_MODES
|
|
* - Has an API process-fn registration OR is in REGISTRY_EXEMPT
|
|
*
|
|
* The existing drift guards (tool-route-drift, tool-registry-drift) check
|
|
* individual layers in isolation. This test is the cross-cutting launch gate:
|
|
* one assertion that no tool is half-wired across all three layers combined.
|
|
*
|
|
* The TOOLS.length is checked dynamically -- never hardcoded -- so the gate
|
|
* cannot silently drift when tools are added or removed.
|
|
*/
|
|
|
|
import { apiToolPath, TOOLS } from "@snapotter/shared";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { getRegisteredToolIds } from "../../../apps/api/src/routes/tool-factory.js";
|
|
import { TOOL_DISPLAY_MODES } from "../../../apps/web/src/lib/tool-display-modes.js";
|
|
import { toolRegistry } from "../../../apps/web/src/lib/tool-registry.js";
|
|
import { buildTestApp, loginAsAdmin, type TestApp } from "../test-server.js";
|
|
|
|
/**
|
|
* 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. Imported from the
|
|
* tool-route-drift test for consistency.
|
|
*/
|
|
const REGISTRY_EXEMPT = new Set([
|
|
"auto-subtitles",
|
|
"background-replace",
|
|
"barcode-generate",
|
|
"barcode-read",
|
|
"blur-background",
|
|
"bulk-rename",
|
|
"collage",
|
|
"color-palette",
|
|
"compare",
|
|
"compose",
|
|
"erase-object",
|
|
"favicon",
|
|
"find-duplicates",
|
|
"html-to-image",
|
|
"image-to-base64",
|
|
"image-to-pdf",
|
|
"info",
|
|
"ocr",
|
|
"ocr-pdf",
|
|
"pdf-to-image",
|
|
"qr-generate",
|
|
"stitch",
|
|
"svg-to-raster",
|
|
"transcribe-audio",
|
|
"watermark-image",
|
|
]);
|
|
|
|
describe("catalog integrity launch gate", () => {
|
|
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("TOOLS catalog is non-empty", () => {
|
|
expect(TOOLS.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("every tool is fully wired: API route + frontend registry + display mode", async () => {
|
|
const registeredProcessFns = new Set(getRegisteredToolIds());
|
|
const missingFrontend: string[] = [];
|
|
const missingDisplayMode: string[] = [];
|
|
const missingApiRoute: string[] = [];
|
|
const missingProcessFn: string[] = [];
|
|
|
|
// Check frontend + display mode synchronously
|
|
for (const tool of TOOLS) {
|
|
if (!toolRegistry.has(tool.id)) {
|
|
missingFrontend.push(tool.id);
|
|
}
|
|
if (!TOOL_DISPLAY_MODES[tool.id]) {
|
|
missingDisplayMode.push(tool.id);
|
|
}
|
|
if (!REGISTRY_EXEMPT.has(tool.id) && !registeredProcessFns.has(tool.id)) {
|
|
missingProcessFn.push(tool.id);
|
|
}
|
|
}
|
|
|
|
// Check API routes (POST, non-404)
|
|
for (const tool of TOOLS) {
|
|
const res = await testApp.app.inject({
|
|
method: "POST",
|
|
url: apiToolPath(tool.id),
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": "application/json",
|
|
},
|
|
payload: {},
|
|
});
|
|
if (res.statusCode === 404) {
|
|
missingApiRoute.push(tool.id);
|
|
}
|
|
}
|
|
|
|
// Report all gaps in one assertion block for clarity
|
|
expect(
|
|
missingFrontend,
|
|
`tools missing frontend registry entry: ${missingFrontend.join(", ")}`,
|
|
).toEqual([]);
|
|
expect(
|
|
missingDisplayMode,
|
|
`tools missing display mode: ${missingDisplayMode.join(", ")}`,
|
|
).toEqual([]);
|
|
expect(
|
|
missingApiRoute,
|
|
`tools with no live API route (404): ${missingApiRoute.join(", ")}`,
|
|
).toEqual([]);
|
|
expect(
|
|
missingProcessFn,
|
|
`non-exempt tools missing process fn: ${missingProcessFn.join(", ")}`,
|
|
).toEqual([]);
|
|
}, 60_000);
|
|
|
|
it("total wired count equals TOOLS.length (dynamic, not hardcoded)", () => {
|
|
const frontendCount = TOOLS.filter((t) => toolRegistry.has(t.id)).length;
|
|
const displayModeCount = TOOLS.filter((t) => !!TOOL_DISPLAY_MODES[t.id]).length;
|
|
|
|
expect(frontendCount, `frontend registry covers ${frontendCount}/${TOOLS.length} tools`).toBe(
|
|
TOOLS.length,
|
|
);
|
|
expect(
|
|
displayModeCount,
|
|
`display-mode map covers ${displayModeCount}/${TOOLS.length} tools`,
|
|
).toBe(TOOLS.length);
|
|
});
|
|
|
|
it("REGISTRY_EXEMPT set matches actual exempt tools (no stale entries)", () => {
|
|
const catalogIds = new Set(TOOLS.map((t) => t.id));
|
|
const stale = [...REGISTRY_EXEMPT].filter((id) => !catalogIds.has(id));
|
|
expect(stale, `stale REGISTRY_EXEMPT entries: ${stale.join(", ")}`).toEqual([]);
|
|
});
|
|
});
|