| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { type Colleague, type HandOffPorts, addressed, handOffPort } from "./handoff.ts"; |
| 5 | import type { HandedOff } from "./surface.ts"; |
| 6 | |
| 7 | const mike: Colleague = { id: "agt_mike", handle: "mike", display_name: "Mike", builtin: false, status: "idle" }; |
| 8 | const g1t: Colleague = { id: "agt_g1t", handle: "g1t", display_name: "g1t", builtin: true, status: "idle" }; |
| 9 | const margo: Colleague = { id: "agt_margo", handle: "margo", display_name: "Margo", builtin: false, status: "paused" }; |
| 10 | const sam: Colleague = { id: "agt_sam", handle: "sam", display_name: "Sam", builtin: false, status: "out_of_budget" }; |
| 11 | |
| 12 | /** A small workspace: these agents, and the person syntaqx. */ |
| 13 | function world(over: Partial<HandOffPorts> = {}, answer: HandedOff = { ok: true, where: "group_dm", opened: true }) { |
| 14 | const handed: { colleague: string; brief: string }[] = []; |
| 15 | const agents = [mike, g1t, margo, sam]; |
| 16 | const ports: HandOffPorts = { |
| 17 | self: { id: "agt_g1t", handle: "g1t" }, |
| 18 | chain: [], |
| 19 | asker: { username: "syntaqx", role: "owner", can_write: true }, |
| 20 | agent: async (handle) => agents.find((a) => a.handle === handle) ?? null, |
| 21 | person: async (handle) => handle === "syntaqx", |
| 22 | handOff: async (colleague, brief) => { |
| 23 | handed.push({ colleague, brief }); |
| 24 | return answer; |
| 25 | }, |
| 26 | ...over, |
| 27 | }; |
| 28 | return { port: handOffPort(ports), handed }; |
| 29 | } |
| 30 | |
| 31 | test("g1t hands Mike the hiring work: he is woken with the brief addressed to him, and g1t is told where it went", async () => { |
| 32 | const { port, handed } = world(); |
| 33 | const done = await port("mike", "Help Chase hire an engineering manager: draft the role brief."); |
| 34 | assert.equal(done.ok, true); |
| 35 | assert.deepEqual(handed, [{ colleague: "agt_mike", brief: "@mike Help Chase hire an engineering manager: draft the role brief." }]); |
| 36 | assert.match(done.message, /Handed to @mike in a new group message with @syntaqx, you and them/); |
| 37 | assert.match(done.message, /a card here links to it/); |
| 38 | }); |
| 39 | |
| 40 | test("handed here, or into the group message they already share", async () => { |
| 41 | const here = await world({}, { ok: true, where: "here", opened: false }).port("mike", "x"); |
| 42 | assert.match(here.message, /your brief is posted in this conversation/); |
| 43 | const again = await world({}, { ok: true, where: "group_dm", opened: false }).port("mike", "x"); |
| 44 | assert.match(again.message, /the group message you three already have/); |
| 45 | }); |
| 46 | |
| 47 | test("refusals: unknown, a person, itself, @g1t, back along the chain, unavailable, an outside asker", async () => { |
| 48 | const refused = async (handle: string, over: Partial<HandOffPorts> = {}) => { |
| 49 | const { port, handed } = world(over); |
| 50 | const done = await port(handle, "Do it."); |
| 51 | assert.equal(done.ok, false, handle); |
| 52 | assert.equal(handed.length, 0, `nothing handed to ${handle}`); |
| 53 | return done.message; |
| 54 | }; |
| 55 | assert.match(await refused("nobody"), /no agent called @nobody/); |
| 56 | assert.match(await refused("syntaqx"), /@syntaqx is a person, not an agent/); |
| 57 | assert.match(await refused("mike", { self: { id: "agt_mike", handle: "mike" } }), /That's you/); |
| 58 | assert.match(await refused("g1t", { self: { id: "agt_mike", handle: "mike" } }), /@g1t can't be handed work by an agent/); |
| 59 | assert.match(await refused("mike", { self: { id: "agt_sam", handle: "sam" }, chain: ["agt_g1t", "agt_mike"] }), /already handled this request/); |
| 60 | assert.match(await refused("margo"), /is paused/); |
| 61 | assert.match(await refused("sam"), /out of budget/); |
| 62 | assert.match(await refused("mike", { asker: { username: "eve", role: "outside", can_write: false } }), /isn't a member of this workspace/); |
| 63 | }); |
| 64 | |
| 65 | test("chat's own refusal comes back to the agent as the tool's answer", async () => { |
| 66 | const { port } = world({}, { ok: false, message: "This request has been passed along too many times." }); |
| 67 | const done = await port("mike", "x"); |
| 68 | assert.equal(done.ok, false); |
| 69 | assert.match(done.message, /couldn't be handed over: This request has been passed along too many times/); |
| 70 | }); |
| 71 | |
| 72 | test("a brief that already names the colleague isn't addressed twice", () => { |
| 73 | assert.equal(addressed("mike", "@Mike, please draft it."), "@Mike, please draft it."); |
| 74 | assert.equal(addressed("mike", "Please draft it, cc @mikey."), "@mike Please draft it, cc @mikey."); |
| 75 | }); |