| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { AgentDelivery } from "@g1t/contracts"; |
| 5 | |
| 6 | import { g1tSurface } from "./surface.ts"; |
| 7 | |
| 8 | const 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]`. */ |
| 25 | function 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 | |
| 37 | test("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 | |
| 56 | test("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 | |
| 78 | test("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 | |
| 85 | test("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 | |
| 92 | test("👀 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 | |
| 105 | test("👀 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 | |
| 113 | test("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 | |
| 126 | const profile = (kind: "user" | "agent", id: string, name: string, display_name: string, title: string | null = null) => ({ kind, id, name, display_name, avatar: null, role: kind === "agent" ? "Does things" : null, title }); |
| 127 | const channel = (kind: "channel" | "dm", isPrivate: boolean, name: string | null) => ({ id: "chn_1", workspace_id: "wsp_1", kind, name, topic: null, private: isPrivate, created_by: { kind: "user", id: "usr_dana" }, created_at: "", archived_at: null, last_message_at: null }); |
| 128 | |
| 129 | test("the conversation: what kind it is and who is in it, asked as the agent with who asked", async () => { |
| 130 | const dm = fakeChat({ |
| 131 | conversation_for_agent: { ok: true, value: { channel: channel("dm", true, null), members: [profile("agent", "agt_ship", "ship", "Ship"), profile("user", "usr_dana", "dana", "Dana")], people: 1, agents: 1 } }, |
| 132 | }); |
| 133 | const one = await g1tSurface(dm.binding, delivery).conversation(); |
| 134 | assert.deepEqual(dm.calls[0].body, { workspace: "acme", channel_id: "chn_1", agent_id: "agt_ship", asked_by: "usr_dana" }); |
| 135 | assert.equal(one?.kind, "dm"); |
| 136 | assert.deepEqual(one?.members.map((m) => [m.kind, m.name, m.title]), [["agent", "ship", "Does things"], ["user", "dana", null]]); |
| 137 | const group = fakeChat({ conversation_for_agent: { ok: true, value: { channel: channel("dm", true, null), members: [], people: 1, agents: 2 } } }); |
| 138 | assert.equal((await g1tSurface(group.binding, delivery).conversation())?.kind, "group_dm"); |
| 139 | const open = fakeChat({ conversation_for_agent: { ok: true, value: { channel: channel("channel", false, "releases"), members: [], people: 300, agents: 1 } } }); |
| 140 | assert.deepEqual(await g1tSurface(open.binding, delivery).conversation(), { kind: "public_channel", name: "releases", members: [], people: 300, agents: 1 }); |
| 141 | const broken = { fetch: async () => new Response("down", { status: 500 }) } as any; |
| 142 | assert.equal(await g1tSurface(broken, delivery).conversation(), null, "never throws"); |
| 143 | }); |
| 144 | |
| 145 | test("a hand-off goes to chat with the delivery's chain, so the colleague works for the same person", async () => { |
| 146 | const chat = fakeChat({ hand_off_as_agent: { ok: true, value: { where: "group_dm", channel_id: "chn_9", message_id: "msg_9", opened: true } } }); |
| 147 | const done = await g1tSurface(chat.binding, delivery).handOff("agt_mike", "@mike draft the role brief."); |
| 148 | assert.deepEqual(done, { ok: true, where: "group_dm", opened: true }); |
| 149 | assert.equal(chat.calls[0].method, "hand_off_as_agent"); |
| 150 | assert.deepEqual(chat.calls[0].body, { |
| 151 | workspace: "acme", |
| 152 | channel_id: "chn_1", |
| 153 | agent_id: "agt_ship", |
| 154 | hand_off: { |
| 155 | colleague_id: "agt_mike", |
| 156 | brief: "@mike draft the role brief.", |
| 157 | thread_root: "msg_0", |
| 158 | hops: 2, |
| 159 | asked_by: "usr_dana", |
| 160 | asker: { username: "dana", role: "member", can_write: false }, |
| 161 | chain: ["agt_g1t"], |
| 162 | }, |
| 163 | }); |
| 164 | const refused = fakeChat({ hand_off_as_agent: { ok: false, error: { code: "invalid", message: "too many times" } } }); |
| 165 | assert.deepEqual(await g1tSurface(refused.binding, delivery).handOff("agt_mike", "x"), { ok: false, message: "too many times" }); |
| 166 | }); |
| 167 | |
| 168 | test("reacting that fails never throws, and nothing is taken back that never went on", async () => { |
| 169 | const broken = { fetch: async () => new Response("down", { status: 500 }) } as any; |
| 170 | const surface = g1tSurface(broken, delivery); |
| 171 | await surface.acknowledge(); |
| 172 | await surface.settle("done"); |
| 173 | const refused = fakeChat({ react_as_agent: { ok: false, error: { code: "forbidden", message: "not a member" } } }); |
| 174 | const s2 = g1tSurface(refused.binding, delivery); |
| 175 | await s2.acknowledge(); |
| 176 | await s2.settle("done"); |
| 177 | assert.equal(refused.calls.length, 1, "no ✅ or removal after a refused 👀"); |
| 178 | }); |