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.

95 lines · 4.0 KBRaw History
1import { describe, expect, it } from "vitest";
2import { DatabaseSync } from "node:sqlite";
3import { ProjectDO, type RunRow, type TaskView } from "../../src/agentic/ProjectDO.js";
4import type { Env } from "../../src/env.js";
5import { testD1 } from "./stubs/d1.js";
6
7/** The Durable Object's SqlStorage over node:sqlite. */
8function sqlStorage() {
9 const db = new DatabaseSync(":memory:");
10 return {
11 exec(query: string, ...args: unknown[]) {
12 const params = args.map((a) => (a === undefined ? null : a)) as Array<string | number | null>;
13 let rows: unknown[] = [];
14 if (!params.length && query.trim().replace(/;\s*$/, "").includes(";")) db.exec(query);
15 else rows = db.prepare(query).all(...params);
16 return {
17 toArray: () => rows,
18 one: () => {
19 if (rows.length !== 1) throw new Error(`expected one row, got ${rows.length}`);
20 return rows[0];
21 },
22 };
23 },
24 };
25}
26
27/**
28 * A project with one task whose run is in its agent step, in a sandbox. Closing the sandbox calls `during`, which
29 * reads (or changes) the project between the run's end and the sandbox's close.
30 */
31function fixture(during: (p: ProjectDO) => Promise<void> | void) {
32 const ctx = { storage: { sql: sqlStorage(), setAlarm: async () => {} }, waitUntil: () => {} } as unknown as DurableObjectState;
33 const stub = { idFromName: (n: string) => n };
34 let p!: ProjectDO;
35 const env = {
36 DB: testD1(),
37 REPO: { ...stub, get: () => ({ closeTaskSandbox: async () => during(p) }) },
38 SANDBOX: { ...stub, get: () => ({ call: async () => ({ status: 200, body: "{}" }) }) },
39 } as unknown as Env;
40 p = new ProjectDO(ctx, env);
41 const sql = (ctx.storage as unknown as { sql: ReturnType<typeof sqlStorage> }).sql;
42 const at = new Date().toISOString();
43 sql.exec(
44 "INSERT INTO tasks (number, id, repo_id, goal, title, runtime, agent_id, agent_login, person_id, person_login, base, branch, state, created_at, updated_at) VALUES (1, 't1', 'r1', 'Do it', 'Do it', 'custom', 'a1', 'bot', 'u1', 'ada', 'main', 'agent/1-do-it', 'running', ?, ?)",
45 at,
46 at,
47 );
48 sql.exec("INSERT INTO runs (task, seq, kind, prompt, providers, step, sandbox_id, proc, started_at) VALUES (1, 1, 'start', '', '[]', 'agent', 'sb1', 'agent', ?)", at);
49 const run = () => sql.exec("SELECT * FROM runs WHERE task = 1 AND seq = 1").one() as RunRow;
50 const end = (step: "done" | "failed" | "stopped") => (p as unknown as { endRun(r: RunRow, s: string, e: string | null): Promise<void> }).endRun(run(), step, null);
51 return { p, sql, end };
52}
53
54describe("a run's end", () => {
55 it("settles its task before the sandbox closes", async () => {
56 let seen: TaskView | null = null;
57 const f = fixture((p) => {
58 seen = p.get(1);
59 });
60 await f.end("done");
61 // Read between the run's end and the sandbox's close: the run is done, and so is the task.
62 expect(seen!.runs[0]!.step).toBe("done");
63 expect(seen!.state).toBe("ready");
64 expect(f.p.get(1)!.state).toBe("ready");
65 });
66
67 it("keeps a waiting message's task running, and starts its run once the sandbox is closed", async () => {
68 let seen: TaskView | null = null;
69 const f = fixture((p) => {
70 seen = p.get(1);
71 });
72 f.sql.exec("UPDATE tasks SET queued = 'And add a test' WHERE number = 1");
73 await f.end("done");
74 expect(seen!.state).toBe("running");
75 expect(seen!.runs.length).toBe(1);
76 const after = f.p.get(1)!;
77 expect(after.state).not.toBe("ready");
78 expect(after.runs.map((r) => [r.seq, r.kind, r.message])).toEqual([
79 [1, "start", null],
80 [2, "message", "And add a test"],
81 ]);
82 expect(after.queued).toBeNull();
83 });
84
85 it("leaves a stop made while the sandbox closed as it is", async () => {
86 const f = fixture(async (p) => {
87 await p.stop(1, "ada");
88 });
89 f.sql.exec("UPDATE tasks SET queued = 'And add a test' WHERE number = 1");
90 await f.end("done");
91 const after = f.p.get(1)!;
92 expect(after.state).toBe("stopped");
93 expect(after.runs.length).toBe(1);
94 });
95});