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.

234 lines · 13.0 KBRaw History
1/**
2 * The public site: the home page and the product, comparison and company pages in site/. Every page
3 * renders for visitors with its search and social tags and structured data, is served as Markdown,
4 * links only to pages that exist, and is listed in the sitemap, llms.txt and llms-full.txt.
5 * Needs src/docs/content.generated.ts (`pnpm build` or `pnpm build:docs`).
6 */
7import { describe, expect, it } from "vitest";
8import { app } from "../../src/app.js";
9import { web } from "../../src/web/routes.js";
10import { slugify } from "../../src/web/markdown.js";
11import type { Env } from "../../src/env.js";
12import { getPage, llmsFullTxt, llmsTxt } from "../../src/docs/pages.js";
13import { CLOUDFLARE_CLAIM, faqs, homeMarkdown, sitePage, sitePages } from "../../src/web/site.js";
14import { RESERVED_LOGINS } from "../../src/lib/util.js";
15
16app.route("/", web);
17
18const ORIGIN = "https://diffinery.example";
19const stmt = { bind: () => stmt, all: async () => ({ results: [] }), first: async () => null, run: async () => ({ success: true, meta: {} }) };
20const env = { PUBLIC_URL: ORIGIN, DB: { prepare: () => stmt } } as unknown as Env;
21const ctx = { waitUntil() {}, passThroughOnException() {}, props: {} } as unknown as ExecutionContext;
22const get = (path: string, headers: Record<string, string> = {}) => app.fetch(new Request(`${ORIGIN}${path}`, { headers }), env, ctx);
23
24const pages = sitePages();
25
26/** The JSON-LD graph of an HTML page. */
27function graph(html: string): Array<Record<string, unknown>> {
28 const m = /<script type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/.exec(html);
29 expect(m, "structured data").toBeTruthy();
30 const ld = JSON.parse(m![1]!) as { "@context": string; "@graph": Array<Record<string, unknown>> };
31 expect(ld["@context"]).toBe("https://schema.org");
32 return ld["@graph"];
33}
34
35const types = (g: Array<Record<string, unknown>>) => g.map((x) => x["@type"]);
36
37/** Markdown without code, so examples aren't checked as links. */
38const prose = (md: string) => md.replace(/^```[\s\S]*?^```/gm, "").replace(/`[^`\n]*`/g, "");
39
40/** Paths outside /docs and the site pages that the site may link to. */
41const OTHER_PATHS = [/^\/$/, /^\/llms(-full)?\.txt$/, /^\/early-access$/, /^\/status$/, /^\/security\/report$/, /^\/\.well-known\/security\.txt$/, /^\/docs\/api\/openapi\.json$/];
42
43function linkProblems(md: string): string[] {
44 const problems: string[] = [];
45 for (const m of prose(md).matchAll(/\]\(([^)\s]+)\)/g)) {
46 const href = m[1]!;
47 if (/^https?:\/\//.test(href)) {
48 if (!href.startsWith("https://")) problems.push(`insecure link: ${href}`);
49 continue;
50 }
51 if (!href.startsWith("/")) {
52 problems.push(`relative link: ${href}`);
53 continue;
54 }
55 const [path, frag] = href.split("#") as [string, string | undefined];
56 if (path === "/docs" || path.startsWith("/docs/")) {
57 if (OTHER_PATHS.some((re) => re.test(path))) continue;
58 const doc = getPage(path === "/docs" ? "index" : path.slice(6), ORIGIN);
59 if (!doc) problems.push(`no docs page ${path}`);
60 else if (frag && !doc.markdown.split("\n").some((l) => /^#{2,6} /.test(l) && slugify(l.replace(/^#+ /, "")) === frag)) problems.push(`no heading ${href}`);
61 continue;
62 }
63 const site = sitePage(path);
64 if (site) {
65 if (frag && !site.markdown.split("\n").some((l) => /^#{2,6} /.test(l) && slugify(l.replace(/^#+ /, "")) === frag)) problems.push(`no heading ${href}`);
66 continue;
67 }
68 if (!OTHER_PATHS.some((re) => re.test(path))) problems.push(`link to ${path} isn't a page`);
69 }
70 return problems;
71}
72
73describe("site pages", () => {
74 it("has the product, comparison and company pages", () => {
75 expect(pages.map((p) => p.path)).toEqual(expect.arrayContaining(["/features", "/agents", "/performance", "/security", "/built-on-cloudflare", "/about", "/faq", "/blog", "/compare", "/compare/github", "/compare/gitlab", "/compare/blacksmith", "/compare/depot"]));
76 for (const p of pages) {
77 expect(p.description.length, p.path).toBeLessThanOrEqual(160);
78 expect(p.updated, p.path).toMatch(/^\d{4}-\d{2}-\d{2}$/);
79 // The blog's index only lists its posts.
80 if (p.path !== "/blog") expect(p.markdown.length, `${p.path} is too short`).toBeGreaterThan(1500);
81 }
82 });
83
84 it("keeps the pages' top-level paths away from account names", () => {
85 for (const p of pages) expect(RESERVED_LOGINS.has(p.path.split("/")[1]!), p.path).toBe(true);
86 });
87
88 it.each(pages.map((p) => [p.path, p] as const))("%s: links resolve", (_path, p) => {
89 expect(linkProblems(p.markdown)).toEqual([]);
90 });
91
92 it("home page links resolve", () => {
93 expect(linkProblems(homeMarkdown(""))).toEqual([]);
94 });
95
96 it("every comparison names its sources and the date they were checked", () => {
97 for (const p of pages.filter((x) => x.path.startsWith("/compare/"))) {
98 expect(p.markdown, p.path).toMatch(/\[source\]\(https:\/\//);
99 // The date the other product's facts were checked, written out ("checked on October 16, 2026").
100 expect(p.markdown, p.path).toMatch(/(January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}/);
101 expect(faqs(p.markdown).length, `${p.path} questions`).toBeGreaterThan(0);
102 }
103 });
104
105 it.each(pages.map((p) => [p.path, p] as const))("%s: renders for visitors with search tags and structured data", async (path, p) => {
106 const res = await get(path);
107 expect(res.status).toBe(200);
108 expect(res.headers.get("link")).toContain(`<${ORIGIN}${path}.md>; rel="alternate"; type="text/markdown"`);
109 const html = await res.text();
110 expect(html).toContain(`<link rel="canonical" href="${ORIGIN}${path}"/>`);
111 expect(html).not.toContain('name="robots"');
112 expect(html.match(/<h1[ >]/g)?.length).toBe(1);
113 expect(html).toContain(`<meta name="description" content="`);
114 expect(html).toContain('href="/features"');
115 const g = graph(html);
116 expect(types(g)).toEqual(expect.arrayContaining(["Organization", "WebSite", "SoftwareApplication", "BreadcrumbList"]));
117 const page = g.find((x) => x["@id"] === `${ORIGIN}${path}#page`)!;
118 expect(page.dateModified).toBe(p.updated);
119 const qs = faqs(p.markdown);
120 const faq = g.find((x) => x["@type"] === "FAQPage") as { mainEntity: Array<{ name: string }> } | undefined;
121 expect(faq?.mainEntity.length ?? 0).toBe(qs.length);
122 });
123
124 it.each(pages.map((p) => [p.path, p] as const))("%s: is served as Markdown", async (path, p) => {
125 for (const res of [await get(`${path}.md`), await get(path, { accept: "text/markdown" })]) {
126 expect(res.status).toBe(200);
127 expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8");
128 const md = await res.text();
129 expect(md.startsWith(`# ${p.title}\n\n> ${p.description}\n\nSource: ${ORIGIN}${path} (last updated ${p.updated})`)).toBe(true);
130 expect(md).not.toMatch(/\]\(\/(?!\/)/);
131 }
132 });
133
134 it("marks blog posts up as BlogPostings with the date they went out", async () => {
135 const post = pages.find((p) => p.path.startsWith("/blog/"))!;
136 const g = graph(await (await get(post.path)).text());
137 const page = g.find((x) => x["@id"] === `${ORIGIN}${post.path}#page`)!;
138 expect(page["@type"]).toBe("BlogPosting");
139 expect(page.datePublished).toBe(post.published ?? post.updated);
140 const crumbs = g.find((x) => x["@type"] === "BreadcrumbList") as { itemListElement: Array<{ name: string }> };
141 expect(crumbs.itemListElement.map((c) => c.name)).toEqual(["Home", "Blog", post.title]);
142 });
143
144 it("sends a trailing slash to the page", async () => {
145 const res = await get("/compare/github/");
146 expect(res.status).toBe(301);
147 expect(res.headers.get("location")).toBe("/compare/github");
148 });
149});
150
151describe("home page", () => {
152 it("renders for visitors with the product, its questions and a Markdown version", async () => {
153 const res = await get("/");
154 expect(res.status).toBe(200);
155 expect(res.headers.get("link")).toContain(`<${ORIGIN}/index.md>; rel="alternate"; type="text/markdown"`);
156 const html = await res.text();
157 expect(html).toContain("<title>Diffinery: open-source GitHub alternative with fast CI, built for AI agents</title>");
158 expect(html).toContain("Code hosting and CI for the agentic era</h1>");
159 const g = graph(html);
160 expect(types(g)).toEqual(expect.arrayContaining(["Organization", "WebSite", "SoftwareApplication", "SoftwareSourceCode", "WebPage", "FAQPage"]));
161 // Open source: the license and the repositories, no pricing, and early access to try it on diffinery.com.
162 const source = g.find((n) => n["@type"] === "SoftwareSourceCode")!;
163 expect(source).toMatchObject({ codeRepository: "https://diffinery.com/codician/diffinery", license: "https://www.apache.org/licenses/LICENSE-2.0", sameAs: ["https://github.com/codician-team/diffinery"] });
164 expect(g.find((n) => n["@type"] === "SoftwareApplication")).toMatchObject({ license: "https://www.apache.org/licenses/LICENSE-2.0", isAccessibleForFree: true, installUrl: `${ORIGIN}/docs/self-hosting/deploy`, downloadUrl: "https://diffinery.com/codician/diffinery/releases" });
165 expect(html).toContain('href="https://diffinery.com/early-access"');
166 const faq = g.find((n) => n["@type"] === "FAQPage") as { mainEntity: Array<{ name: string; acceptedAnswer: { text: string } }> };
167 expect(faq.mainEntity.find((q) => q.name === "Can I try Diffinery without deploying it?")?.acceptedAnswer.text).toContain("Request early access to diffinery.com");
168 expect(html).not.toMatch(/not been announced|hasn't been announced|invite-only while/i);
169 for (const href of ["/compare/github", "/compare/blacksmith", "/performance", "/agents", "/about", "/llms.txt", "/open-source", "/docs/self-hosting", "https://diffinery.com/codician/diffinery", "https://github.com/codician-team/diffinery"]) expect(html, href).toContain(`href="${href}"`);
170 });
171
172 it("doesn't load the Kumo bundle, which only pages with islands use", async () => {
173 expect(await (await get("/")).text()).not.toContain("/-/kumo/");
174 });
175
176 it("is served as Markdown", async () => {
177 for (const res of [await get("/index.md"), await get("/", { accept: "text/markdown" })]) {
178 expect(res.status).toBe(200);
179 expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8");
180 const md = await res.text();
181 expect(md).toContain("## Frequently asked questions");
182 expect(md).toContain(`(${ORIGIN}/compare/github)`);
183 }
184 });
185});
186
187describe("sign-in page", () => {
188 // The Deploy workflow checks that /login names both bundle files and that they answer.
189 it("prefetches the Kumo bundle for the pages after it, without loading it", async () => {
190 const html = await (await get("/login")).text();
191 expect(html).toContain("Sign in with a passkey");
192 expect(html).toMatch(/<link rel="prefetch" href="\/-\/kumo\/[a-z0-9]+\.css" as="style"\/?>/);
193 expect(html).toMatch(/<link rel="prefetch" href="\/-\/kumo\/[a-z0-9]+\.js" as="script"\/?>/);
194 expect(html).not.toMatch(/<(?:script|link rel="stylesheet")[^>]*\/-\/kumo\//);
195 });
196});
197
198describe("crawlers and AI assistants", () => {
199 it("lists every site page in llms.txt, before the docs", () => {
200 const txt = llmsTxt(ORIGIN);
201 expect(txt).toContain(`(${ORIGIN}/index.md)`);
202 for (const p of pages) expect(txt).toContain(`(${ORIGIN}${p.path}.md)`);
203 expect(txt.indexOf(`${ORIGIN}/compare/github.md`)).toBeLessThan(txt.indexOf(`${ORIGIN}/docs/quickstart.md`));
204 expect(txt).toContain("request early access at https://diffinery.com/early-access");
205 });
206
207 it("states the Cloudflare claim the same way everywhere, backed by the sourced list of other projects", async () => {
208 expect(llmsTxt(ORIGIN)).toContain(CLOUDFLARE_CLAIM);
209 const g = graph(await (await get("/")).text());
210 expect((g.find((x) => x["@type"] === "SoftwareApplication")!.featureList as string[]).join(" ")).toContain(CLOUDFLARE_CLAIM);
211 const page = sitePage("/built-on-cloudflare")!;
212 expect(page.markdown).toContain("## Other Git and CI projects on Cloudflare");
213 expect(page.markdown.split("## Other Git and CI projects on Cloudflare")[1]!.split("\n## ")[0]!.match(/\[source\]\(https:\/\//g)!.length).toBeGreaterThan(5);
214 });
215
216 it("holds every site page in llms-full.txt", () => {
217 const txt = llmsFullTxt(ORIGIN);
218 expect(txt).toContain(`Source: ${ORIGIN}/ (last updated`);
219 for (const p of pages) expect(txt).toContain(`# ${p.title}\n\n> ${p.description}\n\nSource: ${ORIGIN}${p.path}`);
220 });
221
222 it("lists the site in the sitemap with the date each page was checked", async () => {
223 const xml = await (await get("/sitemap.xml")).text();
224 expect(xml.match(new RegExp(`<loc>${ORIGIN}/</loc>`, "g"))?.length).toBe(1);
225 for (const p of pages) expect(xml).toContain(`<url><loc>${ORIGIN}${p.path}</loc><lastmod>${p.updated}</lastmod></url>`);
226 });
227
228 it("welcomes search engines and AI assistants in robots.txt", async () => {
229 const txt = await (await get("/robots.txt")).text();
230 expect(txt).toContain("User-agent: *\nContent-Signal: search=yes, ai-input=yes, ai-train=yes\nAllow: /");
231 expect(txt).toContain(`Sitemap: ${ORIGIN}/sitemap.xml`);
232 for (const p of pages) expect(txt).not.toMatch(new RegExp(`^Disallow: ${p.path}$`, "m"));
233 });
234});