import { test } from "node:test"; import assert from "node:assert/strict"; import { appendFileSync, chmodSync, mkdirSync, readdirSync, readFileSync, statSync, symlinkSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { createNotifier, digestContent, digestNonce, dmNonce, journalPath, openJournal, runLoop, zoned } from "../src/notifier.mjs"; import { RestOutcome } from "../../discord/src/rest.mjs"; import { broker, tmp } from "./helpers.mjs"; // Discord-side fake: records sends, answers from a script (default: ok). function fakeDirect(script = []) { const sends = []; let n = 0; return { sends, async send(m) { sends.push(m); const next = script.shift() ?? "ok"; if (next === "ok") return { messageId: `30000000000000${String(++n).padStart(4, "0")}` }; throw new RestOutcome(next, `dm: ${next}`, { status: next === "refused" ? 403 : null }); }, }; } // 2026-10-08 is CDT (UTC-5): 13:00Z is 08:00 Chicago. const at = (iso) => { const clock = { t: new Date(iso) }; return { clock, now: () => clock.t, advance: (ms) => (clock.t = new Date(clock.t.getTime() + ms)) }; }; function setup(t, iso, script) { const bus = broker(t); const dataRoot = tmp(t); const direct = fakeDirect(script); const time = at(iso); const logs = []; const make = () => createNotifier({ business: "demo", dataRoot, inbox: async () => bus.read("inbox"), direct, now: time.now, log: (l) => logs.push(l) }); return { ...bus, dataRoot, direct, time, logs, make, notifier: make(), journal: () => readFileSync(journalPath(dataRoot, "demo"), "utf8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)) }; } test("zoned uses the IANA zone across DST", () => { assert.deepEqual(zoned(new Date("2026-10-08T13:00:00Z")), { day: "2026-10-08", hour: 8 }); assert.deepEqual(zoned(new Date("2026-12-08T13:00:00Z")), { day: "2026-12-08", hour: 7 }); assert.deepEqual(zoned(new Date("2026-10-09T04:59:00Z")), { day: "2026-10-08", hour: 23 }); assert.throws(() => zoned(new Date(), "Not/AZone"), RangeError); }); test("each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not", async (t) => { const s = setup(t, "2026-10-08T12:00:00Z"); const blocking = s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" }); s.raise("deploy", { target: "staging" }); assert.deepEqual(await s.notifier.tick(), { dms: 1, digest: false, failed: 0 }); assert.deepEqual(await s.notifier.tick(), { dms: 0, digest: false, failed: 0 }); assert.deepEqual(await s.make().tick(), { dms: 0, digest: false, failed: 0 }); assert.equal(s.direct.sends.length, 1); const [dm] = s.direct.sends; assert.equal(dm.nonce, dmNonce(blocking.id)); assert.ok(dm.nonce.length <= 25); assert.match(dm.content, /a blocking decision needs you/); assert.match(dm.content, /choosing "yes" authorizes it/); assert.match(dm.content, /task vikunja:1\/7/); assert.match(dm.content, new RegExp(`mosaic decide ${blocking.id.slice(0, 8)}