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.

160 lines · 10.4 KBRaw History
1import { describe, expect, it } from "vitest";
2import { agentPullState, approvalCandidates, classify, pickRepos, type ClassifyInput, type TodayRepo } from "../../src/agentic/today.js";
3import { checksState, needsMergeCheck, type RepoToday, type TodayPull } from "../../src/agentic/today-repo.js";
4import type { Repo } from "../../src/lib/directory.js";
5
6const ME = { id: "u-me", login: "Ayse" };
7const BOT = { id: "a-bot", login: "ayse-bot", kind: "agent" };
8const OTHER_BOT = { id: "a-other", login: "other-bot", kind: "agent" };
9const BOB = { id: "u-bob", login: "bob", kind: "user" };
10
11const pull = (p: Partial<TodayPull> & { number: number }): TodayPull => ({
12 title: `PR ${p.number}`,
13 author: BOB,
14 head: `b${p.number}`,
15 base: "main",
16 draft: false,
17 requestedReviewers: [],
18 intent: null,
19 createdAt: "2026-10-13T08:00:00Z",
20 updatedAt: `2026-10-13T08:${String(p.number).padStart(2, "0")}:00Z`,
21 approvedBy: [],
22 approvedHead: false,
23 changesRequestedBy: [],
24 reviewerIds: [],
25 checks: "success",
26 failingChecks: [],
27 autoMerge: false,
28 queued: false,
29 ...p,
30});
31
32const repo = (id: string): TodayRepo => ({ id, fullName: `acme/${id}`, url: `/acme/${id}`, defaultBranch: "main", visibility: "private" });
33const data = (d: Partial<RepoToday>): RepoToday => ({ pulls: [], merged: [], releases: [], failedRuns: [], ...d });
34const input = (repos: ClassifyInput["repos"], writable: string[] = repos.map((r) => r.repo.id)): ClassifyInput => ({ me: ME, agentIds: new Set([BOT.id]), writable: new Set(writable), repos });
35
36describe("checksState", () => {
37 it("is none without checks, failure when any failed, pending while any runs", () => {
38 expect(checksState([]).state).toBe("none");
39 expect(checksState([{ name: "ci", status: "completed", conclusion: "success" }, { name: "lint", status: "in_progress", conclusion: null }]).state).toBe("pending");
40 const f = checksState([{ name: "ci", status: "completed", conclusion: "failure" }, { name: "lint", status: "in_progress", conclusion: null }]);
41 expect(f).toEqual({ state: "failure", failing: ["ci"] });
42 expect(checksState([{ name: "ci", status: "completed", conclusion: "skipped" }, { name: "x", status: "completed", conclusion: "neutral" }]).state).toBe("success");
43 });
44});
45
46describe("agentPullState", () => {
47 it("orders what a person acts on", () => {
48 expect(agentPullState(pull({ number: 1, draft: true, checks: "failure" }))).toBe("draft");
49 expect(agentPullState(pull({ number: 1, autoMerge: true, checks: "failure" }))).toBe("landing");
50 expect(agentPullState(pull({ number: 1, checks: "failure", changesRequestedBy: ["bob"] }))).toBe("checks_failing");
51 expect(agentPullState(pull({ number: 1, changesRequestedBy: ["bob"], approvedHead: true }))).toBe("changes_requested");
52 expect(agentPullState(pull({ number: 1, approvedHead: true }))).toBe("approved");
53 expect(agentPullState(pull({ number: 1, checks: "pending" }))).toBe("checks_running");
54 expect(agentPullState(pull({ number: 1 }))).toBe("waiting_for_approval");
55 });
56
57 it("puts a pull request that conflicts, or is already in its base, before anything a person would do", () => {
58 expect(agentPullState(pull({ number: 1, mergeState: "conflicting", autoMerge: true }))).toBe("conflicting");
59 expect(agentPullState(pull({ number: 1, mergeState: "in_base" }))).toBe("in_base");
60 expect(agentPullState(pull({ number: 1, mergeState: "conflicting", draft: true }))).toBe("draft");
61 expect(agentPullState(pull({ number: 1, mergeState: "clean" }))).toBe("waiting_for_approval");
62 expect(agentPullState(pull({ number: 1, mergeState: null }))).toBe("waiting_for_approval");
63 });
64});
65
66describe("needsMergeCheck", () => {
67 it("checks agents' pull requests that may wait for a person and have no merge state for their current commits", () => {
68 const agent = (p: Partial<TodayPull>) => pull({ number: 1, author: OTHER_BOT, mergeState: null, ...p });
69 expect(needsMergeCheck(agent({}))).toBe(true);
70 expect(needsMergeCheck(agent({ checks: "pending" }))).toBe(true);
71 for (const p of [{ mergeState: "clean" as const }, { draft: true }, { autoMerge: true }, { queued: true }, { checks: "failure" as const }, { changesRequestedBy: ["bob"] }, { approvedHead: true }, { author: BOB }]) expect(needsMergeCheck(agent(p))).toBe(false);
72 });
73});
74
75describe("classify", () => {
76 it("asks the viewer for requested reviews, case-insensitively, but not on drafts", () => {
77 const t = classify(input([{ repo: repo("web"), data: data({ pulls: [pull({ number: 1, requestedReviewers: ["ayse"] }), pull({ number: 2, requestedReviewers: ["AYSE"], draft: true })] }) }]));
78 expect(t.needsYou.map((x) => [x.kind, x.pull.number])).toEqual([["review_requested", 1]]);
79 });
80
81 it("lists agent pull requests waiting for a person only where the viewer can approve", () => {
82 const pulls = [pull({ number: 1, author: OTHER_BOT }), pull({ number: 2, author: OTHER_BOT, checks: "pending" }), pull({ number: 3, author: OTHER_BOT, approvedHead: true }), pull({ number: 4, author: OTHER_BOT, checks: "failure" }), pull({ number: 5, author: OTHER_BOT, changesRequestedBy: ["bob"] })];
83 const t = classify(input([{ repo: repo("web"), data: data({ pulls }) }, { repo: repo("api"), data: data({ pulls: [pull({ number: 9, author: OTHER_BOT })] }) }], ["web"]));
84 expect(t.needsYou.map((x) => `${x.repo.id}#${x.pull.number}:${x.kind}`).sort()).toEqual(["web#1:agent_approval", "web#2:agent_approval"]);
85 });
86
87 it("only role-checks repositories with agent work waiting for a person", () => {
88 const repos = [
89 { repo: repo("a"), data: data({ pulls: [pull({ number: 1, author: OTHER_BOT })] }) },
90 { repo: repo("b"), data: data({ pulls: [pull({ number: 1, author: OTHER_BOT, approvedHead: true }), pull({ number: 2 })] }) },
91 { repo: repo("c"), data: data({ pulls: [pull({ number: 1, author: { ...ME, kind: "agent" } })] }) },
92 ];
93 expect(approvalCandidates(repos, ME)).toEqual(["a"]);
94 });
95
96 it("lists deployments waiting for the viewer as a required reviewer, unless they started the run and self-review is off", () => {
97 const d = (runId: string, reviewerIds: string[], selfBlocked: string[] = [], since = "2026-10-13T08:00:00Z") => ({ runId, runNumber: 1, workflow: "deploy", title: "", ref: "refs/heads/main", environment: "production", reviewerIds, selfBlocked, actor: BOB, since });
98 const deployments = [d("r1", ["u-me"]), d("r2", ["u-bob"]), d("r3", ["u-me"], ["u-me"]), d("r4", ["u-me", "u-bob"], [], "2026-10-13T09:00:00Z"), d("r5", ["u-me"], [BOT.id]), d("r6", ["u-me"], [OTHER_BOT.id], "2026-10-13T07:00:00Z")];
99 const t = classify({ ...input([{ repo: repo("web"), data: data({ deployments }) }, { repo: repo("old"), data: data({}) }]), ownAgentIds: new Set([BOT.id]) });
100 // r3 the viewer started, r5 their own agent did; r6's agent belongs to their organization, not them.
101 expect(t.deployments.map((x) => x.deployment.runId)).toEqual(["r4", "r1", "r6"]);
102 });
103
104 it("flags the viewer's own blocked pull requests", () => {
105 const mine = { ...ME, kind: "user" };
106 const pulls = [
107 pull({ number: 1, author: mine, changesRequestedBy: ["bob"] }),
108 pull({ number: 2, author: mine, checks: "failure" }),
109 pull({ number: 3, author: mine }),
110 pull({ number: 4, author: mine, mergeState: "conflicting" }),
111 pull({ number: 5, author: mine, mergeState: "conflicting", draft: true }),
112 ];
113 const t = classify(input([{ repo: repo("web"), data: data({ pulls }) }]));
114 expect(t.needsYou.map((x) => [x.pull.number, x.kind]).sort()).toEqual([
115 [1, "changes_requested"],
116 [2, "checks_failing"],
117 [4, "conflicting"],
118 ]);
119 });
120
121 it("leaves agents' pull requests that conflict or are already in their base to their agents", () => {
122 const pulls = [pull({ number: 1, author: OTHER_BOT, mergeState: "conflicting" }), pull({ number: 2, author: OTHER_BOT, mergeState: "in_base" }), pull({ number: 3, author: OTHER_BOT, mergeState: "clean" }), pull({ number: 4, author: BOT, mergeState: "conflicting" })];
123 const t = classify(input([{ repo: repo("web"), data: data({ pulls }) }]));
124 expect(t.needsYou.map((x) => x.pull.number)).toEqual([3]);
125 expect(t.agentsAtWork.map((x) => [x.pull.number, x.state])).toEqual([[4, "conflicting"]]);
126 expect(approvalCandidates([{ repo: repo("web"), data: data({ pulls: pulls.slice(0, 2) }) }], ME)).toEqual([]);
127 });
128
129 it("shows the viewer's agents' pull requests with their state, newest first, and lists them once in needs-you", () => {
130 const t = classify(input([{ repo: repo("web"), data: data({ pulls: [pull({ number: 1, author: BOT }), pull({ number: 2, author: BOT, checks: "failure" }), pull({ number: 3, author: OTHER_BOT })] }) }]));
131 expect(t.agentsAtWork.map((x) => [x.pull.number, x.state])).toEqual([
132 [2, "checks_failing"],
133 [1, "waiting_for_approval"],
134 ]);
135 expect(t.needsYou.map((x) => x.pull.number).sort()).toEqual([1, 3]);
136 });
137
138 it("keeps failures that are the viewer's, their agents' or on the default branch", () => {
139 const run = (id: string, actor: { id: string; login: string; kind: string }, ref: string, event = "push", at = "2026-10-13T07:00:00Z") => ({ id, runNumber: 1, workflow: "CI", title: id, ref, event, actor, conclusion: "failure", prNumber: null, at });
140 const t = classify(
141 input([
142 {
143 repo: repo("web"),
144 data: data({ failedRuns: [run("mine", { ...ME, kind: "user" }, "refs/heads/x"), run("bot", BOT, "refs/heads/y"), run("main", BOB, "refs/heads/main"), run("pr", BOB, "refs/heads/main", "pull_request"), run("other", BOB, "refs/heads/z")] }),
145 },
146 ]),
147 );
148 expect(t.failures.map((x) => x.run.id).sort()).toEqual(["bot", "main", "mine"]);
149 });
150});
151
152describe("pickRepos", () => {
153 const r = (id: string, pushed: string | null, archived = 0): Repo => ({ id, owner_id: "o", owner_login: "acme", owner_kind: "org", name: id, description: "", visibility: "private", default_branch: "main", archived, created_at: "2025-01-01T00:00:00Z", updated_at: "2025-01-01T00:00:00Z", pushed_at: pushed });
154 it("visits active, unarchived repositories, most recent activity first", () => {
155 const now = new Date("2026-10-13T09:00:00Z");
156 const picked = pickRepos([r("old", "2026-01-01T00:00:00Z"), r("pushed", "2026-10-12T00:00:00Z"), r("api", "2026-01-01T00:00:00Z"), r("arch", "2026-10-13T00:00:00Z", 1)], new Map([["api", "2026-10-13T08:00:00Z"]]), now);
157 expect(picked.repos.map((x) => x.id)).toEqual(["api", "pushed"]);
158 expect(picked.skipped).toBe(0);
159 });
160});