Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

224 lines · 10.7 KBRaw History
1/**
2 * The public docs: every page builds, every link and anchor resolves, the
3 * generated references match the code, and the agent formats are complete.
4 * Needs src/docs/content.generated.ts (`pnpm build` or `pnpm build:docs`).
5 */
6import { describe, expect, it } from "vitest";
7import { app } from "../../src/app.js";
8import { web } from "../../src/web/routes.js";
9import { slugify } from "../../src/web/markdown.js";
10import type { Env } from "../../src/env.js";
11import { mcpTools } from "../../src/mcp/server.js";
12import { docsSlug } from "../../src/mcp/docs.js";
13import { ACCOUNT_SCOPES, API_DOCS } from "../../src/docs/api-meta.js";
14import { allPages, getPage, headings, llmsFullTxt, llmsTxt, searchDocs, SLUGS } from "../../src/docs/pages.js";
15import { apiOperations, openApiDocument, openApiPath, OTHER_ENDPOINTS } from "../../src/docs/reference.js";
16
17// index.ts mounts the web UI last; do the same here (it can't load Durable Objects under Node).
18app.route("/", web);
19
20const ORIGIN = "https://diffinery.example";
21// An empty D1: the docs need no data, and the sitemap finds no public repositories.
22const stmt = { bind: () => stmt, all: async () => ({ results: [] }), first: async () => null, run: async () => ({ success: true, meta: {} }) };
23const env = { PUBLIC_URL: ORIGIN, DB: { prepare: () => stmt } } as unknown as Env;
24const ctx = { waitUntil() {}, passThroughOnException() {}, props: {} } as unknown as ExecutionContext;
25const get = (path: string, headers: Record<string, string> = {}) => app.fetch(new Request(`${ORIGIN}${path}`, { headers }), env, ctx);
26
27const pages = allPages(ORIGIN);
28
29/** Markdown with fenced code blocks and inline code removed, so examples aren't checked as prose. */
30function prose(md: string): string {
31 return md.replace(/^ {0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?^ {0,3}\1\s*$/gm, "").replace(/`[^`\n]*`/g, "");
32}
33
34/** Every heading id on a page (all levels). */
35function ids(md: string): Set<string> {
36 const out = new Set<string>();
37 for (const line of prose(md).split("\n")) {
38 const h = /^ {0,3}#{1,6} +(.+?)\s*#*\s*$/.exec(line);
39 if (h) out.add(slugify(h[1]!));
40 }
41 return out;
42}
43
44/** Paths outside /docs that pages may link to: real routes of the app. */
45const SITE_PATHS = [/^\/$/, /^\/llms(-full)?\.txt$/, /^\/-\/cli\/diffinery\.mjs$/, /^\/settings(\/[\w/-]*)?$/, /^\/new$/, /^\/login$/, /^\/notifications$/, /^\/auth\/access$/, /^\/mcp$/, /^\/api\/v3(\/.*)?$/, /^\/\.well-known\/[\w.-]+$/, /^\/security\/report$/, /^\/status$/, /^\/healthz$/];
46
47describe("docs pages", () => {
48 it("has every page in the navigation, in order", () => {
49 expect(pages.map((p) => p.slug)).toEqual(SLUGS);
50 expect(SLUGS.length).toBeGreaterThan(40);
51 for (const p of pages) {
52 expect(p.title, p.slug).toBeTruthy();
53 expect(p.description.length, `${p.slug} description`).toBeGreaterThan(20);
54 expect(p.markdown.length, `${p.slug} is too short`).toBeGreaterThan(400);
55 }
56 });
57
58 it.each(pages.map((p) => [p.slug, p] as const))("%s: links and anchors resolve", (slug, p) => {
59 const problems: string[] = [];
60 const text = prose(p.markdown);
61 for (const m of text.matchAll(/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) {
62 const href = m[1]!;
63 if (/^https?:\/\//.test(href)) {
64 if (href.startsWith(ORIGIN) || href.startsWith("https://diffinery.com/docs")) problems.push(`absolute link to the docs (use a /docs/... path): ${href}`);
65 continue;
66 }
67 if (href.startsWith("#")) {
68 if (!ids(p.markdown).has(href.slice(1))) problems.push(`no heading for ${href}`);
69 continue;
70 }
71 if (!href.startsWith("/")) {
72 problems.push(`relative link (use an absolute /docs/... path): ${href}`);
73 continue;
74 }
75 const [path, frag] = href.split("#") as [string, string | undefined];
76 if (path === "/docs" || path.startsWith("/docs/")) {
77 if (path === "/docs/api/openapi.json") continue;
78 const target = path === "/docs" ? "index" : path.slice(6).replace(/\.md$/, "");
79 const t = getPage(target, ORIGIN);
80 if (!t) problems.push(`no docs page for ${href}`);
81 else if (frag && !ids(t.markdown).has(frag)) problems.push(`no heading #${frag} on ${target}`);
82 } else if (!SITE_PATHS.some((re) => re.test(path))) problems.push(`link to ${path} isn't a docs page or a known site path`);
83 }
84 expect(problems.join("\n"), slug).toBe("");
85 });
86
87 it.each(pages.map((p) => [p.slug, p] as const))("%s: headings are unique and there is no raw HTML", (slug, p) => {
88 const seen = new Map<string, number>();
89 for (const h of headings(p.markdown)) seen.set(h.id, (seen.get(h.id) ?? 0) + 1);
90 expect([...seen].filter(([, n]) => n > 1).map(([id]) => id), `${slug}: duplicate headings`).toEqual([]);
91 // The renderer escapes HTML, so a tag in prose would show up as literal text. (Placeholders like <name> are fine.)
92 expect(prose(p.markdown).match(/<\/?(a|b|i|em|strong|br|p|div|span|img|table|tr|td|th|ul|ol|li|details|summary|code|pre|kbd|sup|sub|hr|h[1-6])(\s[^>]*)?\/?>/gi) ?? [], `${slug}: raw HTML`).toEqual([]);
93 });
94});
95
96describe("generated references", () => {
97 it("has notes only for endpoints that exist", () => {
98 const routes = new Set(apiOperations().map((o) => `${o.method} ${o.path}`));
99 expect(Object.keys(API_DOCS).filter((k) => !routes.has(k))).toEqual([]);
100 expect(Object.keys(ACCOUNT_SCOPES).filter((k) => !routes.has(k))).toEqual([]);
101 });
102
103 it("documents every endpoint under /api/v3", () => {
104 const ops = apiOperations();
105 expect(ops.length).toBeGreaterThan(100);
106 for (const o of ops) expect(o.doc.summary, `${o.method} ${o.path}`).toMatch(/\w/);
107 const page = getPage("api", ORIGIN)!.markdown;
108 for (const o of ops) expect(page, `${o.method} ${o.path}`).toContain(`${o.method} /api/v3${o.path === "/" ? "" : o.path}`);
109 });
110
111 it("lists other endpoints that are really routed", () => {
112 const routed = app.routes.map((r) => openApiPath(r.path));
113 for (const [endpoint] of OTHER_ENDPOINTS) {
114 const path = endpoint.replace(/^[A-Z, ]+ /, "").replace(".git/", "/").replace("{run_id}", "{runId}").replace("{owner}/{repo}", "{owner}/{repo}");
115 const want = path === "/v2/..." ? "/v2/{path}" : path;
116 const docs = ["/llms.txt", "/llms-full.txt", "/docs/{page}.md", "/docs/api/openapi.json"].includes(want);
117 expect(docs || routed.includes(want), endpoint).toBe(true);
118 }
119 });
120
121 it("builds a valid OpenAPI 3.1 document", () => {
122 const doc = openApiDocument(ORIGIN, "test") as { openapi: string; paths: Record<string, Record<string, { operationId: string }>> };
123 expect(doc.openapi).toBe("3.1.0");
124 const opIds = Object.values(doc.paths).flatMap((p) => Object.values(p).map((o) => o.operationId));
125 expect(new Set(opIds).size).toBe(opIds.length);
126 expect(opIds.length).toBe(apiOperations().length);
127 });
128
129 it("documents every MCP tool", () => {
130 const page = getPage("reference/mcp-tools", ORIGIN)!.markdown;
131 const tools = mcpTools();
132 expect(tools.length).toBeGreaterThan(20);
133 for (const t of tools) {
134 expect(t.description.length, t.name).toBeGreaterThan(10);
135 expect(page).toContain(`### ${t.name}`);
136 }
137 });
138
139 it("documents every CLI command", () => {
140 const page = getPage("reference/cli", ORIGIN)!.markdown;
141 for (const cmd of ["login", "repo create", "pr create", "pr merge", "issue create", "run list", "workflow run", "secret set", "runner", "migrate"]) {
142 expect(page, cmd).toMatch(new RegExp(`#+ diffinery ${cmd}\\b`));
143 }
144 });
145});
146
147describe("agent formats", () => {
148 it("llms.txt links every page as Markdown", () => {
149 const txt = llmsTxt(ORIGIN);
150 expect(txt.startsWith("# Diffinery\n\n> ")).toBe(true);
151 for (const s of SLUGS) expect(txt).toContain(`(${ORIGIN}/docs/${s}.md)`);
152 });
153
154 it("llms-full.txt holds every page", () => {
155 const txt = llmsFullTxt(ORIGIN);
156 for (const p of pages) expect(txt).toContain(`# ${p.title}\n\n> ${p.description}\n\nSource: ${ORIGIN}`);
157 expect(txt).not.toMatch(/\]\(\/docs/);
158 });
159
160 it("finds pages by search", () => {
161 expect(searchDocs(ORIGIN, "merge queue")[0]?.page).toBeTruthy();
162 expect(searchDocs(ORIGIN, "zzzz-not-a-word")).toEqual([]);
163 });
164
165 it("read_docs accepts slugs, paths and URLs, with .md, queries and anchors", () => {
166 for (const input of ["actions/events", "/docs/actions/events", `${ORIGIN}/docs/actions/events.md`, `${ORIGIN}/docs/actions/events.md#push`, "/docs/actions/events/?x=1#push"]) expect(docsSlug(input), input).toBe("actions/events");
167 for (const input of ["", "/docs", `${ORIGIN}/docs/`, "index.md", `${ORIGIN}/docs/index.md#start-here`]) expect(docsSlug(input), input).toBe("index");
168 });
169});
170
171describe("docs routes", () => {
172 it("serves the docs without signing in", async () => {
173 const res = await get("/docs");
174 expect(res.status).toBe(200);
175 const html = await res.text();
176 expect(html).toContain("<h1>Overview</h1>");
177 expect(html).toContain('rel="alternate" type="text/markdown" href="/docs/index.md"');
178 expect(html).not.toContain('name="robots"');
179 });
180
181 it("highlights code examples", async () => {
182 const html = await (await get("/docs/actions/workflow-syntax")).text();
183 expect(html).toMatch(/<pre[^>]*><code[^>]*>[\s\S]*?<span style="/);
184 });
185
186 it("serves every page as HTML and as Markdown", async () => {
187 for (const s of SLUGS) {
188 const html = await get(s === "index" ? "/docs" : `/docs/${s}`);
189 expect(html.status, s).toBe(200);
190 const md = await get(`/docs/${s}.md`);
191 expect(md.status, `${s}.md`).toBe(200);
192 expect(md.headers.get("content-type")).toContain("text/markdown");
193 }
194 const neg = await get("/docs/quickstart", { accept: "text/markdown, text/html;q=0.8" });
195 expect(neg.headers.get("content-type")).toContain("text/markdown");
196 // Renders and highlights every page twice: about 1 s alone, several times that on a busy CI runner.
197 }, 30_000);
198
199 it("serves llms.txt, llms-full.txt, OpenAPI, the search index and the CLI", async () => {
200 for (const [path, type] of [
201 ["/llms.txt", "text/plain"],
202 ["/llms-full.txt", "text/plain"],
203 ["/docs/api/openapi.json", "application/json"],
204 ["/docs/search.json", "application/json"],
205 ["/-/cli/diffinery.mjs", "text/javascript"],
206 ]) {
207 const res = await get(path!);
208 expect(res.status, path).toBe(200);
209 expect(res.headers.get("content-type"), path).toContain(type);
210 }
211 expect(await (await get("/-/cli/diffinery.mjs")).text()).toMatch(/^#!\/usr\/bin\/env node\n/);
212 });
213
214 it("returns 404 for unknown pages", async () => {
215 expect((await get("/docs/no-such-page")).status).toBe(404);
216 expect((await get("/docs/no-such-page.md")).status).toBe(404);
217 });
218
219 it("lists the docs in the sitemap", async () => {
220 const res = await get("/sitemap.xml");
221 expect(res.status).toBe(200);
222 expect(await res.text()).toContain(`${ORIGIN}/docs/quickstart`);
223 });
224});