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.

371 lines · 19.3 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 { DOCS_TOOLS, docsSlug } from "../../src/mcp/docs.js";
13import { ACCOUNT_SCOPES, API_DOCS } from "../../src/docs/api-meta.js";
14import { allPages, getPage, headings, llmsFullTxt, llmsSmallTxt, llmsTxt, searchDocs, SECTIONS, sectionLlmsFullTxt, SLUGS } from "../../src/docs/pages.js";
15import { sitePage, sitePages } from "../../src/web/site.js";
16import { apiOperations, openApiDocument, openApiPath, OTHER_ENDPOINTS } from "../../src/docs/reference.js";
17
18// index.ts mounts the web UI last; do the same here (it can't load Durable Objects under Node).
19app.route("/", web);
20
21const ORIGIN = "https://diffinery.example";
22// An empty D1: the docs need no data, and the sitemap finds no public repositories.
23const stmt = { bind: () => stmt, all: async () => ({ results: [] }), first: async () => null, run: async () => ({ success: true, meta: {} }) };
24const env = { PUBLIC_URL: ORIGIN, DB: { prepare: () => stmt } } as unknown as Env;
25const ctx = { waitUntil() {}, passThroughOnException() {}, props: {} } as unknown as ExecutionContext;
26const get = (path: string, headers: Record<string, string> = {}) => app.fetch(new Request(`${ORIGIN}${path}`, { headers }), env, ctx);
27
28const pages = allPages(ORIGIN);
29
30/** Markdown with fenced code blocks and inline code removed, so examples aren't checked as prose. */
31function prose(md: string): string {
32 return md.replace(/^ {0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?^ {0,3}\1\s*$/gm, "").replace(/`[^`\n]*`/g, "");
33}
34
35/** Every heading id on a page (all levels). */
36function ids(md: string): Set<string> {
37 const out = new Set<string>();
38 for (const line of prose(md).split("\n")) {
39 const h = /^ {0,3}#{1,6} +(.+?)\s*#*\s*$/.exec(line);
40 if (h) out.add(slugify(h[1]!));
41 }
42 return out;
43}
44
45/** Paths outside /docs that pages may link to: real routes of the app. */
46const SITE_PATHS = [/^\/$/, /^\/llms(-full|-small)?\.txt$/, /^\/-\/cli\/diffinery\.mjs$/, /^\/settings(\/[\w/-]*)?$/, /^\/new$/, /^\/login$/, /^\/notifications$/, /^\/auth\/access$/, /^\/mcp$/, /^\/api\/v3(\/.*)?$/, /^\/\.well-known\/[\w.-]+$/, /^\/security\/report$/, /^\/early-access(\/remove)?$/, /^\/status$/, /^\/healthz$/];
47
48describe("docs pages", () => {
49 it("has every page in the navigation, in order", () => {
50 expect(pages.map((p) => p.slug)).toEqual(SLUGS);
51 expect(SLUGS.length).toBeGreaterThan(40);
52 for (const p of pages) {
53 expect(p.title, p.slug).toBeTruthy();
54 expect(p.description.length, `${p.slug} description`).toBeGreaterThan(20);
55 expect(p.markdown.length, `${p.slug} is too short`).toBeGreaterThan(400);
56 }
57 });
58
59 it.each(pages.map((p) => [p.slug, p] as const))("%s: links and anchors resolve", (slug, p) => {
60 const problems: string[] = [];
61 const text = prose(p.markdown);
62 for (const m of text.matchAll(/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) {
63 const href = m[1]!;
64 if (/^https?:\/\//.test(href)) {
65 if (href.startsWith(ORIGIN) || href.startsWith("https://diffinery.com/docs")) problems.push(`absolute link to the docs (use a /docs/... path): ${href}`);
66 continue;
67 }
68 if (href.startsWith("#")) {
69 if (!ids(p.markdown).has(href.slice(1))) problems.push(`no heading for ${href}`);
70 continue;
71 }
72 if (!href.startsWith("/")) {
73 problems.push(`relative link (use an absolute /docs/... path): ${href}`);
74 continue;
75 }
76 const [path, frag] = href.split("#") as [string, string | undefined];
77 if (path === "/docs" || path.startsWith("/docs/")) {
78 if (path === "/docs/api/openapi.json") continue;
79 if (path.startsWith("/docs/images/")) continue; // checked when the docs are built (scripts/build-docs.mjs)
80 const llms = /^\/docs\/([\w-]+)\/llms(-full)?\.txt$/.exec(path);
81 if (llms) {
82 if (!SECTIONS.some((x) => x.slug === llms[1])) problems.push(`no docs section for ${href}`);
83 continue;
84 }
85 const target = path === "/docs" ? "index" : path.slice(6).replace(/\.md$/, "");
86 const t = getPage(target, ORIGIN);
87 if (!t) problems.push(`no docs page for ${href}`);
88 else if (frag && !ids(t.markdown).has(frag)) problems.push(`no heading #${frag} on ${target}`);
89 } else if (sitePage(path)) {
90 // The product, comparison and company pages (site/).
91 if (frag && !ids(sitePage(path)!.markdown).has(frag)) problems.push(`no heading #${frag} on ${path}`);
92 } else if (!SITE_PATHS.some((re) => re.test(path))) problems.push(`link to ${path} isn't a docs page or a known site path`);
93 }
94 expect(problems.join("\n"), slug).toBe("");
95 });
96
97 it.each(pages.map((p) => [p.slug, p] as const))("%s: headings are unique and there is no raw HTML", (slug, p) => {
98 const seen = new Map<string, number>();
99 for (const h of headings(p.markdown)) seen.set(h.id, (seen.get(h.id) ?? 0) + 1);
100 expect([...seen].filter(([, n]) => n > 1).map(([id]) => id), `${slug}: duplicate headings`).toEqual([]);
101 // The renderer escapes HTML, so a tag in prose would show up as literal text. (Placeholders like <name> are fine.)
102 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([]);
103 });
104});
105
106describe("generated references", () => {
107 it("has notes only for endpoints that exist", () => {
108 const routes = new Set(apiOperations().map((o) => `${o.method} ${o.path}`));
109 expect(Object.keys(API_DOCS).filter((k) => !routes.has(k))).toEqual([]);
110 expect(Object.keys(ACCOUNT_SCOPES).filter((k) => !routes.has(k))).toEqual([]);
111 });
112
113 it("documents every endpoint under /api/v3", () => {
114 const ops = apiOperations();
115 expect(ops.length).toBeGreaterThan(100);
116 for (const o of ops) expect(o.doc.summary, `${o.method} ${o.path}`).toMatch(/\w/);
117 const page = getPage("api", ORIGIN)!.markdown;
118 for (const o of ops) expect(page, `${o.method} ${o.path}`).toContain(`${o.method} /api/v3${o.path === "/" ? "" : o.path}`);
119 });
120
121 it("lists other endpoints that are really routed", () => {
122 const routed = app.routes.map((r) => openApiPath(r.path));
123 for (const [endpoint] of OTHER_ENDPOINTS) {
124 const path = endpoint.replace(/^[A-Z, ]+ /, "").replace(".git/", "/").replace("{run_id}", "{runId}").replace("{owner}/{repo}", "{owner}/{repo}");
125 const want = path === "/v2/..." ? "/v2/{path}" : path;
126 const docs = ["/llms.txt", "/llms-full.txt", "/docs/{page}.md", "/docs/api/openapi.json"].includes(want);
127 expect(docs || routed.includes(want), endpoint).toBe(true);
128 }
129 });
130
131 it("builds a valid OpenAPI 3.1 document", () => {
132 const doc = openApiDocument(ORIGIN, "test") as { openapi: string; paths: Record<string, Record<string, { operationId: string }>> };
133 expect(doc.openapi).toBe("3.1.0");
134 const opIds = Object.values(doc.paths).flatMap((p) => Object.values(p).map((o) => o.operationId));
135 expect(new Set(opIds).size).toBe(opIds.length);
136 expect(opIds.length).toBe(apiOperations().length);
137 });
138
139 it("documents every MCP tool", () => {
140 const page = getPage("reference/mcp-tools", ORIGIN)!.markdown;
141 const tools = mcpTools();
142 expect(tools.length).toBeGreaterThan(20);
143 for (const t of tools) {
144 expect(t.description.length, t.name).toBeGreaterThan(10);
145 expect(page).toContain(`### ${t.name}`);
146 }
147 });
148
149 it("documents every CLI command", () => {
150 const page = getPage("reference/cli", ORIGIN)!.markdown;
151 for (const cmd of ["login", "repo create", "pr create", "pr merge", "issue create", "run list", "workflow run", "secret set", "runner", "migrate"]) {
152 expect(page, cmd).toMatch(new RegExp(`#+ diffinery ${cmd}\\b`));
153 }
154 });
155});
156
157describe("agent formats", () => {
158 it("llms.txt links every page as Markdown", () => {
159 const txt = llmsTxt(ORIGIN);
160 expect(txt.startsWith("# Diffinery\n\n> ")).toBe(true);
161 for (const s of SLUGS) expect(txt).toContain(`(${ORIGIN}/docs/${s}.md)`);
162 });
163
164 it("llms-full.txt holds every page", () => {
165 const txt = llmsFullTxt(ORIGIN);
166 for (const p of pages) expect(txt).toContain(`# ${p.title}\n\n> ${p.description}\n\nSource: ${ORIGIN}`);
167 expect(txt).not.toMatch(/\]\(\/docs/);
168 });
169
170 it("finds pages by search, the site's comparison pages included", () => {
171 expect(searchDocs(ORIGIN, "merge queue")[0]?.page).toBeTruthy();
172 expect(searchDocs(ORIGIN, "zzzz-not-a-word")).toEqual([]);
173 const hit = searchDocs(ORIGIN, "blacksmith")[0]!;
174 expect(hit.page).toBe("/compare/blacksmith");
175 expect(hit.url).toMatch(new RegExp(`^${ORIGIN}/compare/blacksmith(#|$)`));
176 expect(hit.markdown_url).toBe(`${ORIGIN}/compare/blacksmith.md`);
177 });
178
179 it("read_docs reads site pages by path or URL", async () => {
180 for (const page of ["/compare/github", "compare/github", `${ORIGIN}/compare/github.md`]) {
181 const r = (await DOCS_TOOLS.read_docs!.run({ env } as never, { page })) as { page: string; markdown: string };
182 expect(r.page, page).toBe("/compare/github");
183 expect(r.markdown.startsWith(`# ${sitePage("/compare/github")!.title}\n`)).toBe(true);
184 }
185 });
186
187 it("splits the docs into one llms file per section, and a small file with the site and the essentials", () => {
188 const full = llmsTxt(ORIGIN);
189 for (const sec of SECTIONS) {
190 expect(full).toContain(`${ORIGIN}/docs/${sec.slug}/llms-full.txt`);
191 const txt = sectionLlmsFullTxt(ORIGIN, sec.slug)!;
192 for (const slug of sec.pages) expect(txt).toContain(`Source: ${ORIGIN}${slug === "index" ? "/docs" : `/docs/${slug}`}\n`);
193 }
194 expect(sectionLlmsFullTxt(ORIGIN, "no-such-section")).toBeNull();
195 const small = llmsSmallTxt(ORIGIN);
196 for (const p of sitePages()) expect(small).toContain(`Source: ${ORIGIN}${p.path} (last updated`);
197 expect(small).toContain(`Source: ${ORIGIN}/docs/quickstart\n`);
198 expect(small).not.toContain("# REST API reference\n");
199 // Small enough for an assistant's context: well under a tenth of llms-full.txt.
200 expect(small.length).toBeLessThan(llmsFullTxt(ORIGIN).length / 4);
201 });
202
203 it("read_docs accepts slugs, paths and URLs, with .md, queries and anchors", () => {
204 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");
205 for (const input of ["", "/docs", `${ORIGIN}/docs/`, "index.md", `${ORIGIN}/docs/index.md#start-here`]) expect(docsSlug(input), input).toBe("index");
206 });
207});
208
209describe("docs routes", () => {
210 it("serves the docs without signing in", async () => {
211 const res = await get("/docs");
212 expect(res.status).toBe(200);
213 const html = await res.text();
214 expect(html).toContain("<h1>Overview</h1>");
215 expect(html).toContain('rel="alternate" type="text/markdown" href="/docs/index.md"');
216 expect(html).not.toContain('name="robots"');
217 });
218
219 it("highlights code examples", async () => {
220 const html = await (await get("/docs/actions/workflow-syntax")).text();
221 expect(html).toMatch(/<pre[^>]*><code[^>]*>[\s\S]*?<span style="/);
222 });
223
224 it("serves every page as HTML and as Markdown", async () => {
225 for (const s of SLUGS) {
226 const html = await get(s === "index" ? "/docs" : `/docs/${s}`);
227 expect(html.status, s).toBe(200);
228 const md = await get(`/docs/${s}.md`);
229 expect(md.status, `${s}.md`).toBe(200);
230 expect(md.headers.get("content-type")).toContain("text/markdown");
231 }
232 const neg = await get("/docs/quickstart", { accept: "text/markdown, text/html;q=0.8" });
233 expect(neg.headers.get("content-type")).toContain("text/markdown");
234 // Renders and highlights every page twice: about 1 s alone, several times that on a busy CI runner.
235 }, 30_000);
236
237 it("serves llms.txt, llms-full.txt, OpenAPI, the search index and the CLI", async () => {
238 for (const [path, type] of [
239 ["/llms.txt", "text/plain"],
240 ["/llms-full.txt", "text/plain"],
241 ["/llms-small.txt", "text/plain"],
242 ["/docs/actions/llms.txt", "text/plain"],
243 ["/docs/actions/llms-full.txt", "text/plain"],
244 ["/docs/api/openapi.json", "application/json"],
245 ["/docs/search.json", "application/json"],
246 ["/-/cli/diffinery.mjs", "text/javascript"],
247 ]) {
248 const res = await get(path!);
249 expect(res.status, path).toBe(200);
250 expect(res.headers.get("content-type"), path).toContain(type);
251 }
252 expect(await (await get("/-/cli/diffinery.mjs")).text()).toMatch(/^#!\/usr\/bin\/env node\n/);
253 });
254
255 it("serves the install scripts, pointed at this server", async () => {
256 for (const path of ["/install", "/install.sh"]) {
257 const res = await get(path);
258 expect(res.status, path).toBe(200);
259 expect(res.headers.get("content-type")).toContain("text/plain");
260 const sh = await res.text();
261 expect(sh).toMatch(/^#!\/usr\/bin\/env bash\n/);
262 expect(sh).toContain(`server=\${DIFFINERY_URL:-${ORIGIN}}`);
263 expect(sh).not.toContain("__DIFFINERY_URL__");
264 expect(sh).toContain(`curl -fsSL ${ORIGIN}/install | bash`); // what to run when piped into sh
265 }
266 const ps1 = await (await get("/install.ps1")).text();
267 expect(ps1).toContain(`'${ORIGIN}'`);
268 expect(ps1).toContain("/-/cli/diffinery.mjs");
269 });
270
271 it("serves the JetBrains plugin and the plugin repository the IDEs install and update it from", async () => {
272 const xml = await get("/-/jetbrains/updatePlugins.xml");
273 expect(xml.status).toBe(200);
274 expect(xml.headers.get("content-type")).toContain("application/xml");
275 const body = await xml.text();
276 const plugin = body.match(/<plugin id="com\.diffinery" url="([^"]+)" version="([^"]+)">/);
277 expect(plugin, body).not.toBeNull();
278 const [, url, version] = plugin!;
279 expect(url).toBe(`${ORIGIN}/-/jetbrains/diffinery-jetbrains-${version}.zip`);
280 expect(body).toMatch(/<idea-version since-build="\d+"\/>/);
281 expect(body).toContain(`${ORIGIN}/docs/repositories/clone-in-jetbrains-ides`);
282 for (const path of [new URL(url).pathname, "/-/jetbrains/diffinery-jetbrains.zip"]) {
283 const zip = await get(path);
284 expect(zip.status, path).toBe(200);
285 expect(zip.headers.get("x-diffinery-plugin-version")).toBe(version);
286 const bytes = new Uint8Array(await zip.arrayBuffer());
287 expect(bytes.slice(0, 4)).toEqual(new Uint8Array([0x50, 0x4b, 0x03, 0x04]));
288 const sha = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (b) => b.toString(16).padStart(2, "0")).join("");
289 expect(zip.headers.get("x-diffinery-plugin-sha256")).toBe(sha);
290 }
291 });
292
293 it("serves the Claude Code plugins for claude.ai's Upload plugin", async () => {
294 for (const name of ["diffinery", "diffinery-ci"]) {
295 const latest = await get(`/-/claude/${name}.zip`);
296 expect(latest.status, name).toBe(200);
297 expect(latest.headers.get("content-type")).toBe("application/zip");
298 const version = latest.headers.get("x-diffinery-plugin-version")!;
299 expect(version).toMatch(/^\d+\.\d+\.\d+$/);
300 expect(latest.headers.get("content-disposition")).toBe(`attachment; filename="${name}-${version}.zip"`);
301 const bytes = new Uint8Array(await latest.arrayBuffer());
302 expect(bytes.slice(0, 4)).toEqual(new Uint8Array([0x50, 0x4b, 0x03, 0x04]));
303 const sha = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (b) => b.toString(16).padStart(2, "0")).join("");
304 expect(latest.headers.get("x-diffinery-plugin-sha256")).toBe(sha);
305 const pinned = await get(`/-/claude/${name}-${version}.zip`);
306 expect(new Uint8Array(await pinned.arrayBuffer())).toEqual(bytes);
307 expect((await get(`/-/claude/${name}-0.0.0.zip`)).status).toBe(404);
308 }
309 for (const path of ["/-/claude/other.zip", "/-/claude/diffinery", "/-/claude/constructor.zip"]) expect((await get(path)).status, path).toBe(404);
310 });
311
312 it("serves the screenshots in docs/images", async () => {
313 const res = await get("/docs/images/connected-apps.webp");
314 expect(res.status).toBe(200);
315 expect(res.headers.get("content-type")).toBe("image/webp");
316 expect(new Uint8Array(await res.arrayBuffer()).slice(8, 12)).toEqual(new TextEncoder().encode("WEBP"));
317 expect((await get("/docs/images/nope.webp")).status).toBe(404);
318 expect((await get("/docs/images/constructor")).status).toBe(404);
319 // Pages show them through their own origin, not the image proxy.
320 expect(await (await get("/docs/connect")).text()).toContain('<img src="/docs/images/connected-apps.webp"');
321 });
322
323 it("sends moved pages to their new address, as HTML and as Markdown", async () => {
324 const html = await get("/docs/agents/claude-and-codex");
325 expect(html.status).toBe(301);
326 expect(html.headers.get("location")).toBe("/docs/connect");
327 expect((await get("/docs/agents/claude-and-codex.md")).headers.get("location")).toBe("/docs/connect.md");
328 });
329
330 it("points agents at the connect guides, the plugin and gh from llms.txt", () => {
331 const txt = llmsTxt(ORIGIN);
332 expect(txt).toContain(`${ORIGIN}/docs/connect.md`);
333 expect(txt).toContain(`codex plugin marketplace add ${ORIGIN}/codician/diffinery.git`);
334 expect(txt).toContain(`\`diffinery setup\` adds both`);
335 expect(txt).toContain(`${ORIGIN}/docs/connect/ci-status-in-claude.md`);
336 expect(txt).toContain("diffinery auth setup-gh");
337 });
338
339 it("returns 404 for unknown pages", async () => {
340 expect((await get("/docs/no-such-page")).status).toBe(404);
341 expect((await get("/docs/no-such-page.md")).status).toBe(404);
342 });
343
344 it("lists the docs in the sitemap, with the date a page was last reviewed when it has one", async () => {
345 const res = await get("/sitemap.xml");
346 expect(res.status).toBe(200);
347 const xml = await res.text();
348 expect(xml).toContain(`${ORIGIN}/docs/quickstart`);
349 const dated = pages.find((p) => p.updated)!;
350 expect(xml).toContain(`<url><loc>${ORIGIN}/docs/${dated.slug}</loc><lastmod>${dated.updated}</lastmod></url>`);
351 });
352
353 it("returns 404 for an unknown section's llms files", async () => {
354 expect((await get("/docs/no-such-section/llms-full.txt")).status).toBe(404);
355 expect((await get("/docs/no-such-section/llms.txt")).status).toBe(404);
356 });
357
358 it("gives every page structured data: a TechArticle with breadcrumbs, its review date and its questions", async () => {
359 for (const p of pages) {
360 const html = await (await get(p.slug === "index" ? "/docs" : `/docs/${p.slug}`)).text();
361 const m = /<script type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/.exec(html);
362 expect(m, p.slug).toBeTruthy();
363 const g = (JSON.parse(m![1]!) as { "@graph": Array<Record<string, unknown>> })["@graph"];
364 const article = g.find((x) => x["@type"] === "TechArticle")!;
365 expect(article.headline, p.slug).toBe(p.title);
366 expect(article.dateModified, p.slug).toBe(p.updated);
367 expect(g.some((x) => x["@type"] === "BreadcrumbList"), p.slug).toBe(true);
368 expect(g.some((x) => x["@type"] === "FAQPage"), p.slug).toBe(/^## Frequently asked questions\s*$/m.test(p.markdown));
369 }
370 }, 30_000);
371});