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, baseb9b6cf00, 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 on2557e29dwith 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]>
117 lines
6.3 KiB
JavaScript
117 lines
6.3 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createRest, RestOutcome, USER_AGENT } from "../src/rest.mjs";
|
|
|
|
function fakeFetch(script) {
|
|
const calls = [];
|
|
const fetch = async (url, init) => {
|
|
calls.push({ url, init });
|
|
const next = script.shift();
|
|
if (!next) throw new Error("fake fetch: no scripted response");
|
|
if (next.throw) throw Object.assign(new Error(next.throw), { code: "ECONNRESET" });
|
|
return { status: next.status, text: async () => (next.body === undefined ? "" : JSON.stringify(next.body)) };
|
|
};
|
|
return { fetch, calls };
|
|
}
|
|
|
|
const TOKEN = "T0KEN-not-real-1234567890";
|
|
|
|
test("rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference", async () => {
|
|
const f = fakeFetch([{ status: 200, body: { id: "m1" } }]);
|
|
const rest = createRest({ token: TOKEN, fetch: f.fetch });
|
|
const r = await rest.createMessage("c1", { content: "hi", nonce: "n1", replyTo: "orig" });
|
|
assert.deepEqual(r, { messageId: "m1", status: 200 });
|
|
const call = f.calls[0];
|
|
assert.equal(call.url, "https://discord.com/api/v10/channels/c1/messages");
|
|
assert.equal(call.init.method, "POST");
|
|
assert.equal(call.init.headers.Authorization, `Bot ${TOKEN}`);
|
|
assert.equal(call.init.headers["User-Agent"], USER_AGENT);
|
|
assert.deepEqual(JSON.parse(call.init.body), {
|
|
content: "hi", nonce: "n1", enforce_nonce: true,
|
|
allowed_mentions: { parse: [], replied_user: false },
|
|
message_reference: { message_id: "orig", fail_if_not_exists: false },
|
|
});
|
|
});
|
|
|
|
test("rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown", async () => {
|
|
const waits = [];
|
|
const sleep = async (ms) => waits.push(ms);
|
|
let f = fakeFetch([{ status: 429, body: { retry_after: 0.25 } }, { status: 200, body: { id: "m2" } }]);
|
|
let rest = createRest({ token: TOKEN, fetch: f.fetch, sleep });
|
|
const r = await rest.createMessage("c1", { content: "hi", nonce: "n2" });
|
|
assert.equal(r.messageId, "m2");
|
|
assert.deepEqual(waits, [250]);
|
|
assert.equal(f.calls.length, 2);
|
|
|
|
f = fakeFetch([{ status: 403, body: { code: 50001, message: "Missing Access" } }]);
|
|
rest = createRest({ token: TOKEN, fetch: f.fetch, sleep });
|
|
await assert.rejects(rest.createMessage("c1", { content: "hi", nonce: "n3" }), (err) => err instanceof RestOutcome && err.kind === "refused" && err.details.status === 403);
|
|
|
|
f = fakeFetch([{ status: 502, body: { message: "bad gateway" } }]);
|
|
rest = createRest({ token: TOKEN, fetch: f.fetch, sleep });
|
|
await assert.rejects(rest.createMessage("c1", { content: "hi", nonce: "n4" }), (err) => err instanceof RestOutcome && err.kind === "unknown");
|
|
|
|
f = fakeFetch([{ throw: "socket hang up" }]);
|
|
rest = createRest({ token: TOKEN, fetch: f.fetch, sleep });
|
|
await assert.rejects(rest.createMessage("c1", { content: "hi", nonce: "n5" }), (err) => err instanceof RestOutcome && err.kind === "unknown" && !err.message.includes(TOKEN));
|
|
|
|
// Bounded 429 retries: four 429s in a row end refused.
|
|
f = fakeFetch([429, 429, 429, 429].map(() => ({ status: 429, body: { retry_after: 0.01 } })));
|
|
rest = createRest({ token: TOKEN, fetch: f.fetch, sleep });
|
|
await assert.rejects(rest.createMessage("c1", { content: "hi", nonce: "n6" }), (err) => err.kind === "refused" && err.details.status === 429);
|
|
assert.equal(f.calls.length, 4);
|
|
});
|
|
|
|
test("rest: content and nonce limits are enforced locally; typing never throws", async () => {
|
|
const f = fakeFetch([{ status: 500 }, { throw: "down" }]);
|
|
const rest = createRest({ token: TOKEN, fetch: f.fetch });
|
|
await assert.rejects(rest.createMessage("c", { content: "x".repeat(2001), nonce: "n" }), /1..2000/);
|
|
await assert.rejects(rest.createMessage("c", { content: "x", nonce: "n".repeat(26) }), /1..25/);
|
|
await rest.typing("c");
|
|
await rest.typing("c");
|
|
assert.equal(f.calls.length, 2);
|
|
});
|
|
|
|
test("rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws", async () => {
|
|
const f = fakeFetch([{ status: 204 }, { status: 403, body: { message: "Missing Permissions", code: 50013 } }, { throw: "down" }]);
|
|
const logs = [];
|
|
const rest = createRest({ token: TOKEN, fetch: f.fetch, log: (m) => logs.push(m) });
|
|
assert.equal(await rest.react("c1", "m1", "\u{1F440}"), true);
|
|
assert.equal(f.calls[0].url, "https://discord.com/api/v10/channels/c1/messages/m1/reactions/%F0%9F%91%80/@me");
|
|
assert.equal(f.calls[0].init.method, "PUT");
|
|
assert.equal(f.calls[0].init.body, undefined);
|
|
assert.equal(f.calls[0].init.headers.Authorization, `Bot ${TOKEN}`);
|
|
assert.equal(await rest.react("c1", "m2", "\u{1F440}"), false);
|
|
assert.equal(await rest.react("c1", "m3", "\u{1F440}"), false);
|
|
assert.equal(logs.length, 2);
|
|
assert.match(logs[0], /HTTP 403/);
|
|
assert.ok(logs.every((l) => !l.includes(TOKEN)));
|
|
await assert.rejects(rest.react("c1", "m4", ""), /emoji required/);
|
|
});
|
|
|
|
test("rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent", async () => {
|
|
let f = fakeFetch([{ status: 200, body: { id: "100000000000000900", type: 1 } }]);
|
|
let rest = createRest({ token: TOKEN, fetch: f.fetch });
|
|
assert.equal(await rest.createDm("100000000000000002"), "100000000000000900");
|
|
assert.equal(f.calls[0].url, "https://discord.com/api/v10/users/@me/channels");
|
|
assert.equal(f.calls[0].init.method, "POST");
|
|
assert.deepEqual(JSON.parse(f.calls[0].init.body), { recipient_id: "100000000000000002" });
|
|
|
|
f = fakeFetch([{ status: 403, body: { code: 50007, message: "Cannot send messages to this user" } }]);
|
|
rest = createRest({ token: TOKEN, fetch: f.fetch });
|
|
await assert.rejects(rest.createDm("100000000000000002"), (err) => err instanceof RestOutcome && err.kind === "refused" && err.details.status === 403);
|
|
|
|
f = fakeFetch([{ status: 503 }]);
|
|
rest = createRest({ token: TOKEN, fetch: f.fetch });
|
|
await assert.rejects(rest.createDm("100000000000000002"), (err) => err instanceof RestOutcome && err.kind === "unknown");
|
|
|
|
f = fakeFetch([{ status: 200, body: {} }]);
|
|
rest = createRest({ token: TOKEN, fetch: f.fetch });
|
|
await assert.rejects(rest.createDm("100000000000000002"), (err) => err instanceof RestOutcome && err.kind === "unknown");
|
|
|
|
f = fakeFetch([]);
|
|
rest = createRest({ token: TOKEN, fetch: f.fetch });
|
|
await assert.rejects(rest.createDm("not-an-id"), /recipient must be a snowflake/);
|
|
assert.equal(f.calls.length, 0);
|
|
});
|