Files
SnapOtter/tests/integration/generated/catalog-integrity.test.ts
T
SnapOtter 1727a7a73e test: reorganize flat test files into purpose-based subdirectories (phase 6)
Group 245 flat integration tests and 25 loose unit tests into
discoverable subdirectories per spec section 6:

  integration/tools/{image,video,audio,document,data}/  (156 files)
  integration/platform/                                  (64 files)
  integration/generated/                                 (14 files)
  integration/security/                                  (10 files)

  unit/security/     (8 files, new subdir)
  unit/api/          (7 files moved in)
  unit/web/          (3 files moved in)
  unit/shared/       (6 files moved in)
  unit/image-engine/ (1 file moved in)

All moves via git mv (history preserved). Relative imports repaired
for both depth levels (platform/generated/security = +1, tools/ = +2):
static from-imports, dynamic import() calls, vi.mock() paths,
import.meta.dirname joins, and __dirname joins.

Vitest discovery unchanged (no test.include in config, recursive glob
matches subdirs, shard-by-hash unaffected). test-server.ts and
tool-route-drift.test.ts stay at integration root. fixtures/ untouched.

Parity gate: 13189 passing test names before = 13189 after (0 dropped).
2026-06-20 05:07:46 +08:00

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 { 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: `/api/v1/tools/${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([]);
});
});