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.

187 lines · 9.1 KBRaw History
1// Shared setup for the agent e2e files (tasks.test.mjs, terminals.test.mjs): a real `wrangler dev`
2// Worker, sandboxes in an `agent.mjs --sandbox-pool` process, a fake model provider, and helpers.
3// Importing it registers the before/after hooks that start and stop them.
4import { spawn, execFileSync } from "node:child_process";
5import { mkdtempSync, writeFileSync, mkdirSync, rmSync, readdirSync, statSync, readFileSync, existsSync } from "node:fs";
6import { tmpdir } from "node:os";
7import { createHash, randomBytes } from "node:crypto";
8import { createRequire } from "node:module";
9import { join, dirname, resolve } from "node:path";
10import { fileURLToPath } from "node:url";
11import { createServer as netServer } from "node:net";
12import { createServer as httpServer } from "node:http";
13import { before, after } from "node:test";
14import assert from "node:assert/strict";
15
16export const APP = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
17export const AGENT = resolve(APP, "../../containers/runner/dist/agent.mjs");
18export const work = mkdtempSync(join(tmpdir(), "diffinery-tasks-e2e-"));
19export const poolRoot = join(work, "pool");
20// The terminal test speaks WebSocket with headers (Origin, cookies), which Node's built-in client can't set.
21export const { WebSocket: WS } = createRequire(resolve(APP, "../../containers/runner/package.json"))("ws");
22export const RIVAL_SESSION = `rival-${randomBytes(12).toString("hex")}`;
23export const ANTHROPIC_KEY = `sk-ant-api03-${randomBytes(30).toString("hex")}`;
24export const OPENROUTER_KEY = `sk-or-v1-${randomBytes(32).toString("hex")}`;
25export let base;
26export let dev;
27export let pool;
28export let upstream;
29export let devLog = "";
30export let poolLog = "";
31export let token;
32export const modelRequests = [];
33
34export const freePort = () =>
35 new Promise((res) => {
36 const s = netServer();
37 s.listen(0, "127.0.0.1", () => {
38 const { port } = s.address();
39 s.close(() => res(port));
40 });
41 });
42
43export async function api(method, path, body, tok = token) {
44 const res = await fetch(`${base}/api/v3${path}`, {
45 method,
46 headers: { ...(tok === null ? {} : { authorization: `Bearer ${tok}` }), "content-type": "application/json" },
47 body: body === undefined ? undefined : JSON.stringify(body),
48 });
49 const text = await res.text();
50 let json = null;
51 try {
52 json = text ? JSON.parse(text) : null;
53 } catch {
54 json = text;
55 }
56 return { status: res.status, json };
57}
58export async function ok(method, path, body, tok) {
59 const r = await api(method, path, body, tok);
60 assert.ok(r.status >= 200 && r.status < 300, `${method} ${path} -> ${r.status} ${JSON.stringify(r.json)}`);
61 return r.json;
62}
63/** As the signed-in person in a browser (the dev sign-in: no credentials). */
64export const asPerson = (method, path, body) => api(method, path, body, null);
65
66export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
67export async function until(what, fn, timeoutMs = 90_000) {
68 const end = Date.now() + timeoutMs;
69 let last;
70 while (Date.now() < end) {
71 last = await fn();
72 if (last) return last;
73 await sleep(500);
74 }
75 assert.fail(`timed out waiting for ${what}\n--- wrangler log tail ---\n${devLog.slice(-4000)}\n--- pool log tail ---\n${poolLog.slice(-3000)}`);
76}
77
78export function git(cwd, args, tok = token) {
79 const auth = Buffer.from(`x-token:${tok}`).toString("base64");
80 return execFileSync("git", ["-c", `http.extraHeader=Authorization: Basic ${auth}`, "-c", "protocol.version=2", ...args], {
81 cwd,
82 encoding: "utf8",
83 stdio: ["ignore", "pipe", "pipe"],
84 env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_AUTHOR_NAME: "E2E", GIT_AUTHOR_EMAIL: "e2e@example.com", GIT_COMMITTER_NAME: "E2E", GIT_COMMITTER_EMAIL: "e2e@example.com" },
85 });
86}
87
88export async function mcp(name, args, tok = token) {
89 const r = await fetch(`${base}/mcp`, { method: "POST", headers: { authorization: `Bearer ${tok}`, "content-type": "application/json", accept: "application/json, text/event-stream" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }) });
90 const { result } = await r.json();
91 return result;
92}
93
94/** The sandboxes the pool has started, newest first. */
95export const poolSandboxes = () =>
96 existsSync(poolRoot)
97 ? readdirSync(poolRoot)
98 .map((id) => ({ id, at: statSync(join(poolRoot, id)).mtimeMs }))
99 .sort((a, b) => b.at - a.at)
100 .map((x) => x.id)
101 : [];
102
103before(async () => {
104 if (!existsSync(AGENT)) execFileSync("pnpm", ["--filter", "@diffinery/runner", "build"], { cwd: APP, stdio: "pipe" });
105 // A stand-in for the provider: records what reaches it and answers like the Messages API.
106 upstream = httpServer((req, res) => {
107 let body = "";
108 req.on("data", (d) => (body += d));
109 req.on("end", () => {
110 modelRequests.push({ method: req.method, url: req.url, headers: req.headers, body });
111 res.writeHead(200, { "content-type": "application/json", "set-cookie": "upstream=1" });
112 res.end(JSON.stringify({ type: "message", content: [{ type: "text", text: "fake model says hi" }] }));
113 });
114 });
115 await new Promise((r) => upstream.listen(0, "127.0.0.1", r));
116 const upstreamUrl = `http://127.0.0.1:${upstream.address().port}`;
117
118 // A stand-in for a Claude Code someone signed in to in their terminal (phase 4b): it reports what it was given and
119 // where it ran, and changes a file, without any model.
120 const fakeBin = join(work, "bin");
121 mkdirSync(fakeBin, { recursive: true });
122 writeFileSync(
123 join(fakeBin, "claude"),
124 [
125 "#!/bin/sh",
126 'd=$(basename "$PWD")',
127 'echo "{\\"type\\":\\"assistant\\",\\"message\\":{\\"content\\":[{\\"type\\":\\"text\\",\\"text\\":\\"signed-in claude: key=${ANTHROPIC_API_KEY:-none} base=${ANTHROPIC_BASE_URL:-none} dir=$d\\"}]}}"',
128 'printf "%s\\n" "$DIFFINERY_TASK_PROMPT"',
129 'echo "made in the terminal" > terminal.txt',
130 'echo \'{"type":"result","subtype":"success","result":"Done in the terminal."}\'',
131 ].join("\n"),
132 { mode: 0o755 },
133 );
134 const poolPort = await freePort();
135 const poolSecret = randomBytes(16).toString("hex");
136 pool = spawn("node", [AGENT, "--sandbox-pool", "--work-root", poolRoot], { env: { ...process.env, PATH: `${fakeBin}:${process.env.PATH}`, DIFFINERY_SANDBOX_POOL_SECRET: poolSecret, DIFFINERY_SANDBOX_PORT: String(poolPort) }, stdio: ["ignore", "pipe", "pipe"], detached: true });
137 pool.stdout.on("data", (d) => (poolLog += d));
138 pool.stderr.on("data", (d) => (poolLog += d));
139 await until("sandbox pool", async () => poolLog.includes("listening"), 30_000);
140
141 const port = await freePort();
142 base = `http://127.0.0.1:${port}`;
143 const persist = join(work, "state");
144 execFileSync("pnpm", ["exec", "wrangler", "d1", "migrations", "apply", "diffinery-dev", "--local", "-e", "dev", "--persist-to", persist], { cwd: APP, stdio: "pipe", env: { ...process.env, CI: "1" } });
145 // A second person, signed in with a session: not the terminal's opener, whatever their role.
146 {
147 const at = new Date().toISOString();
148 const later = new Date(Date.now() + 30 * 86400_000).toISOString();
149 const hash = createHash("sha256").update(RIVAL_SESSION).digest("hex");
150 execFileSync("pnpm", ["exec", "wrangler", "d1", "execute", "diffinery-dev", "--local", "-e", "dev", "--persist-to", persist, "--command", `INSERT INTO accounts (id, login, kind, name, status, created_at) VALUES ('u-rival', 'rival', 'user', 'Rival', 'active', '${at}'); INSERT INTO sessions (id, account_id, method, created_at, last_seen_at, expires_at) VALUES ('${hash}', 'u-rival', 'passkey', '${at}', '${at}', '${later}')`], { cwd: APP, stdio: "pipe", env: { ...process.env, CI: "1" } });
151 }
152 const vars = { PUBLIC_URL: base, MASTER_KEY: randomBytes(32).toString("base64"), SANDBOX_POOL_URL: `http://127.0.0.1:${poolPort}`, SANDBOX_POOL_SECRET: poolSecret, MODEL_UPSTREAM_ANTHROPIC: upstreamUrl, MODEL_UPSTREAM_OPENAI: upstreamUrl, MODEL_UPSTREAM_OPENROUTER: upstreamUrl, AGENT_POLL_MS: "500", TERMINAL_POLL_MS: "3000" };
153 dev = spawn("pnpm", ["exec", "wrangler", "dev", "-e", "dev", "--ip", "127.0.0.1", "--port", String(port), "--inspector-port", String(await freePort()), "--persist-to", persist, ...Object.entries(vars).flatMap(([k, v]) => ["--var", `${k}:${v}`]), "--show-interactive-dev-session=false"], {
154 cwd: APP,
155 env: { ...process.env, CI: "1", WRANGLER_SEND_METRICS: "false" },
156 stdio: ["ignore", "pipe", "pipe"],
157 detached: true,
158 });
159 dev.stdout.on("data", (d) => (devLog += d));
160 dev.stderr.on("data", (d) => (devLog += d));
161 await until(
162 "wrangler dev",
163 async () => {
164 try {
165 return (await fetch(`${base}/healthz`)).ok;
166 } catch {
167 return false;
168 }
169 },
170 120_000,
171 );
172 const res = await fetch(`${base}/api/v3/diffinery/tokens`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name: "e2e", scopes: ["admin"] }) });
173 assert.equal(res.status, 201, await res.clone().text());
174 token = (await res.json()).token;
175});
176
177after(() => {
178 for (const p of [dev, pool]) {
179 if (p?.pid) {
180 try {
181 process.kill(-p.pid, "SIGTERM");
182 } catch {}
183 }
184 }
185 upstream?.close();
186 if (process.env.E2E_KEEP !== "1") rmSync(work, { recursive: true, force: true });
187});