Files
SnapOtter/tests/integration/platform/docs.test.ts
T
SnapOtterandGitHub f858c4cea0 feat: clearer, disambiguated tool names across all surfaces (#520)
Renames 18 ambiguous or hard-to-search tool names so image tools self-qualify like the other modalities ("Compress" becomes "Compress Image"), and cleans up a few awkward names. Propagated across search (constants.ts), display (en.ts + 20 locales), the OpenAPI base spec + 20 locale specs, and the docs tool-page headings in 21 languages. Removes the duplicate "Normalize Audio" summary shared by the video and audio endpoints. Tool ids and routes are unchanged, so no API paths or bookmarks break.
2026-07-15 21:55:51 +08:00

144 lines
5.1 KiB
TypeScript

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { apiToolPath, TOOLS } from "@snapotter/shared";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { buildTestApp, type TestApp } from "../test-server";
describe("API docs", () => {
let testApp: TestApp;
beforeAll(async () => {
testApp = await buildTestApp();
});
afterAll(async () => {
await testApp.cleanup();
});
function openApiPathSet(body: string): Set<string> {
return new Set([...body.matchAll(/^ {2}(\/[^:]+):/gm)].map((match) => match[1]));
}
it("serves the OpenAPI spec as YAML", async () => {
const res = await testApp.app.inject({
method: "GET",
url: "/api/v1/openapi.yaml",
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toContain("text/yaml");
expect(res.body).toContain("openapi: 3.1.0");
expect(res.body).toContain("SnapOtter API");
});
it("serves an ASCII-only spec (strict YAML parsers reject high-byte chars)", async () => {
// Schemathesis (and other strict YAML parsers) mis-decode multi-byte UTF-8
// sequences as C1 control characters and refuse to load the schema. Keep the
// spec ASCII-only: use '-' instead of em dashes, plain ASCII section dividers.
const res = await testApp.app.inject({ method: "GET", url: "/api/v1/openapi.yaml" });
const offending = [...res.body].find((ch) => ch.charCodeAt(0) > 0x7f);
const hint = offending
? `OpenAPI spec has non-ASCII char U+${offending
.charCodeAt(0)
.toString(16)
.padStart(4, "0")} (${JSON.stringify(offending)}); replace it with ASCII.`
: "ok";
expect(hint).toBe("ok");
});
it("serves the Scalar docs page without auth", async () => {
// Scalar redirects /api/docs -> /api/docs/ (trailing slash)
const redirect = await testApp.app.inject({
method: "GET",
url: "/api/docs",
});
expect([200, 301, 302]).toContain(redirect.statusCode);
const res = await testApp.app.inject({
method: "GET",
url: "/api/docs/",
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toContain("text/html");
});
it("includes every catalog tool endpoint in the spec", async () => {
const res = await testApp.app.inject({
method: "GET",
url: "/api/v1/openapi.yaml",
});
const paths = openApiPathSet(res.body);
const missing = TOOLS.map((tool) => apiToolPath(tool.id)).filter((path) => !paths.has(path));
expect(missing, `OpenAPI missing tool paths: ${missing.join(", ")}`).toEqual([]);
});
it("documents public docs metadata routes", async () => {
const res = await testApp.app.inject({
method: "GET",
url: "/api/v1/openapi.yaml",
});
const paths = openApiPathSet(res.body);
const expectedPaths = [
"/llms.txt",
"/llms-full.txt",
"/api/v1/openapi.yaml",
"/api/v1/tools/popular",
];
const missing = expectedPaths.filter((path) => !paths.has(path));
expect(missing, `OpenAPI missing docs metadata paths: ${missing.join(", ")}`).toEqual([]);
});
it("documents the surrounding non-tool API surface in the spec", async () => {
const res = await testApp.app.inject({
method: "GET",
url: "/api/v1/openapi.yaml",
});
const paths = openApiPathSet(res.body);
const expectedPaths = [
"/api/v1/readyz",
"/api/v1/jobs/{jobId}/cancel",
"/api/v1/preferences",
"/api/auth/mfa/enroll",
"/api/auth/oidc/login",
"/api/auth/saml/metadata",
"/api/v1/files/{id}/preview",
"/api/v1/preview/generate",
"/api/v1/admin/log-level",
"/api/v1/metrics",
"/api/v1/enterprise/scim/token",
"/api/v1/scim/v2/ServiceProviderConfig",
];
const missing = expectedPaths.filter((path) => !paths.has(path));
expect(missing, `OpenAPI missing non-tool API paths: ${missing.join(", ")}`).toEqual([]);
});
it("keeps published docs counts aligned with the live catalog", () => {
const root = process.cwd();
const gettingStarted = readFileSync(join(root, "apps/docs/guide/getting-started.md"), "utf8");
const deployment = readFileSync(join(root, "apps/docs/guide/deployment.md"), "utf8");
const architecture = readFileSync(join(root, "apps/docs/guide/architecture.md"), "utf8");
expect(gettingStarted).toContain("| **Image** | 106 |");
expect(gettingStarted).toContain("| **Video** | 57 |");
expect(gettingStarted).toContain("| **Audio** | 27 |");
expect(gettingStarted).toContain("| **PDF / Document** | 42 |");
expect(gettingStarted).toContain("| **Files** | 10 |");
expect(deployment).not.toContain("All 138 non-AI tools");
expect(architecture).toContain("242 tool routes");
});
it("serves an LLM summary with live catalog tools", async () => {
const res = await testApp.app.inject({
method: "GET",
url: "/llms.txt",
});
expect(res.statusCode).toBe(200);
expect(res.body).toContain("## Tools");
expect(res.body).toContain("- Image (106 tools)");
expect(res.body).toContain("Resize Image - Resize by pixels");
expect(res.body).toContain("Sign PDF -");
});
});