mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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).
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 { 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([]);
|
|
});
|
|
});
|