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.

127 lines · 8.2 KBRaw History
1import { describe, expect, it } from "vitest";
2import { agentPullState, approvalCandidates, classify, pickRepos, type ClassifyInput, type TodayRepo } from "../../src/agentic/today.js";
3import { checksState, 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
58describe("classify", () => {
59 it("asks the viewer for requested reviews, case-insensitively, but not on drafts", () => {
60 const t = classify(input([{ repo: repo("web"), data: data({ pulls: [pull({ number: 1, requestedReviewers: ["ayse"] }), pull({ number: 2, requestedReviewers: ["AYSE"], draft: true })] }) }]));
61 expect(t.needsYou.map((x) => [x.kind, x.pull.number])).toEqual([["review_requested", 1]]);
62 });
63
64 it("lists agent pull requests waiting for a person only where the viewer can approve", () => {
65 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"] })];
66 const t = classify(input([{ repo: repo("web"), data: data({ pulls }) }, { repo: repo("api"), data: data({ pulls: [pull({ number: 9, author: OTHER_BOT })] }) }], ["web"]));
67 expect(t.needsYou.map((x) => `${x.repo.id}#${x.pull.number}:${x.kind}`).sort()).toEqual(["web#1:agent_approval", "web#2:agent_approval"]);
68 });
69
70 it("only role-checks repositories with agent work waiting for a person", () => {
71 const repos = [
72 { repo: repo("a"), data: data({ pulls: [pull({ number: 1, author: OTHER_BOT })] }) },
73 { repo: repo("b"), data: data({ pulls: [pull({ number: 1, author: OTHER_BOT, approvedHead: true }), pull({ number: 2 })] }) },
74 { repo: repo("c"), data: data({ pulls: [pull({ number: 1, author: { ...ME, kind: "agent" } })] }) },
75 ];
76 expect(approvalCandidates(repos, ME)).toEqual(["a"]);
77 });
78
79 it("lists deployments waiting for the viewer as a required reviewer, unless they started the run and self-review is off", () => {
80 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 });
81 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")];
82 const t = classify({ ...input([{ repo: repo("web"), data: data({ deployments }) }, { repo: repo("old"), data: data({}) }]), ownAgentIds: new Set([BOT.id]) });
83 // r3 the viewer started, r5 their own agent did; r6's agent belongs to their organization, not them.
84 expect(t.deployments.map((x) => x.deployment.runId)).toEqual(["r4", "r1", "r6"]);
85 });
86
87 it("flags the viewer's own blocked pull requests", () => {
88 const mine = { ...ME, kind: "user" };
89 const t = classify(input([{ repo: repo("web"), data: data({ pulls: [pull({ number: 1, author: mine, changesRequestedBy: ["bob"] }), pull({ number: 2, author: mine, checks: "failure" }), pull({ number: 3, author: mine })] }) }]));
90 expect(t.needsYou.map((x) => [x.pull.number, x.kind]).sort()).toEqual([
91 [1, "changes_requested"],
92 [2, "checks_failing"],
93 ]);
94 });
95
96 it("shows the viewer's agents' pull requests with their state, newest first, and lists them once in needs-you", () => {
97 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 })] }) }]));
98 expect(t.agentsAtWork.map((x) => [x.pull.number, x.state])).toEqual([
99 [2, "checks_failing"],
100 [1, "waiting_for_approval"],
101 ]);
102 expect(t.needsYou.map((x) => x.pull.number).sort()).toEqual([1, 3]);
103 });
104
105 it("keeps failures that are the viewer's, their agents' or on the default branch", () => {
106 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 });
107 const t = classify(
108 input([
109 {
110 repo: repo("web"),
111 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")] }),
112 },
113 ]),
114 );
115 expect(t.failures.map((x) => x.run.id).sort()).toEqual(["bot", "main", "mine"]);
116 });
117});
118
119describe("pickRepos", () => {
120 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 });
121 it("visits active, unarchived repositories, most recent activity first", () => {
122 const now = new Date("2026-10-13T09:00:00Z");
123 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);
124 expect(picked.repos.map((x) => x.id)).toEqual(["api", "pushed"]);
125 expect(picked.skipped).toBe(0);
126 });
127});