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.

91 lines · 4.4 KBRaw History
1import { execFileSync } from "node:child_process";
2import { readFileSync, rmSync, writeFileSync } from "node:fs";
3import path from "node:path";
4import { afterAll, beforeAll, describe, expect, it } from "vitest";
5import { startSandbox, type SandboxHandle } from "../src/sandbox.js";
6import { tmp } from "./helpers.js";
7
8const SECRET = "sandbox-secret-xyz";
9const TOKEN = "job-token-abc-123";
10let sb: SandboxHandle;
11let workRoot: string;
12let origin: string;
13let base: string;
14
15async function call(p: string, body?: unknown, auth = SECRET) {
16 const res = await fetch(`${base}${p}`, {
17 method: body === undefined ? "GET" : "POST",
18 headers: { authorization: `Bearer ${auth}`, "content-type": "application/json" },
19 body: body === undefined ? undefined : JSON.stringify(body),
20 });
21 return { status: res.status, json: (await res.json()) as Record<string, any> };
22}
23
24beforeAll(async () => {
25 workRoot = tmp("sandbox-");
26 origin = tmp("sandbox-origin-");
27 const env = { ...process.env, GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t" };
28 execFileSync("git", ["init", "-q", "-b", "main"], { cwd: origin, env });
29 writeFileSync(path.join(origin, "hello.txt"), "hello sandbox\n");
30 execFileSync("git", ["add", "."], { cwd: origin, env });
31 execFileSync("git", ["commit", "-q", "-m", "init"], { cwd: origin, env });
32 execFileSync("git", ["checkout", "-q", "-b", "feature"], { cwd: origin, env });
33 writeFileSync(path.join(origin, "feature.txt"), "on feature\n");
34 execFileSync("git", ["add", "."], { cwd: origin, env });
35 execFileSync("git", ["commit", "-q", "-m", "feat"], { cwd: origin, env });
36 execFileSync("git", ["checkout", "-q", "main"], { cwd: origin, env });
37 sb = await startSandbox({ port: 0, host: "127.0.0.1", secret: SECRET, workRoot, cloneUrl: `file://${origin}`, ref: "feature", token: TOKEN, maxOutputBytes: 1000 });
38 base = `http://127.0.0.1:${sb.port}`;
39 await sb.ready;
40});
41
42afterAll(async () => {
43 await sb?.close();
44 rmSync(workRoot, { recursive: true, force: true });
45 rmSync(origin, { recursive: true, force: true });
46});
47
48describe("sandbox server", () => {
49 it("reports health without auth and rejects unauthenticated calls", async () => {
50 const h = await fetch(`${base}/health`);
51 expect(await h.json()).toEqual({ ok: true, ready: true });
52 expect((await call("/exec", { cmd: "true" }, "wrong")).status).toBe(401);
53 const noAuth = await fetch(`${base}/exec`, { method: "POST", body: "{}" });
54 expect(noAuth.status).toBe(401);
55 });
56
57 it("cloned the repo at the requested ref", async () => {
58 const r = await call("/exec", { cmd: "git rev-parse --abbrev-ref HEAD && cat hello.txt feature.txt" });
59 expect(r.status).toBe(200);
60 expect(r.json).toMatchObject({ exitCode: 0, stdout: "feature\nhello sandbox\non feature\n", stderr: "" });
61 });
62
63 it("executes commands with cwd/env, masks secrets and truncates output", async () => {
64 await call("/exec", { cmd: "mkdir -p sub" });
65 const r = await call("/exec", { cmd: 'pwd; echo "v=$MYVAR"; echo "s=$DIFFINERY_SANDBOX_SECRET"; echo err >&2; echo "tok=' + TOKEN + '"; exit 3', cwd: "sub", env: { MYVAR: "x1" } });
66 expect(r.json.exitCode).toBe(3);
67 expect(r.json.stdout).toBe(`${path.join(sb.repoDir, "sub")}\nv=x1\ns=\ntok=***\n`);
68 expect(r.json.stderr).toBe("err\n");
69 const big = await call("/exec", { cmd: "head -c 5000 /dev/zero | tr '\\0' a" });
70 expect(big.json.stdout.length).toBe(1000);
71 expect(big.json.truncated).toBe(true);
72 });
73
74 it("times out long commands", async () => {
75 const r = await call("/exec", { cmd: "sleep 20", timeoutMs: 300 });
76 expect(r.json.timedOut).toBe(true);
77 expect(r.json.exitCode).toBe(124);
78 });
79
80 it("reads and writes files (base64)", async () => {
81 const content = Buffer.from("bin\0ary ✓").toString("base64");
82 const w = await call("/files/write", { path: "new/dir/file.bin", content });
83 expect(w.json).toMatchObject({ ok: true, size: Buffer.from("bin\0ary ✓").length });
84 expect(readFileSync(path.join(sb.repoDir, "new/dir/file.bin"), "utf8")).toBe("bin\0ary ✓");
85 const r = await call("/files/read", { path: "new/dir/file.bin" });
86 expect(Buffer.from(r.json.content, "base64").toString()).toBe("bin\0ary ✓");
87 expect((await call("/files/read", { path: "missing.txt" })).status).toBe(404);
88 expect((await call("/files/read", {})).status).toBe(400);
89 expect((await call("/nope", {})).status).toBe(404);
90 });
91});