Files
stack/packages/discord/tests/notify.test.mjs
T
jason.woltjeandClaude Opus 5.5 2f5303c1c7 feat(cli): the mosaic CLI, broker host and decision notifier (row 39, S4, rocko)
packages/cli adds mosaic inbox, decide, tasks, agents and trail over the
human-cli transport, and mosaic bus start, stop and status as the trusted
host (unit mosaic-bus@<business>, scripts/bus-service.sh). The host boots
packages/bus/src/process.mjs, passes config.trackers from the tracker.*
variables (lead decision 70), and runs a notifier child. The notifier DMs
each open blocking decision once and sends an 08:00 America/Chicago
digest, journaled in notify/<business>/sent.jsonl at 0600 with no Discord
ids. A torn journal tail is copied aside and truncated; a malformed line,
a directory looser than 0700 or a symlinked journal refuses (lead
decision 71). packages/discord gains dmRecipient, createDm and notify.mjs.

Candidate agents/rocko/work/slice1-s4, base b9b6cf00, build.patch
b52f7d68, manifest e858504e (29 files). Darkwing approved round 2 on
#1521 (comment 26855), Filbert approved round 2 (comment 26856). The
packet's mutant table lists M28 as killed; it survived, and BUILD-LOG
records the correction.

Integration gate in a worktree on 2557e29d with the patch applied:
bus 67, business 60, cli 49, control-board 124, discord 178, ledger 78,
mosaic 69, queue 148, seat 19, tasks 51 and webui 14, all with no
failures. Conversation is 149/3, the same K1, K3 and K10 cases that fail
on the base; S4 doesn't touch the package. Every scripts/test-*.sh is
green, with test-release 14/14 and test-task 98/98 on the existing gate2
compose network. A scratch test, not in this commit, booted the real
host with trackers against S3's fake Vikunja: the adapter went ready and
a task.close on a missing task answered task-not-found after a Vikunja
read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 07:49:38 -05:00

81 lines
4.7 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { openDirect } from "../src/notify.mjs";
import { RestOutcome } from "../src/rest.mjs";
import { DiscordError } from "../src/errors.mjs";
import { makeRoot, makeDeployment, IDS } from "./helpers.mjs";
const CHANNEL = "100000000000000900";
const TOKEN = "MTAw.abcdefghijklmnopqrstuvwxyz0123456789";
function fakeFetch(script) {
const calls = [];
const fetch = async (url, init) => {
calls.push({ url, method: init.method, body: init.body ? JSON.parse(init.body) : null, auth: init.headers.Authorization });
const next = script.shift();
if (!next) throw new Error("fake fetch: no scripted response");
return { status: next.status, text: async () => (next.body === undefined ? "" : JSON.stringify(next.body)) };
};
return { fetch, calls };
}
const secretFree = (err) => ![IDS.owner, CHANNEL, TOKEN].some((s) => err.message.includes(s) || JSON.stringify(err.details ?? {}).includes(s));
test("notify: the DM channel opens once, every send carries the nonce, and only the message id comes back", async () => {
const { dataRoot } = makeDeployment(makeRoot(), { dmRecipient: IDS.owner });
const f = fakeFetch([
{ status: 200, body: { id: CHANNEL, type: 1 } },
{ status: 200, body: { id: "300000000000000001" } },
{ status: 200, body: { id: "300000000000000002" } },
]);
const dm = openDirect({ dataRoot, name: "test-seat", fetch: f.fetch, base: "http://fake" });
assert.deepEqual(await dm.send({ content: "one", nonce: "dm1" }), { messageId: "300000000000000001" });
assert.deepEqual(await dm.send({ content: "two", nonce: "dm2" }), { messageId: "300000000000000002" });
assert.deepEqual(f.calls.map((c) => `${c.method} ${c.url}`), [
"POST http://fake/users/@me/channels",
`POST http://fake/channels/${CHANNEL}/messages`,
`POST http://fake/channels/${CHANNEL}/messages`,
]);
assert.deepEqual(f.calls[0].body, { recipient_id: IDS.owner });
assert.equal(f.calls[1].body.nonce, "dm1");
assert.equal(f.calls[1].body.enforce_nonce, true);
assert.deepEqual(f.calls[1].body.allowed_mentions, { parse: [], replied_user: false });
assert.ok(f.calls.every((c) => c.auth === `Bot ${TOKEN}`));
assert.equal(JSON.stringify(dm).includes(TOKEN) || JSON.stringify(dm).includes(IDS.owner), false);
});
test("notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time", async () => {
const { dataRoot } = makeDeployment(makeRoot(), { dmRecipient: IDS.owner });
const f = fakeFetch([
{ status: 403, body: { code: 50007, message: `Cannot send messages to ${IDS.owner}` } },
{ status: 200, body: { id: CHANNEL } },
{ status: 502, body: { message: `channel ${CHANNEL} upstream` } },
{ status: 200, body: { id: "300000000000000003" } },
{ status: 403, body: { code: 50007, message: "Cannot send messages to this user" } },
{ status: 200, body: { id: CHANNEL } },
{ status: 200, body: { id: "300000000000000004" } },
]);
const dm = openDirect({ dataRoot, name: "test-seat", fetch: f.fetch, base: "http://fake" });
await assert.rejects(dm.send({ content: "x", nonce: "n1" }), (e) => e instanceof RestOutcome && e.kind === "refused" && e.details.status === 403 && secretFree(e));
await assert.rejects(dm.send({ content: "x", nonce: "n1" }), (e) => e instanceof RestOutcome && e.kind === "unknown" && secretFree(e));
// unknown keeps the channel; the retry reuses it with the same nonce.
assert.deepEqual(await dm.send({ content: "x", nonce: "n1" }), { messageId: "300000000000000003" });
await assert.rejects(dm.send({ content: "y", nonce: "n2" }), (e) => e.kind === "refused" && secretFree(e));
assert.deepEqual(await dm.send({ content: "y", nonce: "n2" }), { messageId: "300000000000000004" });
assert.equal(f.calls.filter((c) => c.url.endsWith("/users/@me/channels")).length, 3);
});
test("notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use", async () => {
let calls = 0;
const fetch = async () => {
calls++;
throw new Error("network");
};
const none = makeDeployment(makeRoot());
assert.throws(() => openDirect({ dataRoot: none.dataRoot, name: "test-seat", fetch }), (e) => e instanceof DiscordError && e.exitCode === 2 && /no dmRecipient/.test(e.message));
const loose = makeDeployment(makeRoot(), { dmRecipient: IDS.owner }, { tokenMode: 0o644 });
assert.throws(() => openDirect({ dataRoot: loose.dataRoot, name: "test-seat", fetch }), (e) => e instanceof DiscordError && /mode 0600/.test(e.message) && !e.message.includes(TOKEN));
assert.throws(() => openDirect({ dataRoot: none.dataRoot, name: "absent", fetch }), (e) => e instanceof DiscordError && /binding not found/.test(e.message));
assert.equal(calls, 0);
});