Skip to content

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.sh
269 linesCodeBlameRaw

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.

Merge main into stream A: coordination meets the merge queue's decisions and the CLI. get_work gives the issue's decision records newest first and its latest integration_failure decision as last_integration_failure, a merge queue failure noted in a pull request's session is noted in the live harness sessions on it or its issue too, the CLI's coordination commands use the generated operations now that the API describes them (its pending list is empty) and show get_work's definition of done, and the session, decision and coordination events sit together in the catalog, the webhook form and the docs.1// Tests for the plugin's hooks: node --test plugins/claude-code/test/
2import assert from "node:assert/strict";
3import { spawnSync } from "node:child_process";
4import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
5import { tmpdir } from "node:os";
6import { join } from "node:path";
7import { afterEach, beforeEach, describe, test } from "node:test";
8import { fileURLToPath } from "node:url";
9
10import {
11 PROMPT_LIMIT,
12 hook,
13 isG1tRemote,
14 isGitPush,
15 issueFromBranch,
16 issueFromCommand,
17 plan,
18 redact,
19 sessionFiles,
20 summarizeTool,
21 work,
22} from "../hooks/g1t.mjs";
23
24const HERE = fileURLToPath(new URL(".", import.meta.url));
25const SCRIPT = join(HERE, "..", "hooks", "g1t.mjs");
26const FAKE = join(HERE, "fake-g1t.mjs");
27
28const base = { session_id: "abc-123", cwd: "/work/repo", transcript_path: "/t.jsonl", permission_mode: "default" };
29
30describe("plan", () => {
31 test("SessionStart starts the session", () => {
32 const job = plan({ ...base, hook_event_name: "SessionStart", source: "startup" }, {});
33 assert.equal(job.start, true);
34 assert.deepEqual(job.steps, []);
35 });
36
37 test("UserPromptSubmit logs the prompt, cut to the limit", () => {
38 const prompt = "x".repeat(PROMPT_LIMIT + 500);
39 const job = plan({ ...base, hook_event_name: "UserPromptSubmit", prompt }, {});
40 const args = job.steps[0].args;
41 assert.deepEqual(args.slice(0, 5), ["session", "log", "--kind", "prompt", "--"]);
42 assert.equal(args[5].length, PROMPT_LIMIT);
43 assert.ok(args[5].endsWith("…"));
44 });
45
46 test("G1T_RECORD_PROMPTS=0 keeps prompts back, and G1T_RECORD=0 everything", () => {
47 assert.equal(plan({ ...base, hook_event_name: "UserPromptSubmit", prompt: "hi" }, { G1T_RECORD_PROMPTS: "0" }), null);
48 assert.ok(plan({ ...base, hook_event_name: "Stop" }, { G1T_RECORD_PROMPTS: "0" }));
49 for (const hook_event_name of ["SessionStart", "UserPromptSubmit", "PostToolUse", "Stop", "SessionEnd"]) {
50 assert.equal(plan({ ...base, hook_event_name, prompt: "hi", tool_name: "Bash" }, { G1T_RECORD: "0" }), null);
51 }
52 });
53
54 test("PostToolUse sends the tool and its input's summary, never its response", () => {
55 const event = {
56 ...base,
57 hook_event_name: "PostToolUse",
58 tool_name: "Bash",
59 tool_input: { command: "npm test", description: "Run the tests" },
60 tool_response: { stdout: "SECRET OUTPUT 1234", stderr: "", interrupted: false },
61 tool_use_id: "toolu_1",
62 };
63 const job = plan(event, {});
64 assert.deepEqual(job.steps, [{ args: ["session", "log", "--kind", "tool_call", "--", "Bash: npm test"] }]);
65 assert.ok(!JSON.stringify(job).includes("SECRET OUTPUT"));
66 });
67
68 test("PostToolUse after a git push updates the head commit", () => {
69 const job = plan({ ...base, hook_event_name: "PostToolUse", tool_name: "Bash", tool_input: { command: "git push -u origin 42-fix" } }, {});
70 assert.deepEqual(job.steps[1], { headCommit: true });
71 const other = plan({ ...base, hook_event_name: "PostToolUse", tool_name: "Bash", tool_input: { command: "git status" } }, {});
72 assert.equal(other.steps.length, 1);
73 });
74
75 test("PostToolUse remembers the issue a command claims", () => {
76 const job = plan({ ...base, hook_event_name: "PostToolUse", tool_name: "Bash", tool_input: { command: "g1t work claim 42 --note x" } }, {});
77 assert.equal(job.issue, 42);
78 });
79
80 test("Stop sends a heartbeat and SessionEnd ends the session with its reason", () => {
81 assert.deepEqual(plan({ ...base, hook_event_name: "Stop", stop_hook_active: false }, {}).steps, [{ args: ["session", "heartbeat"] }]);
82 const end = plan({ ...base, hook_event_name: "SessionEnd", reason: "prompt_input_exit" }, {});
83 assert.deepEqual(end.steps, [{ args: ["session", "end", "--reason", "prompt_input_exit"] }]);
84 assert.equal(end.end, true);
85 });
86
87 test("events without a session, and other events, do nothing", () => {
88 assert.equal(plan({ hook_event_name: "Stop" }, {}), null);
89 assert.equal(plan({ ...base, hook_event_name: "PreToolUse", tool_name: "Bash" }, {}), null);
90 });
91});
92
93describe("summaries", () => {
94 test("say what a tool was called with, in one short line", () => {
95 assert.equal(summarizeTool("Edit", { file_path: "/work/repo/src/a.ts", old_string: "x", new_string: "y" }, "/work/repo"), "Edit: src/a.ts");
96 assert.equal(summarizeTool("Grep", { pattern: "TODO", path: "src" }, "/w"), "Grep: TODO in src");
97 assert.equal(summarizeTool("WebFetch", { url: "https://example.com", prompt: "p" }), "WebFetch: https://example.com");
98 assert.equal(summarizeTool("Bash", { command: "echo one\necho two" }), "Bash: echo one …");
99 assert.equal(summarizeTool("mcp__g1t__issue", { action: "get", number: 4 }), "mcp__g1t__issue: get");
100 assert.ok(summarizeTool("Bash", { command: "y".repeat(1000) }).length <= 200);
101 });
102
103 test("hide what looks like a secret", () => {
104 const line = redact('curl -H "Authorization: Bearer g1t_abcdefghijklmnop" https://u:pw@x.com GITHUB_TOKEN=ghp_123456789012 --token abc');
105 assert.ok(!line.includes("abcdefghijklmnop"));
106 assert.ok(!line.includes("ghp_123456789012"));
107 assert.ok(!line.includes("u:pw@"));
108 assert.ok(!line.includes("--token abc"));
109 });
110
111 test("find the issue in a branch name", () => {
112 assert.equal(issueFromBranch("42-fix-login"), 42);
113 assert.equal(issueFromBranch("issue-7"), 7);
114 assert.equal(issueFromBranch("fix/123-thing"), 123);
115 assert.equal(issueFromBranch("main"), null);
116 assert.equal(issueFromBranch("release/1.2"), null);
117 assert.equal(issueFromBranch("HEAD"), null);
118 assert.equal(issueFromCommand("g1t session attach --issue 9"), 9);
119 assert.equal(issueFromCommand("g1t pr create --issue=12 --draft"), 12);
120 assert.equal(isGitPush("cd x && git -C repo push"), true);
121 assert.equal(isGitPush("git pushd"), false);
122 assert.equal(isG1tRemote(["remote.origin.url https://g1t.sh/a/b.git"], {}), true);
123 assert.equal(isG1tRemote(["remote.origin.url https://example.com/a/b.git"], {}), false);
124 assert.equal(isG1tRemote(["remote.origin.url https://git.acme.dev/a/b.git"], { G1T_HOST: "git.acme.dev" }), true);
125 });
126});
127
128describe("the plugin's files", () => {
129 const root = join(HERE, "..", "..", "..");
130 test("parse as JSON", () => {
131 for (const file of [".claude-plugin/marketplace.json", "plugins/claude-code/.claude-plugin/plugin.json", "plugins/claude-code/.mcp.json", "plugins/claude-code/hooks/hooks.json"]) {
132 assert.doesNotThrow(() => JSON.parse(readFileSync(join(root, file), "utf8")), file);
133 }
134 });
135
136 test("the AGENTS.md snippet is the one in the docs", () => {
137 const snippet = readFileSync(join(root, "plugins/other-harnesses/AGENTS.snippet.md"), "utf8").replaceAll("\r\n", "\n");
138 const guide = readFileSync(join(root, "apps/docs/src/content/docs/guides/claude-code-plugin.md"), "utf8").replaceAll("\r\n", "\n");
139 const block = /```md title="AGENTS.md"\n([\s\S]*?)```/.exec(guide)?.[1];
140 assert.equal(block, snippet);
141 });
142});
143
144describe("with a working copy", () => {
145 let dir, env, log;
146
147 const git = (...args) => spawnSync("git", args, { cwd: dir, encoding: "utf8" });
148 const calls = () =>
149 existsSync(log)
150 ? readFileSync(log, "utf8")
151 .trim()
152 .split("\n")
153 .filter(Boolean)
154 .map((l) => JSON.parse(l))
155 : [];
156
157 beforeEach(() => {
158 dir = mkdtempSync(join(tmpdir(), "g1t-hook-"));
159 log = join(dir, "calls.jsonl");
160 git("init", "-q", "-b", "42-fix-login");
161 git("remote", "add", "origin", "https://g1t.sh/acme/app.git");
162 env = { PATH: process.env.PATH, G1T_BIN: FAKE, FAKE_G1T_LOG: log, CLAUDE_PLUGIN_DATA: join(dir, ".data") };
163 });
164 afterEach(() => rmSync(dir, { recursive: true, force: true, maxRetries: 20, retryDelay: 100 }));
165
166 test("SessionStart gives Claude the branch's issue's work context", () => {
167 const { output, job } = hook({ ...base, cwd: dir, hook_event_name: "SessionStart", source: "startup" }, env);
168 const context = output.hookSpecificOutput.additionalContext;
169 assert.equal(output.hookSpecificOutput.hookEventName, "SessionStart");
170 assert.match(context, /issue #42/);
171 assert.match(context, /Definition of done: it works/);
172 assert.deepEqual(calls()[0].args, ["work", "context", "42"]);
173 assert.equal(job.start, true);
174 });
175
176 test("the background half starts the session once, then logs into it", () => {
177 work({ cwd: dir, sid: base.session_id, start: true, steps: [] }, env);
178 const { job } = hook({ ...base, cwd: dir, hook_event_name: "UserPromptSubmit", prompt: "fix the login" }, env);
179 // No SessionStart marked this session as one in a g1t working copy.
180 assert.equal(job, null);
181 work(plan({ ...base, cwd: dir, hook_event_name: "UserPromptSubmit", prompt: "fix the login" }, env), env);
182 const [start, logged] = calls();
183 assert.deepEqual(start.args, ["session", "start", "--harness", "claude_code", "--harness-session-id", "abc-123", "--json"]);
184 assert.deepEqual(logged, { args: ["session", "log", "--kind", "prompt", "--", "fix the login"], session: "ses_fake01" });
185 assert.equal(readFileSync(sessionFiles(base.session_id, env).id, "utf8"), "ses_fake01");
186 });
187
188 test("hooks after SessionStart record; SessionEnd forgets the session", () => {
189 hook({ ...base, cwd: dir, hook_event_name: "SessionStart", source: "startup" }, env);
190 const files = sessionFiles(base.session_id, env);
191 assert.ok(existsSync(files.marker));
192 const { job } = hook({ ...base, cwd: dir, hook_event_name: "Stop" }, env);
193 work(job, env);
194 work(plan({ ...base, cwd: dir, hook_event_name: "SessionEnd", reason: "clear" }, env), env);
195 const args = calls().map((c) => c.args.join(" "));
196 assert.ok(args.includes("session heartbeat"));
197 assert.ok(args.includes("session end --reason clear"));
198 assert.ok(!existsSync(files.marker) && !existsSync(files.id));
199 });
200
201 test("a push that reached the remote updates the head commit", () => {
202 git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "--allow-empty", "-m", "one");
203 git("update-ref", "refs/remotes/origin/42-fix-login", "HEAD");
204 const head = git("rev-parse", "HEAD").stdout.trim();
205 work(plan({ ...base, cwd: dir, hook_event_name: "PostToolUse", tool_name: "Bash", tool_input: { command: "git push" } }, env), env);
206 const update = calls().find((c) => c.args[0] === "api");
207 assert.deepEqual(update.args, ["api", "update_session", "-f", "id=ses_fake01", "-f", `head_commit=${head}`, "--silent"]);
208 });
209
210 test("a missing CLI records nothing and says so once", () => {
211 const missing = { ...env, G1T_BIN: join(dir, "no-such-g1t") };
212 const first = hook({ ...base, cwd: dir, hook_event_name: "SessionStart" }, missing);
213 assert.match(first.output.systemMessage, /not installed/);
214 assert.equal(first.job, null);
215 const second = hook({ ...base, cwd: dir, hook_event_name: "SessionStart" }, missing);
216 assert.equal(second.output, null);
217 assert.equal(hook({ ...base, cwd: dir, hook_event_name: "Stop" }, missing).job, null);
218 assert.deepEqual(calls(), []);
219 });
220
221 test("a signed-out CLI is said once, at the next prompt", () => {
222 hook({ ...base, cwd: dir, hook_event_name: "SessionStart" }, { ...env, CLAUDE_PLUGIN_DATA: env.CLAUDE_PLUGIN_DATA });
223 work({ cwd: dir, sid: base.session_id, start: true, steps: [] }, { ...env, FAKE_G1T_EXIT: "4" });
224 const first = hook({ ...base, cwd: dir, hook_event_name: "UserPromptSubmit", prompt: "hi" }, env);
225 assert.match(first.output.systemMessage, /not signed in/);
226 const second = hook({ ...base, cwd: dir, hook_event_name: "UserPromptSubmit", prompt: "hi" }, env);
227 assert.equal(second.output, null);
228 });
229
230 test("outside a g1t repository nothing happens", () => {
231 git("remote", "set-url", "origin", "https://example.com/acme/app.git");
232 const { output, job } = hook({ ...base, cwd: dir, hook_event_name: "SessionStart" }, env);
233 assert.equal(output, null);
234 assert.equal(job, null);
235 assert.equal(hook({ ...base, cwd: dir, hook_event_name: "Stop" }, env).job, null);
236 assert.deepEqual(calls(), []);
237 });
238
239 test("the script answers SessionStart on stdin quickly, as JSON", async () => {
240 const started = Date.now();
241 const out = spawnSync(process.execPath, [SCRIPT], {
242 input: JSON.stringify({ ...base, cwd: dir, hook_event_name: "SessionStart", source: "startup" }),
243 encoding: "utf8",
244 env: { ...process.env, ...env },
245 });
246 const took = Date.now() - started;
247 assert.equal(out.status, 0);
248 assert.match(JSON.parse(out.stdout).hookSpecificOutput.additionalContext, /issue #42/);
249 assert.ok(took < 3000, `took ${took} ms`);
250 // The detached half starts the session after the hook has returned.
251 const id = sessionFiles(base.session_id, env).id;
252 for (let i = 0; i < 100 && !existsSync(id); i++) await new Promise((done) => setTimeout(done, 50));
253 assert.equal(readFileSync(id, "utf8"), "ses_fake01");
254 await new Promise((done) => setTimeout(done, 200));
255 });
256
257 test("the script prints nothing for a prompt, and never fails on bad input", () => {
258 const prompt = spawnSync(process.execPath, [SCRIPT], {
259 input: JSON.stringify({ ...base, cwd: dir, hook_event_name: "UserPromptSubmit", prompt: "hello" }),
260 encoding: "utf8",
261 env: { ...process.env, ...env },
262 });
263 assert.equal(prompt.status, 0);
264 assert.equal(prompt.stdout, "");
265 const bad = spawnSync(process.execPath, [SCRIPT], { input: "not json", encoding: "utf8", env: { ...process.env, ...env } });
266 assert.equal(bad.status, 0);
267 assert.equal(bad.stdout, "");
268 });
269});

This file's history is long; its oldest lines are credited to the oldest commit read.