feat(discord): binding reload without a restart, and a per-user channel allowlist (#1509)
`reload` validates the binding file and sends SIGHUP to the live owner; the running connector re-reads it and swaps guildName, channels, users and limits in place. name, seat, guildId, botUserId, tokenFile, engine and context are fixed for the life of the process; a change there, an invalid file or a channel outside the guild refuses the reload and keeps the old binding. Every attempt is one line in reloads.jsonl. The service unit maps `systemctl --user reload` to the same signal. A user entry may carry `channels`, an allowlist of listed channel ids; absent means every listed channel. Outside the list the message is dropped as channel-not-for-user; threads count as their parent. Suite 41/41, 101 node tests. QUEUE rows 19 and 20 opened. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
||||
import { createConnector, FIXED_LINES, RECONCILE_WINDOW_MS, READ_RECEIPT } from "../src/connector.mjs";
|
||||
import { readOutbox, readDrops, listTurns, readInboxIds, appendOutbox, appendInbox, ensureJournal, requestStop, writeTurn, countAdmissionsOn, noticeOn } from "../src/journal.mjs";
|
||||
import { makeRoot, binding, message, IDS, fakeRest, fakeGateway, fakeEngine } from "./helpers.mjs";
|
||||
import { DiscordError } from "../src/errors.mjs";
|
||||
|
||||
function setup({ bindingOverrides = {}, replies = [], outcomes = [], now, engineOverrides = {} } = {}) {
|
||||
const root = makeRoot();
|
||||
@@ -435,3 +436,43 @@ test("receipt: Discord refusing the reaction leaves the turn intact and records
|
||||
assert.deepEqual(turns[0].receipt, { emoji: READ_RECEIPT, ok: false });
|
||||
await connector.stop();
|
||||
});
|
||||
|
||||
test("reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once", async () => {
|
||||
const { journalDir, engine, connector } = setup();
|
||||
await connector.start();
|
||||
const r0 = await connector.handleMessage(message({ id: "300000000000000090", author: { id: IDS.stranger } }));
|
||||
assert.equal(r0.reason, "user-unlisted");
|
||||
const diff = connector.reload(binding({
|
||||
channels: [{ id: IDS.admin, name: "seat-admin", mode: "open" }],
|
||||
users: [{ id: IDS.owner, name: "owner" }, { id: IDS.stranger, name: "guest", channels: [IDS.admin] }],
|
||||
limits: { turnsPerDay: 2, turnTimeoutSeconds: 180, replyChunkChars: 1900, inboundMaxChars: 4000 },
|
||||
}));
|
||||
assert.deepEqual(diff.users.added, [IDS.stranger]);
|
||||
assert.deepEqual(diff.channels.removed, [IDS.general]);
|
||||
assert.deepEqual(diff.limits, ["turnsPerDay"]);
|
||||
const r1 = await connector.handleMessage(message({ id: "300000000000000091", author: { id: IDS.stranger } }));
|
||||
assert.equal(r1.accepted, true);
|
||||
assert.equal(await r1.turn, "ok");
|
||||
const r2 = await connector.handleMessage(message({ id: "300000000000000092", channel_id: IDS.general, mentions: [{ id: IDS.bot }] }));
|
||||
assert.equal(r2.reason, "channel-unlisted");
|
||||
const r3 = await connector.handleMessage(message({ id: "300000000000000093" }));
|
||||
assert.equal(await r3.turn, "ok");
|
||||
const r4 = await connector.handleMessage(message({ id: "300000000000000094" }));
|
||||
assert.equal(r4.reason, "ceiling");
|
||||
assert.equal(engine.prompts.length, 2);
|
||||
assert.equal(readDrops(journalDir).map((d) => d.reason).join(","), "user-unlisted,channel-unlisted,ceiling");
|
||||
await connector.stop();
|
||||
});
|
||||
|
||||
test("reload: a fixed key refuses with exit 2 and the old binding stays in force", async () => {
|
||||
const { connector } = setup();
|
||||
await connector.start();
|
||||
const before = connector.binding;
|
||||
assert.throws(() => connector.reload(binding({ seat: "other" })), (err) => err instanceof DiscordError && err.exitCode === 2 && /seat cannot change/.test(err.message));
|
||||
assert.throws(() => connector.reload(binding({ engine: { provider: "zai", model: "glm-5.3", thinking: "low" } })), /engine cannot change/);
|
||||
assert.equal(connector.binding, before);
|
||||
const r = await connector.handleMessage(message({ id: "300000000000000095" }));
|
||||
assert.equal(r.accepted, true);
|
||||
assert.equal(await r.turn, "ok");
|
||||
await connector.stop();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user