Skip to content
136 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { AgentDelivery } from "@g1t/contracts";
5
6import { g1tSurface } from "./surface.ts";
7
8const delivery: AgentDelivery = {
9 workspace: "acme",
10 workspace_id: "wsp_1",
11 channel_id: "chn_1",
12 channel_kind: "channel",
13 channel_name: "releases",
14 agent_id: "agt_ship",
15 message_id: "msg_1",
16 thread_root: "msg_0",
17 asked_by: "usr_dana",
18 hops: 2,
19 asker: { username: "dana", role: "member", can_write: false },
20 chain: ["agt_g1t"],
21 surface: "g1t",
22};
23
24/** A chat binding that records each call and answers `answers[method]`. */
25function fakeChat(answers: Record<string, unknown>) {
26 const calls: { method: string; body: any }[] = [];
27 const binding = {
28 async fetch(url: string, init: { body: string }) {
29 const method = new URL(url).pathname.replace("/rpc/", "");
30 calls.push({ method, body: JSON.parse(init.body) });
31 return Response.json(answers[method] ?? { ok: true, value: null });
32 },
33 };
34 return { binding: binding as any, calls };
35}
36
37test("the g1t adapter posts in the thread, handing on hops, asked_by and the asker's access", async () => {
38 const chat = fakeChat({ post_as_agent: { ok: true, value: { id: "msg_2" } } });
39 const id = await g1tSurface(chat.binding, delivery).post("Tagged 1.4.");
40 assert.equal(id, "msg_2");
41 const [call] = chat.calls;
42 assert.equal(call.method, "post_as_agent");
43 assert.equal(call.body.agent_id, "agt_ship");
44 assert.equal(call.body.channel_id, "chn_1");
45 assert.deepEqual(call.body.message, {
46 body: "Tagged 1.4.",
47 card: null,
48 thread_root: "msg_0",
49 hops: 2,
50 asked_by: "usr_dana",
51 asker: { username: "dana", role: "member", can_write: false },
52 chain: ["agt_g1t"],
53 });
54});
55
56test("the g1t adapter reads the thread as the agent, and leaves out deleted messages", async () => {
57 const message = (id: string, deleted: string | null) => ({
58 id,
59 channel_id: "chn_1",
60 author: { kind: "user", id: "usr_dana", name: "dana", display_name: "Dana", avatar: null, role: null },
61 kind: "text",
62 body: "hi",
63 card: null,
64 thread_root: "msg_0",
65 reply_count: 0,
66 last_reply_at: null,
67 created_at: "2026-10-08T12:00:00Z",
68 edited_at: null,
69 deleted_at: deleted,
70 });
71 const chat = fakeChat({ history_for_agent: { ok: true, value: [message("msg_0", null), message("msg_1", "2026-10-08T12:01:00Z")] } });
72 const history = await g1tSurface(chat.binding, delivery).history(30);
73 assert.deepEqual(chat.calls[0].body, { workspace: "acme", channel_id: "chn_1", agent_id: "agt_ship", thread_root: "msg_0", limit: 30 });
74 assert.deepEqual(history.map((m) => m.id), ["msg_0"]);
75 assert.equal(history[0].author.name, "dana");
76});
77
78test("a failed post is an error the reply loop records; typing never throws", async () => {
79 const chat = fakeChat({ post_as_agent: { ok: false, error: { code: "forbidden", message: "not a member" } } });
80 await assert.rejects(g1tSurface(chat.binding, delivery).post("x"), /not a member/);
81 const broken = { fetch: async () => new Response("down", { status: 500 }) } as any;
82 await g1tSurface(broken, delivery).typing();
83});
84
85test("reactions: in channels and group DMs, never a one-person DM", async () => {
86 const { reactsIn } = await import("./surface.ts");
87 assert.equal(reactsIn("channel", 1), true);
88 assert.equal(reactsIn("dm", 2), true);
89 assert.equal(reactsIn("dm", 1), false);
90});
91
92test("👀 when the desk picks it up, then ✅ when it answered", async () => {
93 const chat = fakeChat({ react_as_agent: { ok: true, value: [] } });
94 const surface = g1tSurface(chat.binding, delivery);
95 await surface.acknowledge();
96 await surface.settle("done");
97 const reacts = chat.calls.filter((c) => c.method === "react_as_agent").map((c) => [c.body.emoji, c.body.remove, c.body.message_id]);
98 assert.deepEqual(reacts, [
99 ["👀", false, "msg_1"],
100 ["👀", true, "msg_1"],
101 ["✅", false, "msg_1"],
102 ]);
103});
104
105test("👀 is taken back after a notice or an apology", async () => {
106 const chat = fakeChat({ react_as_agent: { ok: true, value: [] } });
107 const surface = g1tSurface(chat.binding, delivery);
108 await surface.acknowledge();
109 await surface.settle("withdrawn");
110 assert.deepEqual(chat.calls.map((c) => [c.body.emoji, c.body.remove]), [["👀", false], ["👀", true]]);
111});
112
113test("a one-person DM gets no reactions; a group DM does", async () => {
114 const dm = { ...delivery, channel_kind: "dm" as const, channel_name: null };
115 const alone = fakeChat({ audience: { ok: true, value: { kind: "dm", member_user_ids: ["usr_dana"], member_count: 1 } } });
116 const quiet = g1tSurface(alone.binding, dm);
117 await quiet.acknowledge();
118 await quiet.settle("done");
119 assert.deepEqual(alone.calls.map((c) => c.method), ["audience"]);
120 const group = fakeChat({ audience: { ok: true, value: { kind: "dm", member_user_ids: ["usr_dana", "usr_bo"], member_count: 2 } }, react_as_agent: { ok: true, value: [] } });
121 const loud = g1tSurface(group.binding, dm);
122 await loud.acknowledge();
123 assert.deepEqual(group.calls.map((c) => c.method), ["audience", "react_as_agent"]);
124});
125
126test("reacting that fails never throws, and nothing is taken back that never went on", async () => {
127 const broken = { fetch: async () => new Response("down", { status: 500 }) } as any;
128 const surface = g1tSurface(broken, delivery);
129 await surface.acknowledge();
130 await surface.settle("done");
131 const refused = fakeChat({ react_as_agent: { ok: false, error: { code: "forbidden", message: "not a member" } } });
132 const s2 = g1tSurface(refused.binding, delivery);
133 await s2.acknowledge();
134 await s2.settle("done");
135 assert.equal(refused.calls.length, 1, "no ✅ or removal after a refused 👀");
136});