// Approvals: the ledger, reply and button resolution, and the connector // flow end to end with a fake rest, a fake engine whose turn opened a // request, and a fake record service client. No network, no model. import { test } from "node:test"; import assert from "node:assert/strict"; import { join } from "node:path"; import { APPROVAL_LINES, CALLBACK_TYPE, EPHEMERAL, INTERACTION_TYPE, COMPONENT_TYPE, validateRequest, renderRequest, customId, approveComponents, appendApproval, readApprovals, foldApprovals, loadOpenRequests, resolveReply, resolveInteraction, } from "../src/approvals.mjs"; import { SETSPARK_REFUSAL, SetsparkRefusal } from "../src/setspark.mjs"; import { createConnector } from "../src/connector.mjs"; import { readDrops, readTurn, ensureJournal } from "../src/journal.mjs"; import { makeRoot, binding, message, IDS, fakeRest, fakeGateway, fakeEngine } from "./helpers.mjs"; const CARMEN = "100000000000000102"; const DIGEST = "0123456789abcdef0123456789abcdef"; const M2 = "500000000000000002"; const REQ = { requestId: "APR-7", decisionId: "DEC-012", proposalVersion: 2, digest: DIGEST, approvers: [IDS.owner, CARMEN] }; function fakeApi({ bind = [], add = [] } = {}) { const calls = []; const next = (q) => { const o = q.length > 0 ? q.shift() : { ok: true }; if (o.ok) return { ok: true }; throw o.error || new SetsparkRefusal(o.reason || SETSPARK_REFUSAL.REJECTED, { code: o.code || "fake" }); }; return { calls, async bindApprovalMessage(args) { calls.push({ op: "bind", ...args }); return next(bind); }, async addApproval(args) { calls.push({ op: "add", ...args }); return next(add); }, }; } function withInteractions(rest) { rest.callbacks = []; rest.edits = []; rest.callbackOk = true; rest.interactionCallback = async (id, token, body) => { rest.callbacks.push({ id, token, body }); return rest.callbackOk; }; rest.editInteractionMessage = async (appId, token, body) => { rest.edits.push({ appId, token, body }); return { status: 200 }; }; return rest; } function twoUsers() { return binding({ users: [{ id: IDS.owner, name: "owner" }, { id: CARMEN, name: "carmen" }] }); } function interaction({ id = "300000000000000001", messageId, requestId = REQ.requestId, userId = IDS.owner, custom = null } = {}) { return { id, token: `tok-${id}`, application_id: IDS.bot, type: INTERACTION_TYPE.MESSAGE_COMPONENT, channel_id: IDS.admin, guild_id: IDS.guild, data: { component_type: COMPONENT_TYPE.BUTTON, custom_id: custom ?? customId(requestId) }, message: { id: messageId, channel_id: IDS.admin }, member: { user: { id: userId } }, }; } test("approvals: a request is validated before anything is posted; the rendering shows names and never ids", () => { const r = validateRequest(REQ); assert.deepEqual(r, REQ); assert.throws(() => validateRequest(null), /not an object/); assert.throws(() => validateRequest({ ...REQ, requestId: "bad id" }), /request id/); assert.throws(() => validateRequest({ ...REQ, requestId: "x".repeat(65) }), /request id/, "the id regex keeps the button custom id under Discord's limit"); assert.throws(() => validateRequest({ ...REQ, proposalVersion: 0 }), /version/); assert.throws(() => validateRequest({ ...REQ, digest: "zz" }), /digest/); assert.throws(() => validateRequest({ ...REQ, approvers: [] }), /approvers/); assert.throws(() => validateRequest({ ...REQ, approvers: ["nope"] }), /approver id/); assert.throws(() => validateRequest({ ...REQ, approvers: [IDS.owner, IDS.owner] }), /duplicate/); const text = renderRequest(r, ["owner", "carmen"]); assert.match(text, /DEC-012, proposal version 2/); assert.match(text, /owner, carmen may approve/); assert.match(text, /single word approve/); assert.ok(!text.includes(IDS.owner) && !text.includes(CARMEN)); const comps = approveComponents("APR-7"); assert.equal(comps[0].components[0].custom_id, "approve:APR-7"); assert.equal(comps[0].components[0].disabled, false); assert.equal(approveComponents("APR-7", { disabled: true })[0].components[0].disabled, true); }); test("approvals: the ledger is appended and folded into open requests with bind and approval states", () => { const dir = join(makeRoot(), "j"); ensureJournal(dir); assert.deepEqual(loadOpenRequests(dir).size, 0); appendApproval(dir, { kind: "opened", at: "t0", ...REQ, messageId: "m1", channelId: IDS.admin, content: "c" }); appendApproval(dir, { kind: "bind", at: "t1", requestId: "APR-7", messageId: "m1", channelId: IDS.admin, status: "intent" }); appendApproval(dir, { kind: "bind", at: "t2", requestId: "APR-7", messageId: "m1", channelId: IDS.admin, status: "done" }); appendApproval(dir, { kind: "approval", at: "t3", requestId: "APR-7", authorId: IDS.owner, eventId: "e1", messageId: "m1", how: "button", status: "intent" }); appendApproval(dir, { kind: "approval", at: "t4", requestId: "APR-7", authorId: IDS.owner, eventId: "e1", messageId: "m1", how: "button", status: "unknown", error: "x" }); appendApproval(dir, { kind: "approval", at: "t5", requestId: "OTHER", authorId: IDS.owner, eventId: "e9", messageId: "m9", how: "reply", status: "done" }); assert.equal(readApprovals(dir).length, 6); const open = foldApprovals(readApprovals(dir)); assert.equal(open.size, 1); const rec = open.get("m1"); assert.deepEqual(rec.request, REQ); assert.equal(rec.bind.status, "done"); assert.equal(rec.approvals.get(IDS.owner).status, "unknown"); assert.throws(() => appendApproval(dir, { at: "t" }), /kind/); }); test("approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once", () => { const open = foldApprovals([{ kind: "opened", at: "t0", ...REQ, messageId: "m1", channelId: IDS.admin, content: "c" }]); const reply = (over = {}) => message({ id: "200000000000000050", content: "approve", message_reference: { message_id: "m1" }, ...over }); assert.equal(resolveReply(reply(), open).ok, true); assert.equal(resolveReply(reply({ content: " approve\n" }), open).ok, true, "surrounding whitespace is trimmed"); assert.equal(resolveReply(message({ content: "approve" }), open).reason, "not-a-request"); assert.equal(resolveReply(reply({ message_reference: { message_id: M2 } }), open).reason, "not-a-request"); assert.equal(resolveReply(reply({ content: "Approve" }), open).reason, "not-approve"); assert.equal(resolveReply(reply({ content: "approve it" }), open).reason, "not-approve"); assert.equal(resolveReply(reply({ author: { id: IDS.stranger } }), open).reason, "not-approver"); open.get("m1").approvals.set(IDS.owner, { status: "done" }); assert.equal(resolveReply(reply(), open).reason, "already"); open.get("m1").approvals.set(IDS.owner, { status: "unknown" }); assert.equal(resolveReply(reply(), open).reason, "pending"); open.get("m1").approvals.set(IDS.owner, { status: "refused" }); assert.equal(resolveReply(reply(), open).ok, true, "a refused attempt may be retried"); }); test("approvals: a button approves only on its own request message with the matching custom id", () => { const open = foldApprovals([{ kind: "opened", at: "t0", ...REQ, messageId: "m1", channelId: IDS.admin, content: "c" }]); assert.equal(resolveInteraction(interaction({ messageId: "m1" }), open).ok, true); assert.equal(resolveInteraction(interaction({ messageId: M2 }), open).reason, "not-a-request"); assert.equal(resolveInteraction(interaction({ messageId: "m1", custom: "approve:APR-8" }), open).reason, "not-a-request"); assert.equal(resolveInteraction(interaction({ messageId: "m1", userId: IDS.stranger }), open).reason, "not-approver"); assert.equal(resolveInteraction({ ...interaction({ messageId: "m1" }), type: 2 }, open).reason, "not-a-request"); assert.equal(resolveInteraction(null, open).reason, "not-a-request"); const dm = { ...interaction({ messageId: "m1" }), member: undefined, user: { id: CARMEN } }; assert.equal(resolveInteraction(dm, open).ok, true, "a user field outside a guild member is read too"); }); test("approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve", async () => { const journalDir = join(makeRoot(), "j"); const rest = withInteractions(fakeRest({ snowflakes: true })); const api = fakeApi(); const engine = fakeEngine({ replies: [{ text: "Here is the proposal.", tools: [{ name: "open_approval_request", ok: true, request: REQ, ms: 1 }] }] }); const c = createConnector({ binding: twoUsers(), journalDir, rest, gateway: fakeGateway(), engine, api }); const r = await c.handleMessage(message({ id: "200000000000000060", content: "propose it" })); assert.equal(r.accepted, true); await r.turn; assert.equal(rest.calls.length, 2, "the reply, then the request message"); const posted = rest.calls[1]; assert.match(posted.content, /Approval requested for DEC-012/); assert.match(posted.content, /owner, carmen may approve/); assert.equal(posted.components[0].components[0].custom_id, "approve:APR-7"); assert.equal(posted.nonce, "200000000000000060-a"); assert.equal(posted.replyTo, "200000000000000060"); assert.equal(c.approvals.size, 1); const rec = [...c.approvals.values()][0]; assert.equal(rec.messageId, M2); assert.equal(rec.bind.status, "done"); assert.deepEqual(api.calls, [{ op: "bind", requestId: "APR-7", messageId: M2, channelId: IDS.admin, idempotencyKey: `sage:${M2}:bind` }]); const turn = readTurn(journalDir, "200000000000000060"); assert.deepEqual(turn.approvalRequests, [{ requestId: "APR-7", status: "posted", messageId: M2, bind: "done" }]); const ledger = readApprovals(journalDir); assert.deepEqual(ledger.map((e) => `${e.kind}:${e.status || "-"}`), ["opened:-", "bind:intent", "bind:done"]); // the owner replies with the word; carmen presses the button const reply = await c.handleMessage(message({ id: "200000000000000061", content: "approve", message_reference: { message_id: M2 } })); assert.deepEqual(reply, { accepted: true, approval: "done" }); assert.equal(engine.prompts.length, 1, "an approval reply never reaches the model"); assert.equal(rest.calls[2].content, "Approved by owner."); assert.equal(api.calls[1].op, "add"); assert.equal(api.calls[1].authorId, IDS.owner); assert.equal(api.calls[1].messageId, "200000000000000061"); assert.equal(api.calls[1].boundMessageId, M2, "a reply approval also names the bound request message"); assert.equal(api.calls[1].kind, "reply"); assert.equal(api.calls[1].sourceUrl, `https://discord.com/channels/${IDS.guild}/${IDS.admin}/200000000000000061`, "the reply is its own evidence"); assert.equal(api.calls[1].statement, "approve"); assert.equal(api.calls[1].idempotencyKey, "sage:200000000000000061:approval"); const press = await c.handleInteraction(interaction({ id: "300000000000000002", messageId: M2, userId: CARMEN })); assert.deepEqual(press, { accepted: true, approval: "done", edited: true }); assert.equal(rest.callbacks[0].body.type, CALLBACK_TYPE.DEFERRED_UPDATE_MESSAGE); // the confirmation line is posted first and is the button press's evidence const confirmation = rest.calls[3]; const confirmationId = "500000000000000004"; // the fourth message the fake rest returned assert.equal(confirmation.content, "Approval: carmen approved DEC-012 v2 (digest 01234567) by button."); assert.equal(confirmation.replyTo, M2); assert.equal(confirmation.nonce, "300000000000000002-c"); assert.equal(api.calls[2].kind, "button"); assert.equal(api.calls[2].authorId, CARMEN); assert.equal(api.calls[2].messageId, M2); assert.equal(api.calls[2].boundMessageId, M2); assert.equal(api.calls[2].sourceUrl, `https://discord.com/channels/${IDS.guild}/${IDS.admin}/${confirmationId}`); assert.equal(api.calls[2].statement, confirmation.content); assert.equal(api.calls[2].idempotencyKey, "sage:300000000000000002:approval"); const done = readApprovals(journalDir).filter((e) => e.kind === "approval" && e.status === "done"); assert.equal(done[1].evidenceId, confirmationId, "the ledger keeps the evidence for a retry"); assert.equal(rest.edits.length, 1); assert.match(rest.edits[0].body.content, /Approved by owner, carmen\.$/); assert.equal(rest.edits[0].body.components[0].components[0].disabled, true, "the button is disabled once every approver has approved"); assert.equal(readApprovals(journalDir).filter((e) => e.kind === "approval" && e.status === "done").length, 2); }); test("approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry", async () => { const journalDir = join(makeRoot(), "j"); const rest = withInteractions(fakeRest({ snowflakes: true })); const api = fakeApi({ add: [{ ok: false, reason: SETSPARK_REFUSAL.REJECTED, code: "digest_mismatch" }, { ok: true }] }); const b = binding({ users: [{ id: IDS.owner, name: "owner" }, { id: CARMEN, name: "carmen" }, { id: IDS.stranger, name: "guest" }] }); const engine = fakeEngine({ replies: [{ text: "Proposal.", tools: [{ name: "open_approval_request", ok: true, request: REQ, ms: 1 }] }] }); const c = createConnector({ binding: b, journalDir, rest, gateway: fakeGateway(), engine, api }); await (await c.handleMessage(message({ id: "200000000000000070", content: "propose" }))).turn; const reqMsg = rest.calls[1]; assert.equal(reqMsg.components[0].components[0].custom_id, "approve:APR-7"); // a listed user who is not an approver replies approve let r = await c.handleMessage(message({ id: "200000000000000071", content: "approve", author: { id: IDS.stranger }, message_reference: { message_id: M2 } })); assert.equal(r.reason, "approval-not-approver"); assert.equal(rest.calls[2].content, APPROVAL_LINES.notApprover); // the same person presses the button r = await c.handleInteraction(interaction({ id: "300000000000000010", messageId: M2, userId: IDS.stranger })); assert.equal(r.reason, "approval-not-approver"); assert.equal(rest.callbacks[0].body.type, CALLBACK_TYPE.CHANNEL_MESSAGE); assert.equal(rest.callbacks[0].body.data.flags, EPHEMERAL); assert.equal(rest.callbacks[0].body.data.content, APPROVAL_LINES.notApprover); // a button with another request's id on this message r = await c.handleInteraction(interaction({ id: "300000000000000011", messageId: M2, custom: "approve:APR-99" })); assert.equal(r.reason, "approval-not-a-request"); // a duplicate interaction event r = await c.handleInteraction(interaction({ id: "300000000000000011", messageId: M2 })); assert.equal(r.reason, "duplicate"); // the service refuses the owner's approval (digest mismatch): fixed line, ledger refused, a retry may succeed r = await c.handleInteraction(interaction({ id: "300000000000000012", messageId: M2, userId: IDS.owner })); assert.deepEqual(r, { accepted: true, approval: "refused", edited: true }); assert.match(rest.edits[0].body.content, new RegExp(APPROVAL_LINES.failed.replace(/[.]/g, "\\."))); assert.equal(rest.edits[0].body.components[0].components[0].disabled, false); r = await c.handleMessage(message({ id: "200000000000000072", content: "approve", message_reference: { message_id: M2 } })); assert.deepEqual(r, { accepted: true, approval: "done" }); // now a repeat by the owner r = await c.handleMessage(message({ id: "200000000000000073", content: "approve", message_reference: { message_id: M2 } })); assert.equal(r.reason, "approval-already"); assert.equal(rest.calls.at(-1).content, APPROVAL_LINES.already); // a reply to the request message that is not the word goes to the model r = await c.handleMessage(message({ id: "200000000000000074", content: "what does this change?", message_reference: { message_id: M2 } })); assert.equal(r.accepted, true); await r.turn; assert.equal(engine.prompts.length, 2); const reasons = readDrops(journalDir).map((d) => d.reason); assert.deepEqual(reasons, ["approval-not-approver", "approval-not-approver", "approval-not-a-request", "approval-already"]); assert.equal(api.calls.filter((x) => x.op === "add").length, 2); }); test("approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing", async () => { const journalDir = join(makeRoot(), "j"); const rest = withInteractions(fakeRest({ snowflakes: true, outcomes: [{ ok: true }, { ok: true }, { ok: false, kind: "refused" }] })); const api = fakeApi(); const engine = fakeEngine({ replies: [ { text: "one", tools: [{ name: "open_approval_request", ok: true, request: { ...REQ, digest: "bad" }, ms: 1 }] }, { text: "two", tools: [{ name: "open_approval_request", ok: true, request: REQ, ms: 1 }] }, ] }); const c = createConnector({ binding: twoUsers(), journalDir, rest, gateway: fakeGateway(), engine, api }); await (await c.handleMessage(message({ id: "200000000000000080", content: "a" }))).turn; assert.equal(rest.calls.length, 1, "nothing posted for a bad request"); assert.match(readTurn(journalDir, "200000000000000080").approvalRequests[0].error, /digest/); await (await c.handleMessage(message({ id: "200000000000000081", content: "b" }))).turn; assert.equal(readTurn(journalDir, "200000000000000081").approvalRequests[0].status, "refused", "the request message was refused by Discord"); assert.equal(c.approvals.size, 0); assert.equal(api.calls.length, 0); assert.equal(readApprovals(journalDir).length, 0); const noApi = createConnector({ binding: twoUsers(), journalDir: join(makeRoot(), "j"), rest: fakeRest(), gateway: fakeGateway(), engine: fakeEngine({ replies: [{ text: "x", tools: [{ name: "open_approval_request", ok: true, request: REQ, ms: 1 }] }] }) }); const t = await (await noApi.handleMessage(message({ id: "200000000000000082", content: "c" }))).turn; assert.equal(t, "ok"); assert.equal(noApi.approvals.size, 0); }); test("approvals flow: start retries a bind and an approval left as unknown, under their original keys", async () => { const journalDir = join(makeRoot(), "j"); ensureJournal(journalDir); appendApproval(journalDir, { kind: "opened", at: "t0", ...REQ, messageId: "400000000000000001", channelId: IDS.admin, content: "c" }); appendApproval(journalDir, { kind: "bind", at: "t1", requestId: "APR-7", messageId: "400000000000000001", channelId: IDS.admin, status: "unknown", error: "timeout" }); appendApproval(journalDir, { kind: "approval", at: "t2", requestId: "APR-7", authorId: CARMEN, eventId: "300000000000000020", messageId: "400000000000000001", how: "button", evidenceId: "400000000000000002", statement: "Approval: Carmen approved DEC-12 v2 (digest 0123abcd) by button.", status: "intent" }); const api = fakeApi(); const c = createConnector({ binding: twoUsers(), journalDir, rest: withInteractions(fakeRest({ snowflakes: true })), gateway: fakeGateway(), engine: fakeEngine(), api }); const out = await c.reconcileApprovals(); assert.deepEqual(out, [{ kind: "bind", requestId: "APR-7", status: "done" }, { kind: "approval", requestId: "APR-7", authorId: CARMEN, status: "done" }]); assert.equal(api.calls[0].idempotencyKey, "sage:400000000000000001:bind"); assert.equal(api.calls[1].idempotencyKey, "sage:300000000000000020:approval"); assert.equal(api.calls[1].sourceUrl, `https://discord.com/channels/${IDS.guild}/${IDS.admin}/400000000000000002`, "the retry names the same evidence message"); assert.equal(api.calls[1].statement, "Approval: Carmen approved DEC-12 v2 (digest 0123abcd) by button."); assert.deepEqual(await c.reconcileApprovals(), [], "nothing left once done"); // a listed approver's later press is a repeat const r = await c.handleInteraction(interaction({ id: "300000000000000021", messageId: "400000000000000001", userId: CARMEN })); assert.equal(r.reason, "approval-already"); // start() runs the same reconcile and reports it const c2 = createConnector({ binding: twoUsers(), journalDir, rest: withInteractions(fakeRest({ snowflakes: true })), gateway: fakeGateway(), engine: fakeEngine(), api: fakeApi() }); const s = await c2.start(); assert.equal(s.inbox, 0); });