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.

Tests: Markdown speed checks that don't depend on the host, and an email e2e claim timeout that says why

The Markdown linear-time checks asserted under 500 ms of wall clock; on a slow CI host one took 558 ms
(#267's unit job). Each input is now compared with ordinary Markdown of the same length rendered on the
same machine (best of three, under 25 times as long), as #203 did for the pack tests: the inputs take 0.1
to 5 times as long as prose, a backtracking pattern hundreds of times.

The email e2e's self-hosted job once never became claimable (#267, 10:48), and the timeout printed only
the claim polling's own log lines. It now prints what the push said and where the repository's runs and
their jobs stood, and leaves the polling lines out of the log tail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191bbNxX5G8d7osLx1A83WC
Claude authored signed1 parent 683b739commit 32c4b64f7323c8f4968f9a17f281bec2dfebcf77Browse files
2 files changed+73−20
modifiedapps/diffinery/test/e2e/email.test.mjs+47−13View file
@@ -12,7 +12,7 @@
1212 // and complaint intake, no email to a suppressed address, lifting it again; the
1313 // morning digest on its schedule (phase 5c) and Today at phone width.
1414 import "./fresh-connections.mjs";
15-import { spawn, execFileSync } from "node:child_process";
15+import { spawn, spawnSync, execFileSync } from "node:child_process";
1616 import { mkdtempSync, writeFileSync, mkdirSync, existsSync, rmSync } from "node:fs";
1717 import { tmpdir } from "node:os";
1818 import { createHmac, randomBytes } from "node:crypto";
@@ -77,7 +77,8 @@
7777 }
7878
7979 const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
80-async function until(what, fn, timeoutMs = 60_000) {
80+/** `explain`, if given, says on a timeout where things stood (the polling's own log lines are left out of the tail). */
81+async function until(what, fn, timeoutMs = 60_000, explain = async () => "") {
8182 const end = Date.now() + timeoutMs;
8283 let last;
8384 while (Date.now() < end) {
@@ -85,9 +86,38 @@
8586 if (last) return last;
8687 await sleep(500);
8788 }
88- assert.fail(`timed out waiting for ${what}\n--- wrangler log tail ---\n${devLog.slice(-4000)}`);
89+ const tail = devLog
90+ .split("\n")
91+ .filter((l) => !/runners\/claim 204/.test(l))
92+ .join("\n")
93+ .slice(-4000);
94+ assert.fail(`timed out waiting for ${what}\n${await explain().catch((e) => `(couldn't say where things stood: ${e.message})`)}\n--- wrangler log tail ---\n${tail}`);
8995 }
9096
97+/**
98+ * Waits for a self-hosted job of the repository to claim; on a timeout, says what the push printed and
99+ * where the repository's runs and their jobs stood, so a job that never became claimable can be told apart.
100+ */
101+async function claimJob(repoPath, pushed = "") {
102+ return until(
103+ "a job to claim",
104+ async () => {
105+ const r = await runnerClaim(repoPath, ["linux"]);
106+ return r.status === 200 ? r.json : null;
107+ },
108+ 60_000,
109+ async () => {
110+ const runs = (await ok("GET", `${repoPath}/actions/runs?per_page=10`)).workflow_runs;
111+ const lines = [`push output: ${pushed.trim() || "(none)"}`, `runs: ${runs.length}`];
112+ for (const r of runs) {
113+ const jobs = (await ok("GET", `${repoPath}/actions/runs/${r.id}/jobs?filter=all`)).jobs;
114+ lines.push(`- run ${r.run_number} ${r.head_sha.slice(0, 7)} ${r.status}/${r.conclusion}: ${jobs.map((j) => `${j.name} ${j.status}/${j.conclusion}`).join(", ") || "no jobs"}`);
115+ }
116+ return lines.join("\n");
117+ },
118+ );
119+}
120+
91121 /** My emails, newest first, with the full message (kept in local development only). */
92122 const emails = async (q = "") => ok("GET", `/diffinery/emails?per_page=200${q}`);
93123 /** Wait for an email matching `pred` and return it. */
@@ -103,6 +133,18 @@
103133 });
104134 }
105135
136+/** Pushes main and returns what the server said (a workflow that failed to start says so here). */
137+function gitPush(cwd) {
138+ const auth = Buffer.from(`x-token:${token}`).toString("base64");
139+ const r = spawnSync("git", ["-c", `http.extraHeader=Authorization: Basic ${auth}`, "-c", "protocol.version=2", "push", "origin", "main"], {
140+ cwd,
141+ encoding: "utf8",
142+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
143+ });
144+ assert.equal(r.status, 0, `git push failed: ${r.stderr}`);
145+ return r.stderr.split("\n").filter((l) => l.startsWith("remote:")).join("\n");
146+}
147+
106148 function spawnAsync(cmd, args, env, timeoutMs) {
107149 return new Promise((done) => {
108150 const child = spawn(cmd, args, { env: { ...process.env, ...env } });
@@ -277,11 +319,7 @@
277319 git(src, ["add", "-A"]);
278320 git(src, ["commit", "-q", "-m", "Add CI that fails"]);
279321 git(src, ["remote", "add", "origin", `${base}/${owner}/mailbox.git`]);
280- git(src, ["push", "-q", "origin", "main"]);
281- const claim = await until("a job to claim", async () => {
282- const r = await runnerClaim(`/repos/${owner}/mailbox`, ["linux"]);
283- return r.status === 200 ? r.json : null;
284- });
322+ const claim = await claimJob(`/repos/${owner}/mailbox`, gitPush(src));
285323 await spawnAsync(process.execPath, [AGENT], { DIFFINERY_API_URL: claim.api_url, DIFFINERY_JOB_ID: claim.job_id, DIFFINERY_JOB_TOKEN: claim.token, DIFFINERY_WORK_ROOT: join(work, `job-${claim.job_id}`) }, 180_000);
286324 const failed = await email("run failed", (e) => e.kind === "run" && e.subject.includes(`${owner}/mailbox`));
287325 assert.equal(failed.status, "sent");
@@ -294,11 +332,7 @@
294332 writeFileSync(join(src, file), `${file}\n`);
295333 git(src, ["add", "-A"]);
296334 git(src, ["commit", "-q", "-m", `Add ${file}`]);
297- git(src, ["push", "-q", "origin", "main"]);
298- const c = await until("a job to claim", async () => {
299- const r = await runnerClaim(`/repos/${owner}/mailbox`, ["linux"]);
300- return r.status === 200 ? r.json : null;
301- });
335+ const c = await claimJob(`/repos/${owner}/mailbox`, gitPush(src));
302336 await spawnAsync(process.execPath, [AGENT], { DIFFINERY_API_URL: c.api_url, DIFFINERY_JOB_ID: c.job_id, DIFFINERY_JOB_TOKEN: c.token, DIFFINERY_WORK_ROOT: join(work, `job-${c.job_id}`) }, 180_000);
303337 };
304338 await ok("PUT", `/repos/${owner}/mailbox/subscription`, { ignored: true });
modifiedapps/diffinery/test/unit/markdown-safety.test.ts+26−7View file
@@ -10,6 +10,25 @@
1010 return { html, ms: performance.now() - t };
1111 }
1212
13+/** Ordinary Markdown of `length` characters: paragraphs with emphasis, a link and code. */
14+function prose(length: number): string {
15+ const p = "Some *plain* prose with a [link](/x) and `code`.\n\n";
16+ return p.repeat(Math.ceil(length / p.length)).slice(0, length);
17+}
18+
19+/**
20+ * Renders `source` about as fast as ordinary Markdown of the same length, timed on the same machine,
21+ * so a slower or busy CI host doesn't change the verdict (the hosts differ about 2x per core and can
22+ * stall for a moment, vitest.shared.ts). Each takes the best of three. These inputs took 0.1 to 5
23+ * times as long as the prose on 2026-10-24; a backtracking pattern took seconds, hundreds of times as long.
24+ */
25+function expectLinear(source: string): void {
26+ const best = (s: string) => Math.min(...[0, 1, 2].map(() => timed(s).ms));
27+ const baseline = best(prose(source.length));
28+ const ms = best(source);
29+ expect(ms, `${ms.toFixed(1)} ms, against ${baseline.toFixed(1)} ms for ordinary Markdown as long`).toBeLessThan(25 * baseline + 20);
30+}
31+
1332 describe("markdown block patterns", () => {
1433 // Each of these took seconds with a single backtracking regex per line.
1534 it.each([
@@ -20,7 +39,7 @@
2039 ["a table delimiter row with spaces before text", "a|b\n|-" + spaces + "x"],
2140 ["a dash run with spaces before text", "a|b\n---" + spaces + "x"],
2241 ])("renders %s in linear time", (_name, source) => {
23- expect(timed(source).ms).toBeLessThan(500);
42+ expectLinear(source);
2443 });
2544
2645 // Unmatched link and emphasis openers each scan ahead; a budget per render bounds the total.
@@ -31,7 +50,7 @@
3150 ["list items of emphasis openers", "- " + "*> ".repeat(19_000)],
3251 ["many such paragraphs", ("_1.".repeat(19_000) + "\n\n").repeat(8)],
3352 ])("bounds the lookahead for %s", (_name, source) => {
34- expect(timed(source).ms).toBeLessThan(500);
53+ expectLinear(source);
3554 });
3655
3756 it("formats ordinary text in full", () => {
@@ -83,7 +102,7 @@
83102 ["a long tilde run", "~".repeat(59_000) + "`"],
84103 ["a long backtick run", "`".repeat(59_000) + "x`"],
85104 ])("renders %s in linear time", (_name, source) => {
86- expect(timed(source).ms).toBeLessThan(500);
105+ expectLinear(source);
87106 });
88107
89108 it("keeps hard line breaks", () => {
@@ -134,10 +153,10 @@
134153 });
135154
136155 it("renders many comment openers in linear time", () => {
137- expect(timed("a <!-- ".repeat(40_000)).ms).toBeLessThan(500);
138- expect(timed("a <!-- b --> ".repeat(30_000)).ms).toBeLessThan(500);
139- expect(timed("<!-- x -->\n".repeat(40_000)).ms).toBeLessThan(500);
140- expect(timed("`` <!-- ` ".repeat(30_000) + "-->").ms).toBeLessThan(500);
156+ expectLinear("a <!-- ".repeat(40_000));
157+ expectLinear("a <!-- b --> ".repeat(30_000));
158+ expectLinear("<!-- x -->\n".repeat(40_000));
159+ expectLinear("`` <!-- ` ".repeat(30_000) + "-->");
141160 });
142161
143162 it("strips comments from plain text, outside code", () => {