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.

94 lines · 4.7 KBRaw History
1import { existsSync, readFileSync, writeFileSync } from "node:fs";
2import path from "node:path";
3import { afterEach, beforeEach, describe, expect, it } from "vitest";
4import { fakeServer, isolatedEnv, run, tempDir, type FakeServer } from "./helpers.js";
5
6/** A stand-in for containers/runner/dist/agent.mjs: records argv, env and cwd, then exits. */
7function fakeAgent(exitCode = 0): { agent: string; record: string } {
8 const dir = tempDir("diffinery-agent-");
9 const record = path.join(dir, "record.json");
10 const agent = path.join(dir, "agent.mjs");
11 writeFileSync(
12 agent,
13 `import { writeFileSync } from "node:fs";
14writeFileSync(${JSON.stringify(record)}, JSON.stringify({ argv: process.argv.slice(2), cwd: process.cwd(), env: process.env }));
15console.log("hello from the agent");
16console.error("agent stderr line");
17process.exit(${exitCode});
18`,
19 );
20 return { agent, record };
21}
22
23describe("runner", () => {
24 let srv: FakeServer;
25 let env: NodeJS.ProcessEnv;
26 beforeEach(async () => {
27 srv = await fakeServer();
28 env = isolatedEnv({ DIFFINERY_URL: srv.url, DIFFINERY_TOKEN: "dfy_user_token" });
29 });
30 afterEach(() => srv.close());
31
32 it("--once claims a job, runs the agent with the job's env, and returns its exit code", async () => {
33 let claims = 0;
34 srv.on("POST", "/api/v3/repos/acme/app/actions/runners/claim", () =>
35 ++claims === 1 ? { body: { job_id: "job42", run_id: "run7", name: "build (linux)", token: "djt_repo_job42_secret", api_url: srv.url } } : { status: 204 },
36 );
37 const { agent, record } = fakeAgent(3);
38 const work = tempDir("diffinery-work-");
39 const r = await run(["runner", "-R", "acme/app", "--once", "--agent", agent, "--labels", "gpu, linux", "--name", "box-1", "--work-dir", work], { env });
40
41 expect(r.code).toBe(3);
42 const claimReq = srv.find("POST", "/api/v3/repos/acme/app/actions/runners/claim");
43 expect(claimReq).toHaveLength(1);
44 expect(claimReq[0]!.body).toEqual({ name: "box-1", labels: ["gpu", "linux"] });
45 expect(claimReq[0]!.headers.authorization).toBe("Bearer dfy_user_token");
46
47 const rec = JSON.parse(readFileSync(record, "utf8")) as { argv: string[]; cwd: string; env: Record<string, string> };
48 expect(rec.argv).toEqual([]);
49 expect(rec.env.DIFFINERY_API_URL).toBe(srv.url);
50 expect(rec.env.DIFFINERY_JOB_ID).toBe("job42");
51 expect(rec.env.DIFFINERY_JOB_TOKEN).toBe("djt_repo_job42_secret");
52 expect(rec.env.DIFFINERY_WORK_ROOT).toBe(work);
53 expect(rec.env.DIFFINERY_TOKEN).toBeUndefined();
54 expect(rec.env.DIFFINERY_URL).toBeUndefined();
55
56 expect(r.stdout).toContain("[build (linux)] hello from the agent");
57 expect(r.stderr).toContain("[build (linux)] agent stderr line");
58 expect(r.stdout).not.toContain("djt_repo_job42_secret");
59 });
60
61 it("--once with no waiting job exits 0 without running the agent", async () => {
62 srv.on("POST", /\/runners\/claim$/, { status: 204 });
63 const { agent, record } = fakeAgent(0);
64 const r = await run(["runner", "-R", "acme/app", "-R", "acme/other", "--once", "--agent", agent], { env });
65 expect(r.code).toBe(0);
66 expect(r.stdout).toContain("No jobs waiting");
67 expect(existsSync(record)).toBe(false);
68 expect(srv.find("POST", /runners\/claim$/).map((q) => q.path)).toEqual(["/api/v3/repos/acme/app/actions/runners/claim", "/api/v3/repos/acme/other/actions/runners/claim"]);
69 });
70
71 it("uses DIFFINERY_RUNNER_AGENT and tries the next repository", async () => {
72 srv.on("POST", "/api/v3/repos/acme/app/actions/runners/claim", { status: 204 });
73 srv.on("POST", "/api/v3/repos/acme/other/actions/runners/claim", { body: { job_id: "j1", run_id: "r1", name: "test", token: "djt_x", api_url: srv.url } });
74 const { agent, record } = fakeAgent(0);
75 const r = await run(["runner", "-R", "acme/app", "-R", "acme/other", "--once", "--work-dir", tempDir()], { env: { ...env, DIFFINERY_RUNNER_AGENT: agent } });
76 expect(r.code).toBe(0);
77 expect(JSON.parse(readFileSync(record, "utf8")).env.DIFFINERY_JOB_ID).toBe("j1");
78 });
79
80 it("explains how to build the agent when none is configured", async () => {
81 const r = await run(["runner", "-R", "acme/app", "--once"], { env });
82 expect(r.code).toBe(1);
83 expect(r.stderr).toContain("pnpm --filter @diffinery/runner build");
84 expect(r.stderr).toContain("containers/runner/dist/agent.mjs");
85 });
86
87 it("fails fast on permission errors", async () => {
88 srv.on("POST", "/api/v3/repos/acme/app/actions/runners/claim", { status: 403, body: { message: "admin access required" } });
89 const { agent } = fakeAgent(0);
90 const r = await run(["runner", "-R", "acme/app", "--agent", agent, "--interval", "0"], { env });
91 expect(r.code).toBe(1);
92 expect(r.stderr).toContain("admin access required");
93 });
94});