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
243 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { G1T_KEY, isG1t, parsePrincipalKey } from "@g1t/contracts/chat";
5import { ASK_MAX_MESSAGE_CHARS, ASK_MAX_TOTAL_CHARS } from "@g1t/contracts/integrations";
6
7import { dmKey, dmMembers } from "./names.ts";
8import {
9 AnswerReader,
10 answerAllowed,
11 conversation,
12 finalBody,
13 g1tDmPerson,
14 latestPage,
15 metaColumn,
16 notSetUpText,
17 pageFrom,
18 readsNote,
19 shownAnswer,
20 startsAnswer,
21 type SaidRow,
22} from "./g1t.ts";
23
24const ana = "user:u1";
25const bo = "user:u2";
26
27test("g1t is one reserved member, system:g1t, and only that system key parses", () => {
28 assert.deepEqual(parsePrincipalKey(G1T_KEY), { kind: "system", id: "g1t" });
29 assert.equal(parsePrincipalKey("system:other"), null);
30 assert.ok(isG1t({ kind: "system", id: "g1t" }));
31 assert.ok(!isG1t({ kind: "user", id: "g1t" }));
32});
33
34test("a direct message with g1t is one person and g1t, keyed like any other", () => {
35 const members = dmMembers(ana, [G1T_KEY]);
36 assert.equal(g1tDmPerson(members), ana);
37 // Opening it again, from either side's order, finds the same one.
38 assert.equal(dmKey(members), dmKey(dmMembers(ana, [G1T_KEY, G1T_KEY])));
39 assert.equal(g1tDmPerson(dmMembers(ana, [G1T_KEY, bo])), null);
40 assert.equal(g1tDmPerson(dmMembers(ana, [bo])), null);
41 assert.equal(g1tDmPerson([G1T_KEY]), null);
42 assert.equal(g1tDmPerson(["agent:a1", G1T_KEY]), null);
43});
44
45test("only a person's own top-level words in their direct message with g1t start an answer", () => {
46 const members = [G1T_KEY, ana];
47 const base = { channelKind: "dm" as const, members, author: ana, threadRoot: null, card: false, body: "What shipped today?" };
48 assert.ok(startsAnswer(base));
49 assert.ok(!startsAnswer({ ...base, author: G1T_KEY }), "g1t's own messages never do");
50 assert.ok(!startsAnswer({ ...base, threadRoot: "msg_1" }), "nor thread replies");
51 assert.ok(!startsAnswer({ ...base, card: true }), "nor cards");
52 assert.ok(!startsAnswer({ ...base, body: " " }));
53 assert.ok(!startsAnswer({ ...base, members: [ana, bo] }), "a direct message between people");
54 assert.ok(!startsAnswer({ ...base, channelKind: "channel" }), "a channel, even one g1t were in");
55 assert.ok(!startsAnswer({ ...base, author: bo }), "someone not in it");
56});
57
58const row = (id: string, author: string, body: string, meta: object | null = null, extra: Partial<SaidRow> = {}): SaidRow => ({
59 id,
60 author,
61 kind: "text",
62 body,
63 meta: meta ? JSON.stringify(meta) : null,
64 deleted_at: null,
65 ...extra,
66});
67const done = { answer: { state: "done" } };
68
69test("the conversation maps the person to user and g1t's answers to assistant, alternating", () => {
70 const rows = [
71 row("m1", ana, "Hi"),
72 row("m2", G1T_KEY, "Hello! What can I do?", done),
73 row("m3", ana, "What shipped?"),
74 row("m4", ana, "Today, I mean."),
75 ];
76 assert.deepEqual(conversation(rows), [
77 { role: "user", content: "Hi" },
78 { role: "assistant", content: "Hello! What can I do?" },
79 { role: "user", content: "What shipped?\n\nToday, I mean." },
80 ]);
81});
82
83test("an earlier answer is read back with what it read in g1t, so it is not taken for made up", () => {
84 const steps = [
85 { type: "tool", id: "t1", name: "repository", action: "list", label: "Listed repositories", status: "done", summary: "9 repositories" },
86 { type: "tool", id: "t2", name: "repository", action: "get", label: "Read acme/rocket", status: "failed", summary: "Repository not found" },
87 { type: "sources", sources: [] },
88 ];
89 const rows = [
90 row("m1", ana, "Open issues in acme/rocket?"),
91 row("m2", G1T_KEY, "There is no acme/rocket.", { answer: { state: "done", steps } }),
92 row("m3", ana, "Did you check?"),
93 ];
94 assert.deepEqual(conversation(rows)[1], {
95 role: "assistant",
96 content:
97 "[g1t's tool reads for this answer: Listed repositories: 9 repositories; Read acme/rocket (failed: Repository not found)]\n\nThere is no acme/rocket.",
98 });
99 assert.equal(readsNote([]), null);
100 assert.equal(readsNote(undefined), null);
101 const many = Array.from({ length: 50 }, (_, i) => ({ type: "tool", id: `t${i}`, label: `Read file number ${i}`, status: "done" }));
102 const long = readsNote(many)!;
103 assert.ok(long.length <= 800 && long.endsWith("…]"));
104});
105
106test("the conversation leaves out failed answers, the set-up note, the answer being written, cards and deleted messages", () => {
107 const rows = [
108 row("m1", ana, "First"),
109 row("m2", G1T_KEY, "I couldn't answer that", { answer: { state: "error", error: "x" } }),
110 row("m3", G1T_KEY, "Choose a provider", { answer: { state: "done", setup: "missing" } }),
111 row("m4", ana, "gone", null, { deleted_at: "2026-10-12T00:00:00Z" }),
112 row("m5", ana, "card", null, { kind: "card" }),
113 row("m6", ana, "Second"),
114 row("m7", G1T_KEY, "", { answer: { state: "thinking" } }),
115 ];
116 assert.deepEqual(conversation(rows), [{ role: "user", content: "First\n\nSecond" }]);
117});
118
119test("a stopped answer still counts as what g1t said", () => {
120 const rows = [row("m1", ana, "Q"), row("m2", G1T_KEY, "Half an ans", { answer: { state: "stopped" } }), row("m3", ana, "Go on")];
121 assert.equal(conversation(rows).length, 3);
122});
123
124test("a new conversation reads only what comes after the latest mark", () => {
125 const rows = [
126 row("m1", ana, "Old question"),
127 row("m2", G1T_KEY, "Old answer", done),
128 row("m3", G1T_KEY, "New conversation", { reset: true }),
129 row("m4", ana, "Fresh question"),
130 ];
131 assert.deepEqual(conversation(rows), [{ role: "user", content: "Fresh question" }]);
132 assert.deepEqual(conversation(rows.slice(0, 3)), [], "nothing to answer right after a mark");
133});
134
135test("the conversation ends with the person and keeps within Ask's limits, newest first", () => {
136 assert.deepEqual(conversation([row("m1", ana, "Q"), row("m2", G1T_KEY, "A", done)]), []);
137 const long = "x".repeat(ASK_MAX_MESSAGE_CHARS + 50);
138 const [only] = conversation([row("m1", ana, `start${long}end`)]);
139 assert.equal(only!.content.length, ASK_MAX_MESSAGE_CHARS);
140 assert.ok(only!.content.endsWith("end"), "a long question keeps its end");
141 const many: SaidRow[] = [];
142 for (let i = 0; i < 60; i++) many.push(row(`q${i}`, ana, `q${i} ${"y".repeat(3_000)}`), row(`a${i}`, G1T_KEY, `a${i} ${"z".repeat(3_000)}`, done));
143 many.push(row("last", ana, "the latest"));
144 const kept = conversation(many);
145 assert.equal(kept[0]!.role, "user");
146 assert.equal(kept.at(-1)!.content, "the latest");
147 assert.ok(kept.length <= 40);
148 assert.ok(kept.reduce((sum, turn) => sum + turn.content.length, 0) <= ASK_MAX_TOTAL_CHARS);
149 for (let i = 1; i < kept.length; i++) assert.notEqual(kept[i]!.role, kept[i - 1]!.role);
150});
151
152test("the page an answer is told is the newest question's, as sent from the top bar", () => {
153 const page = { path: "/acme/web/issues/12", repo: { namespace: "acme", name: "web" }, issue: 12, pull: null };
154 assert.deepEqual(latestPage([row("m1", ana, "Q", { page }), row("m2", G1T_KEY, "A", done)]), page);
155 assert.equal(latestPage([row("m1", ana, "Q", { page }), row("m2", ana, "From chat")]), null);
156 assert.deepEqual(pageFrom({ path: "/acme/web", repo: { namespace: "acme", name: "web" }, issue: -1, extra: true }), {
157 path: "/acme/web",
158 repo: { namespace: "acme", name: "web" },
159 issue: null,
160 pull: null,
161 time_zone: null,
162 });
163 // The browser's time zone is kept when it is one; anything else is dropped.
164 assert.equal(pageFrom({ path: "/acme", time_zone: "America/Denver" })?.time_zone, "America/Denver");
165 assert.equal(pageFrom({ path: "/acme", time_zone: "<script>" })?.time_zone, null);
166 assert.deepEqual(pageFrom({ time_zone: "Asia/Kolkata" }), { path: null, repo: null, issue: null, pull: null, time_zone: "Asia/Kolkata" });
167 assert.equal(pageFrom({ path: "https://elsewhere" }), null);
168 assert.equal(pageFrom("nope"), null);
169 assert.equal(metaColumn({}), null);
170});
171
172test("with no provider chosen, g1t says where an owner chooses one", () => {
173 const text = notSetUpText("acme");
174 assert.match(text, /Integrations → Your AI providers → Ask g1t/);
175 assert.match(text, /\(\/acme\/-\/integrations\/models#ask\)/);
176 assert.match(text, /ask one of them/);
177});
178
179test("an answer is read as it streams, and unknown events are kept as steps, never text", () => {
180 const reader = new AnswerReader();
181 assert.ok(reader.feed('{"type":"meta","provider":"anthropic","provider_label":"Anthropic","model":"m"}\n{"type":"te'));
182 assert.equal(reader.answer.state, "thinking");
183 reader.feed('xt","text":"Hel"}\n{"type":"tool","name":"search","input":{"q":"x"}}\n');
184 reader.feed('{"type":"text","text":"lo"}\nnot json\n{"type":"done","stop_reason":"end_turn"}\n{"type":"text","text":"after"}\n');
185 assert.equal(reader.body, "Hello");
186 assert.equal(reader.answer.state, "done");
187 assert.equal(reader.answer.provider_label, "Anthropic");
188 assert.deepEqual(reader.answer.steps, [{ type: "tool", name: "search", input: { q: "x" } }]);
189});
190
191test("a tool step is kept once, as it last was, and sources are kept whole", () => {
192 const reader = new AnswerReader();
193 const step = { type: "tool", id: "step_0_0", name: "workspace", action: "activity", label: "Read activity", status: "running", summary: null, link: null };
194 reader.feed(`${JSON.stringify(step)}\n${JSON.stringify({ ...step, id: "step_0_1" })}\n`);
195 reader.feed(`${JSON.stringify({ ...step, status: "done", summary: "12 results" })}\n`);
196 reader.feed('{"type":"sources","sources":[{"title":"a","url":"https://g1t.sh/a","cited":true}]}\n');
197 reader.feed('{"type":"sources","sources":[{"title":"b","url":"https://g1t.sh/b","cited":true}]}\n');
198 const steps = reader.answer.steps ?? [];
199 assert.deepEqual(
200 steps.map((kept) => [kept.type, kept.id ?? null, kept.status ?? null]),
201 [
202 ["tool", "step_0_0", "done"],
203 ["tool", "step_0_1", "running"],
204 ["sources", null, null],
205 ],
206 );
207 assert.equal((steps[2] as { sources: { title: string }[] }).sources[0]!.title, "b");
208});
209
210test("an answer that breaks off or fails is never silence", () => {
211 const broken = new AnswerReader();
212 broken.feed('{"type":"text","text":"Half"}\n');
213 broken.finish();
214 assert.equal(broken.answer.state, "error");
215 assert.equal(finalBody(broken.body, broken.answer), "Half");
216 const failed = new AnswerReader();
217 failed.feed('{"type":"error","message":"Anthropic said the key is invalid."}');
218 failed.finish();
219 assert.equal(failed.answer.state, "error");
220 assert.equal(finalBody(failed.body, failed.answer), "I couldn't answer that: Anthropic said the key is invalid.");
221 const stopped = new AnswerReader();
222 stopped.stop();
223 assert.equal(finalBody(stopped.body, stopped.answer), "_Stopped before answering._");
224});
225
226test("one person's answers are limited to twenty a minute", () => {
227 let times: number[] = [];
228 for (let i = 0; i < 20; i++) {
229 const next = answerAllowed(times, 1_000 + i);
230 assert.ok(next.ok);
231 times = next.times;
232 }
233 assert.ok(!answerAllowed(times, 2_000).ok);
234 assert.ok(answerAllowed(times, 70_000).ok);
235});
236
237test("an answer left mid-stream by a room that went away shows as stopped", () => {
238 const at = "2026-10-12T00:00:00Z";
239 assert.equal(shownAnswer({ state: "streaming" }, at, Date.parse(at) + 60_000)!.state, "streaming");
240 assert.equal(shownAnswer({ state: "streaming" }, at, Date.parse(at) + 3_600_000)!.state, "stopped");
241 assert.equal(shownAnswer({ state: "done" }, at, Date.parse(at) + 3_600_000)!.state, "done");
242 assert.equal(shownAnswer(undefined, at, 0), null);
243});