g1t/services/runner/src/mentions.test.ts
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 API | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import type { LifecycleJob, MentionJob, Result } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import { type MentionPorts, buildMentionPrompt, handleMention, planMention } from "./mentions.ts"; | |
| 7 | ||
| 8 | const ana = { id: "usr_ana", username: "ana", verified: true, workspaces: [{ slug: "acme", role: "member" as const }] }; | |
| 9 | ||
| 10 | function 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 | ||
| 27 | function 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 | ||
| 44 | test("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 | ||
| 55 | test("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 | ||
| 63 | test("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 | ||
| 69 | test("someone outside the workspace is thanked and nothing starts", async () => { | |
| 70 | 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, []); | |
| 74 | assert.match(replies[0], /Only members of the acme workspace can put g1t-agent to work here/); | |
| 75 | }); | |
| 76 | ||
| 77 | test("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 | ||
| 85 | test("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 | ||
| 92 | test("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 | ||
| 99 | test("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 | ||
| 105 | test("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 | ||
| 112 | test("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 | ||
| 123 | function 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 | } |