flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/services/runner/src/mentions.test.ts

164 lines6,316 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { LifecycleJob, MentionJob, Result } from "@g1t/contracts";
5
6import { type MentionPorts, buildMentionPrompt, handleMention, planMention } from "./mentions.ts";
7
8const ana = { id: "usr_ana", username: "ana", verified: true, workspaces: [{ slug: "acme", role: "member" as const }] };
9
10function onIssue(intent: MentionJob["intent"], extra: Partial<MentionJob> = {}): MentionJob {
11 return {
12 commentId: "cmt_1",
13 actor: ana,
14 repo: { namespace: "acme", name: "site" },
15 number: 7,
16 body: "@g1t-agent take this",
17 intent,
18 member: true,
19 defaultBranch: "main",
20 issueOpen: true,
21 workingPull: null,
22 pull: null,
23 ...extra,
24 };
25}
26
27function onPull(intent: MentionJob["intent"], pull: Partial<NonNullable<MentionJob["pull"]>> = {}): MentionJob {
28 return onIssue(intent, {
29 issueOpen: null,
30 pull: {
31 id: "pr_1",
32 status: "open",
33 agentAuthored: true,
34 source: { namespace: "acme", name: "site-pr-1" },
35 inRepo: false,
36 branch: null,
37 headCommit: "abc",
38 files: ["src/a.ts"],
39 ...pull,
40 },
41 });
42}
43
44test("on an issue, a request assigns it and a question is answered", () => {
45 assert.equal(planMention(onIssue("work")).kind, "assign");
46 assert.equal(planMention(onIssue("question")).kind, "answer");
47 assert.equal(planMention(onIssue("work", { issueOpen: false })).kind, "closed");
48 // Already at work on it: not a second attempt.
49 const busy = planMention(onIssue("work", { workingPull: 9 }));
50 assert.equal(busy.kind, "closed");
51 assert.match(busy.kind === "closed" ? busy.reason : "", /already working on this in #9/);
52 assert.equal(planMention(onIssue("question", { workingPull: 9 })).kind, "answer");
53});
54
55test("on g1t-agent's pull request, a request sends it back", () => {
56 assert.equal(planMention(onPull("work")).kind, "revise");
57 assert.equal(planMention(onPull("work", { status: "draft" })).kind, "message");
58 assert.equal(planMention(onPull("question")).kind, "answer");
59 assert.equal(planMention(onPull("review")).kind, "review");
60 assert.equal(planMention(onPull("work", { status: "merged" })).kind, "closed");
61});
62
63test("on anyone else's pull request, it reviews or answers", () => {
64 assert.equal(planMention(onPull("review", { agentAuthored: false })).kind, "review");
65 assert.equal(planMention(onPull("work", { agentAuthored: false })).kind, "answer");
66 assert.equal(planMention(onPull("question", { agentAuthored: false, status: "closed" })).kind, "answer");
67});
68
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69test("someone without Write on the repository is thanked and nothing starts", async () => {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API70 const { ports, replies, started } = fakePorts();
71 const plan = await handleMention(onIssue("work", { member: false }), ports);
72 assert.equal(plan.kind, "not_member");
73 assert.deepEqual(started, []);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look74 assert.match(replies[0], /Putting g1t-agent to work needs the Write role on acme\/[^,]+, so I have left this/);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API75});
76
77test("a workspace that cannot run agents is told why, and the run is recorded", async () => {
78 const { ports, replies, started, recorded } = fakePorts({ refusal: "Its free allowance is used up." });
79 await handleMention(onIssue("work"), ports);
80 assert.deepEqual(started, []);
81 assert.deepEqual(recorded, ["Its free allowance is used up."]);
82 assert.match(replies[0], /could not start on this: Its free allowance is used up/);
83});
84
85test("a request on an issue assigns it and says where the work is", async () => {
86 const { ports, replies, started } = fakePorts();
87 await handleMention(onIssue("work"), ports);
88 assert.deepEqual(started, ["assign"]);
89 assert.match(replies[0], /I opened #12/);
90});
91
92test("a revision already under way gets the comment as a message", async () => {
93 const { ports, replies, started } = fakePorts({ revision: { ok: false, error: { code: "conflict", message: "busy" } } });
94 await handleMention(onPull("work"), ports);
95 assert.deepEqual(started, ["message"]);
96 assert.match(replies[0], /passed your comment on/);
97});
98
99test("a revision starts with the comment, credited to whoever asked", async () => {
100 const { ports, started } = fakePorts();
101 await handleMention(onPull("work"), ports);
102 assert.deepEqual(started, ["revise:ana"]);
103});
104
105test("an answer is the reply, so nothing else is said", async () => {
106 const { ports, replies, started } = fakePorts();
107 await handleMention(onIssue("question"), ports);
108 assert.deepEqual(started, ["answer"]);
109 assert.deepEqual(replies, []);
110});
111
112test("the question is in the prompt, and nothing is to change", () => {
113 const prompt = buildMentionPrompt(onIssue("question", { body: "@g1t-agent why is this slow?" }), {
114 title: "Search is slow",
115 body: "It takes 4s.",
116 thread: null,
117 });
118 assert.match(prompt, /issue #7/);
119 assert.match(prompt, /why is this slow\?/);
120 assert.match(prompt, /Do not change any files/);
121});
122
123function fakePorts(options: { refusal?: string; revision?: Result<LifecycleJob> } = {}) {
124 const replies: string[] = [];
125 const started: string[] = [];
126 const recorded: string[] = [];
127 const job = { pullId: "pr_1" } as LifecycleJob;
128 const ports: MentionPorts = {
129 mentions: {
130 takeMention: async () => null,
131 mentionRevision: async () => options.revision ?? { ok: true, value: job },
132 replyMention: async (_id, body) => {
133 replies.push(body);
134 return true;
135 },
136 getAgentRules: async () => ({ ok: true, value: { label: null, updatedBy: null, updatedAt: null } }),
137 setAgentRules: async () => ({ ok: true, value: { label: null, updatedBy: null, updatedAt: null } }),
138 },
139 refusal: async () => options.refusal ?? null,
140 assign: async () => {
141 started.push("assign");
142 return { ok: true, value: { number: 12 } as never };
143 },
144 revise: async (_job, by) => {
145 started.push(`revise:${by}`);
146 },
147 review: async () => {
148 started.push("review");
149 return { ok: true, value: true };
150 },
151 answer: async () => {
152 started.push("answer");
153 return { ok: true, value: true };
154 },
155 message: async () => {
156 started.push("message");
157 return { ok: true, value: null };
158 },
159 record: async (_job, why) => {
160 recorded.push(why);
161 },
162 };
163 return { ports, replies, started, recorded };
164}