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.

299 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { ChatMessage, ChatSidebarEntry } from "@g1t/contracts";
5
6import {
7 backoff,
8 blocks,
9 channelName,
10 channelPath,
11 dayLabel,
12 filterEntries,
13 formerAgentDm,
14 formerAgentLine,
15 inline,
16 mentionNames,
17 mentionQuery,
18 mergeMessages,
19 safeHref,
20 sections,
21 shownHandle,
22 shownName,
23 timeline,
24 typingLabel,
25 unreadTotals,
26 type ShownMessage,
27} from "./chat.ts";
28
29const ana = { kind: "user" as const, id: "u1", name: "ana", display_name: "Ana", avatar: null, role: null };
30// An old message's author: an agent that no longer exists.
31const bot = { kind: "agent" as const, id: "a1", name: "reviewer", display_name: "Reviewer", avatar: null, role: "Reviews pull requests" };
32
33function message(id: string, at: string, over: Partial<ChatMessage> = {}): ShownMessage {
34 return {
35 id,
36 channel_id: "c1",
37 author: ana,
38 kind: "text",
39 body: "hi",
40 card: null,
41 thread_root: null,
42 reply_count: 0,
43 last_reply_at: null,
44 created_at: at,
45 edited_at: null,
46 deleted_at: null,
47 ...over,
48 };
49}
50
51test("messages from one author close together share a header; days get a rule", () => {
52 const now = new Date("2026-10-08T15:00:00Z");
53 const rows = timeline(
54 [
55 message("01", "2026-10-07T10:00:00Z"),
56 message("02", "2026-10-08T10:00:00Z"),
57 message("03", "2026-10-08T10:02:00Z"),
58 message("04", "2026-10-08T10:03:00Z", { author: bot }),
59 message("05", "2026-10-08T10:30:00Z", { author: bot }),
60 message("06", "2026-10-08T10:31:00Z", { author: bot, kind: "card" }),
61 message("07", "2026-10-08T10:32:00Z", { deleted_at: "2026-10-08T10:33:00Z" }),
62 ],
63 now,
64 "UTC",
65 );
66 assert.deepEqual(
67 rows.map((row) => (row.kind === "day" ? row.label : `${row.message.id}${row.head ? "*" : ""}`)),
68 ["Yesterday", "01*", "Today", "02*", "03", "04*", "05*", "06*"],
69 );
70 assert.equal(dayLabel("2026-09-01T10:00:00Z", now, "UTC"), "Tuesday, September 1");
71});
72
73test("merging keeps one copy of each message, and drops the pending one once saved", () => {
74 const pending = { ...message("tmp-1", "2026-10-08T10:00:00Z"), pending: true, client_id: "tmp-1" };
75 const saved = { ...message("05", "2026-10-08T10:00:01Z"), client_id: "tmp-1" };
76 const merged = mergeMessages([message("02", "2026-10-08T09:00:00Z"), pending], [saved, message("02", "2026-10-08T09:00:00Z", { body: "edited" })]);
77 assert.deepEqual(
78 merged.map((m) => m.id),
79 ["02", "05"],
80 );
81 assert.equal(merged[0]!.body, "edited");
82 // Still pending: last, after everything saved.
83 assert.deepEqual(
84 mergeMessages([pending], [message("09", "2026-10-08T11:00:00Z")]).map((m) => m.id),
85 ["09", "tmp-1"],
86 );
87});
88
89test("an @ being typed suggests people", () => {
90 const people = [
91 { kind: "user" as const, name: "ana", display_name: "Ana Lima", avatar: null },
92 { kind: "user" as const, name: "rene", display_name: "Rene", avatar: null },
93 { kind: "user" as const, name: "bo", display_name: "Robert Reed", avatar: null },
94 ];
95 assert.deepEqual(
96 mentionQuery("hey @re", 7, people)?.options.map((p) => p.name),
97 ["rene", "bo"],
98 );
99 assert.equal(mentionQuery("hey @re", 7, people)?.start, 4);
100 assert.equal(mentionQuery("mail me@an", 10, people), null);
101 assert.deepEqual(mentionQuery("@", 1, people)?.options.length, 3);
102 assert.equal(mentionQuery("@zz", 3, people), null);
103});
104
105test("message text: mentions, references, links and code, never HTML", () => {
106 assert.deepEqual(inline("ask @reviewer about #12 and g1t#3"), [
107 { t: "text", v: "ask " },
108 { t: "mention", name: "reviewer" },
109 { t: "text", v: " about " },
110 { t: "ref", repo: null, number: 12 },
111 { t: "text", v: " and " },
112 { t: "ref", repo: "g1t", number: 3 },
113 ]);
114 assert.deepEqual(inline("see #general, `a <b>` **bold**"), [
115 { t: "text", v: "see " },
116 { t: "channel", name: "general" },
117 { t: "text", v: ", " },
118 { t: "code", v: "a <b>" },
119 { t: "text", v: " " },
120 { t: "strong", c: [{ t: "text", v: "bold" }] },
121 ]);
122 assert.ok(inline("[x](javascript:alert(1))").every((span) => span.t === "text"));
123 assert.deepEqual(inline("https://g1t.sh/a."), [
124 { t: "link", href: "https://g1t.sh/a", c: [{ t: "text", v: "https://g1t.sh/a" }] },
125 { t: "text", v: "." },
126 ]);
127 assert.deepEqual(inline("me@example.com"), [{ t: "text", v: "me@example.com" }]);
128 assert.equal(safeHref("//evil.example"), null);
129 assert.equal(safeHref("/acme/web/pull/3"), "/acme/web/pull/3");
130});
131
132test("message blocks: fenced code, lists, quotes and paragraphs", () => {
133 const parsed = blocks("Plan:\n- one\n- two\n\n```ts\nconst a = 1;\n```\n> quoted\nlast");
134 assert.deepEqual(
135 parsed.map((block) => block.t),
136 ["p", "list", "code", "quote", "p"],
137 );
138 assert.deepEqual(parsed[2], { t: "code", lang: "ts", v: "const a = 1;" });
139});
140
141function entry(
142 id: string,
143 over: Partial<ChatSidebarEntry> & { kind?: "channel" | "dm"; last?: string; visibility?: "private" | "workspace" | "public" } = {},
144): ChatSidebarEntry {
145 return {
146 channel: {
147 id,
148 workspace_id: "w",
149 kind: over.kind ?? "channel",
150 name: over.kind === "dm" ? null : id,
151 topic: null,
152 private: over.visibility === "private",
153 visibility: over.visibility ?? (over.kind === "dm" ? "private" : "workspace"),
154 created_by: { kind: "user", id: "u1" },
155 created_at: "2026-10-01T00:00:00Z",
156 archived_at: null,
157 last_message_at: over.last ?? null,
158 },
159 title: over.title ?? id,
160 others: over.others ?? [],
161 starred: over.starred ?? false,
162 muted: over.muted ?? false,
163 unread: over.unread ?? 0,
164 mentions: over.mentions ?? 0,
165 };
166}
167
168test("the sidebar: sections, filters and the rail's count", () => {
169 const all = [
170 entry("random"),
171 entry("general", { starred: true, unread: 2 }),
172 entry("d1", { kind: "dm", title: "reviewer", others: [bot], last: "2026-10-02T00:00:00Z", mentions: 1, unread: 1 }),
173 entry("d2", { kind: "dm", title: "ana", others: [ana], last: "2026-10-05T00:00:00Z" }),
174 entry("noisy", { muted: true, unread: 9 }),
175 entry("d3", { kind: "dm", title: "ana, reviewer", others: [ana, bot], last: "2026-10-03T00:00:00Z" }),
176 entry("d4", { kind: "dm", title: "notes", others: [], last: "2026-10-01T00:00:00Z" }),
177 ];
178 const { pinned, channels, publicChannels, dms } = sections(all);
179 assert.deepEqual(
180 pinned.map((e) => e.title),
181 ["general"],
182 );
183 assert.deepEqual(
184 channels.map((e) => e.title),
185 ["noisy", "random"],
186 );
187 assert.deepEqual(publicChannels, []);
188 assert.deepEqual(
189 dms.map((e) => e.title),
190 ["ana", "ana, reviewer", "reviewer", "notes"],
191 );
192 // An old conversation with an agent alone is left out until searched for or open.
193 assert.deepEqual(
194 filterEntries(all, "unread", "").map((e) => e.title),
195 ["general", "noisy"],
196 );
197 assert.deepEqual(
198 filterEntries(all, "unread", "", "d1").map((e) => e.title),
199 ["general", "reviewer", "noisy"],
200 );
201 assert.deepEqual(
202 filterEntries(all, "all", "@Review").map((e) => e.title),
203 ["reviewer", "ana, reviewer"],
204 );
205 assert.deepEqual(unreadTotals(all), { unread: 3, mentions: 1 });
206});
207
208test("the sidebar keeps public channels apart from the workspace's own", () => {
209 const all = [
210 entry("general"),
211 entry("leads", { visibility: "private", unread: 1 }),
212 entry("support", { visibility: "public", mentions: 2, unread: 2 }),
213 entry("announcements", { visibility: "public" }),
214 entry("launch", { visibility: "public", starred: true }),
215 entry("d1", { kind: "dm", title: "ana", others: [ana] }),
216 ];
217 const { pinned, channels, publicChannels, dms } = sections(all);
218 // Pinned keeps a public channel you pinned; the rest are by name in their own group.
219 assert.deepEqual(
220 pinned.map((e) => e.title),
221 ["launch"],
222 );
223 assert.deepEqual(
224 channels.map((e) => e.title),
225 ["general", "leads"],
226 );
227 assert.deepEqual(
228 publicChannels.map((e) => e.title),
229 ["announcements", "support"],
230 );
231 assert.deepEqual(
232 dms.map((e) => e.title),
233 ["ana"],
234 );
235 // Filters reach both groups.
236 assert.deepEqual(
237 sections(filterEntries(all, "unread", "")).publicChannels.map((e) => e.title),
238 ["support"],
239 );
240 assert.deepEqual(sections(filterEntries(all, "mentions", "")).channels, []);
241});
242
243test("an old conversation with an agent alone opens read-only, under the name the agent had", () => {
244 const deleted = { ...bot, id: "a9", name: "triage", display_name: "Triage", deleted: true };
245 const all = [
246 entry("d1", { kind: "dm", title: "triage", others: [deleted], last: "2026-10-01T00:00:00Z" }),
247 // With a person in it, the conversation is theirs to go on with.
248 entry("d2", { kind: "dm", title: "ana, triage", others: [ana, deleted], last: "2026-10-03T00:00:00Z" }),
249 entry("d3", { kind: "dm", title: "ana", others: [ana], last: "2026-10-04T00:00:00Z" }),
250 ];
251 assert.deepEqual(
252 all.map((e) => formerAgentDm(e)?.name ?? null),
253 ["triage", null, null],
254 );
255 assert.equal(formerAgentLine(deleted), "Triage was an agent and no longer answers here. Everything said here stays to read.");
256});
257
258test("addresses, names, money and reconnecting", () => {
259 assert.equal(channelPath("acme", { id: "c1", kind: "channel", name: "general" }), "/acme/-/chat/general");
260 assert.equal(channelPath("acme", { id: "d1", kind: "dm", name: null }), "/acme/-/chat/dm/d1");
261 assert.equal(channelName("#Release Train_2!"), "release-train-2");
262 assert.equal(
263 backoff(0, () => 0.5),
264 1000,
265 );
266 assert.equal(
267 backoff(10, () => 0.5),
268 30_000,
269 );
270});
271
272test("a member shows by display name, else by their username as they wrote it", () => {
273 assert.equal(shownName({ name: "ana", display_name: "Ana Lima", display_username: "Ana" }), "Ana Lima");
274 assert.equal(shownName({ name: "ana", display_name: " ", display_username: "Ana" }), "Ana");
275 assert.equal(shownName({ name: "ana", display_name: "" }), "ana");
276 assert.equal(shownHandle({ name: "ana", display_username: "Ana" }), "Ana");
277 assert.equal(shownHandle({ name: "ana", display_username: "Bo" }), "ana");
278});
279
280test("mentions read as the names people know, found by the lowercased handle", () => {
281 const names = mentionNames([
282 { kind: "user", name: "ana", display_username: "Ana", display_name: "Ana Lima", avatar: null },
283 { kind: "user", name: "bo", display_username: "Bo", display_name: "", avatar: null },
284 { kind: "user", name: "rene", display_name: "Rene", avatar: null },
285 ]);
286 assert.equal(names.get("ana"), "Ana Lima");
287 assert.equal(names.get("bo"), "Bo");
288 assert.equal(names.get("rene"), "Rene");
289 assert.equal(names.get("cy"), undefined);
290});
291
292test("the typing line: who is typing", () => {
293 assert.equal(typingLabel([]), null);
294 assert.deepEqual(typingLabel([{ member: ana }]), { who: "Ana", doing: "is typing" });
295 const bo = { ...ana, id: "u2", name: "bo", display_name: "Bo" };
296 assert.deepEqual(typingLabel([{ member: ana }, { member: bo }]), { who: "Ana and Bo", doing: "are typing" });
297 const third = { ...ana, id: "u3", name: "cy", display_name: "Cy" };
298 assert.deepEqual(typingLabel([{ member: ana }, { member: bo }, { member: third }]), { who: "Several people", doing: "are typing" });
299});