| 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 | test("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 | }); |