Row 25, parts 2a and 2b, against the shared-signals contract a5425a2. Model side: eight fixed verbs in the pi extension (record_list, record_get, record_create, record_update, resolve_id, open_approval_request, get_approval_request, create_document), each one HTTP call with arguments checked before any request. Writes carry an idempotency key <principal>:<message id>:<call index> and an audit context. The seat key is read from a 0600 file on every call and never cached, printed or journaled. Connector side: append-only approval ledger, Approve button and exact "approve" reply resolved by the connector against the required approvers, confirmation message posted as button evidence, bind and add_approval through the service under connector keys, retry of unknown entries on start. Evidence: node tests 162 pass, scripts/test-discord.sh 63/63. Review by rev-code-02, round 1 approved (#1509 comment 26467, tree 7872d8c5). Co-Authored-By: Claude Fable 5.1 <[email protected]>
301 lines
20 KiB
JavaScript
301 lines
20 KiB
JavaScript
// 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);
|
|
});
|