import { test } from "node:test"; import assert from "node:assert/strict"; import { writeFileSync, existsSync, mkdirSync, rmSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { spawn, spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { ensureJournal, writePid, readPid, clearPid, stopTarget, ownerAlive, processStart, lockPath, ownerPath, unlock, appendNotice, noticeOn, stopRequested, stopPath, bootId, ownerState, validStart, validBoot, pidAlive, } from "../src/journal.mjs"; import { DiscordError } from "../src/errors.mjs"; import { makeRoot } from "./helpers.mjs"; function journal() { const dir = join(makeRoot(), "journal"); ensureJournal(dir); return dir; } function publish(dir, rec) { mkdirSync(lockPath(dir), { recursive: true }); writeFileSync(ownerPath(dir), JSON.stringify(rec) + "\n", { mode: 0o600 }); } // Spawns n claim workers over dir, releases them together, and resolves with // their verdicts once all have answered. `finish` releases any winner. function race(dir, n, tag) { const worker = fileURLToPath(new URL("../fixtures/claim-worker.mjs", import.meta.url)); const go = join(dir, `go-${tag}`); const done = join(dir, `done-${tag}`); const closes = []; const verdicts = []; let settle; const answered = new Promise((resolve) => { settle = resolve; }); for (let i = 0; i < n; i += 1) { const child = spawn(process.execPath, [worker, dir, go, done], { stdio: ["ignore", "pipe", "pipe"] }); const entry = { pid: child.pid, out: "", err: "", code: null }; child.stdout.on("data", (d) => { entry.out += d; if (entry.out.endsWith("\n")) { verdicts.push(entry); if (verdicts.length === n) settle(); } }); child.stderr.on("data", (d) => { entry.err += d; }); closes.push(new Promise((resolve) => child.on("close", (code) => { entry.code = code; resolve(entry); }))); } writeFileSync(go, ""); return answered.then(() => ({ winners: verdicts.filter((r) => r.out.trim() === "claimed"), losers: verdicts.filter((r) => r.out.trim() === "refused"), stopped: verdicts.filter((r) => r.out.trim() === "stopped"), verdicts, async finish() { writeFileSync(done, ""); const results = await Promise.all(closes); assert.equal(results.every((r) => r.code === 0), true, JSON.stringify(results)); }, })); } test("lock: the claim is exclusive; a second start against a live owner refuses", () => { const dir = journal(); writePid(dir, process.pid); const rec = readPid(dir); assert.equal(rec.pid, process.pid); assert.equal(rec.start, processStart(process.pid)); assert.equal(rec.boot, bootId()); assert.ok(rec.start !== null && rec.boot !== null, "this host has /proc; start marker and boot id are recorded"); assert.equal(ownerState(rec), "live"); assert.equal(existsSync(join(lockPath(dir), "owner.json.tmp")), false, "the record is published by rename"); assert.throws(() => writePid(dir, process.pid), (err) => err instanceof DiscordError && /another connector is running/.test(err.message), "a live matching owner refuses even a repeat claim"); assert.equal(stopTarget(dir), process.pid); assert.throws(() => unlock(dir), /refusing to unlock: the connector is running/); assert.equal(stopRequested(dir), true, "unlock wrote STOP before inspecting"); rmSync(stopPath(dir)); clearPid(dir, process.pid + 1); assert.equal(existsSync(lockPath(dir)), true, "a different pid cannot clear the lock"); clearPid(dir, process.pid); assert.equal(existsSync(lockPath(dir)), false); }); test("lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it", () => { const dir = journal(); const dead = { pid: 2 ** 22 - 7, start: "1", boot: bootId() }; publish(dir, dead); assert.equal(stopTarget(dir), null); assert.throws(() => writePid(dir, process.pid), (err) => err instanceof DiscordError && /which is gone/.test(err.message) && /unlock/.test(err.message)); assert.deepEqual(readPid(dir), dead, "run did not touch the stale lock"); assert.deepEqual(unlock(dir), dead); assert.equal(existsSync(lockPath(dir)), false); assert.equal(unlock(dir), false, "nothing to unlock"); assert.equal(stopRequested(dir), true, "STOP stays after unlock"); assert.throws(() => writePid(dir, process.pid), /STOP is present/); assert.equal(existsSync(lockPath(dir)), false, "a claim that meets STOP releases itself"); rmSync(stopPath(dir)); // Reused pid: our own live pid but a start marker that does not match. const otherStart = String(BigInt(processStart(process.pid)) + 1n); publish(dir, { pid: process.pid, start: otherStart, boot: bootId() }); assert.equal(ownerState(readPid(dir)), "mismatch"); assert.equal(ownerAlive(readPid(dir)), false); assert.equal(stopTarget(dir), null, "stop must not signal a process whose identity does not match"); assert.throws(() => writePid(dir, process.pid), /which is gone or is a different process/); assert.equal(readPid(dir).start, otherStart, "still untouched"); unlock(dir); rmSync(stopPath(dir)); // Same pid and start ticks but a different boot id: a process from another // boot. Never signaled, refuses run, unlock clears it. publish(dir, { pid: process.pid, start: processStart(process.pid), boot: "00000000-0000-0000-0000-000000000000" }); assert.equal(ownerState(readPid(dir)), "mismatch"); assert.equal(stopTarget(dir), null, "a different boot is never a signal target"); assert.throws(() => writePid(dir, process.pid), /which is gone or is a different process/); assert.deepEqual(readPid(dir).boot, "00000000-0000-0000-0000-000000000000", "still untouched"); unlock(dir); rmSync(stopPath(dir)); // A record without a start marker or boot id is never a signal target; // with a live pid it is unknown (refuses everything), with a dead pid it is dead. publish(dir, { pid: process.pid, boot: bootId() }); assert.equal(ownerState(readPid(dir)), "unknown"); assert.equal(stopTarget(dir), null); assert.throws(() => writePid(dir, process.pid), /cannot be verified/); assert.throws(() => unlock(dir), /cannot be verified; nothing removed/); rmSync(stopPath(dir)); rmSync(lockPath(dir), { recursive: true }); publish(dir, { pid: 2 ** 22 - 7, start: "1" }); assert.equal(ownerState(readPid(dir)), "dead"); unlock(dir); rmSync(stopPath(dir)); writePid(dir, process.pid); assert.equal(readPid(dir).start, processStart(process.pid)); clearPid(dir, process.pid); }); test("lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it", () => { const dir = journal(); mkdirSync(lockPath(dir)); assert.throws(() => writePid(dir, process.pid), (err) => err instanceof DiscordError && /without an owner record/.test(err.message)); assert.equal(existsSync(lockPath(dir)), true, "the in-progress claim was left alone"); assert.equal(readPid(dir), null); assert.equal(stopTarget(dir), null); assert.equal(unlock(dir), null); assert.equal(existsSync(lockPath(dir)), false); rmSync(stopPath(dir)); writePid(dir, process.pid); clearPid(dir, process.pid); }); test("lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over", () => { const dir = journal(); for (const content of ["12345\n", "{\"pid\":\"x\"}\n", "null\n", ""]) { mkdirSync(lockPath(dir), { recursive: true }); writeFileSync(ownerPath(dir), content, { mode: 0o600 }); assert.equal(ownerState(readPid(dir)), "invalid", JSON.stringify(content)); assert.equal(stopTarget(dir), null); assert.throws(() => writePid(dir, process.pid), /cannot be read/); assert.throws(() => unlock(dir), /cannot be read; nothing removed/); assert.equal(readFileSync(ownerPath(dir), "utf8"), content, "byte-identical"); clearPid(dir, process.pid); assert.equal(existsSync(ownerPath(dir)), true, "clearPid never acts on an unreadable record"); rmSync(stopPath(dir)); rmSync(lockPath(dir), { recursive: true }); } writePid(dir, process.pid); clearPid(dir, process.pid); }); // Spawn a live child that publishes an owner record it never clears, and // prove that while it lives nothing signals it, removes its lock, or claims // over it; once it exits, unlock clears the lock and one claim succeeds. async function liveOwnerRefusesEverything(startArg, bootArg, label) { const dir = journal(); const worker = fileURLToPath(new URL("../fixtures/legacy-owner-worker.mjs", import.meta.url)); const done = join(dir, "done"); const child = spawn(process.execPath, [worker, dir, done, startArg, bootArg], { stdio: ["ignore", "pipe", "inherit"] }); const exited = new Promise((resolve) => child.on("close", resolve)); await new Promise((resolve) => child.stdout.on("data", (d) => { if (String(d).includes("legacy-published")) resolve(); })); const before = readFileSync(ownerPath(dir), "utf8"); const rec = readPid(dir); assert.equal(rec.pid, child.pid, label); assert.equal(ownerState(rec), "unknown", label); assert.equal(stopTarget(dir), null, `${label}: no signal target`); assert.throws(() => unlock(dir), /pid \d+ is alive and its identity cannot be verified; nothing removed/, label); assert.equal(readFileSync(ownerPath(dir), "utf8"), before, `${label}: byte-identical lock`); assert.equal(stopRequested(dir), true, label); rmSync(stopPath(dir)); assert.throws(() => writePid(dir, process.pid), /cannot be verified/, `${label}: no second owner while the process lives`); assert.equal(readFileSync(ownerPath(dir), "utf8"), before, label); assert.equal(pidAlive(child.pid), true, `${label}: the original process is still alive`); writeFileSync(done, ""); assert.equal(await exited, 0, label); assert.equal(ownerState(readPid(dir)), "dead", label); assert.deepEqual(unlock(dir), rec, label); assert.equal(existsSync(lockPath(dir)), false, label); rmSync(stopPath(dir)); writePid(dir, process.pid); assert.equal(stopTarget(dir), process.pid, `${label}: one owner after recovery`); clearPid(dir, process.pid); return rec; } test("lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it", async () => { const rec = await liveOwnerRefusesEverything("real", "-", "legacy"); assert.equal(rec.boot, null, "the round-five record carries no boot id"); assert.notEqual(rec.start, null); }); test("lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it", async () => { const cases = [ ["", "real", "empty start"], ["not-a-tick", "real", "nondecimal start"], ["12abc", "real", "mixed start"], ["real", "", "empty boot"], ["real", "not-a-uuid", "malformed boot"], ["real", "97DF3044-E52D-45A3-810D-C3F2F54634F4", "uppercase boot"], ["000", "real", "leading-zero start"], ["0", "real", "zero start"], ["99999999999999999999", "real", "start above 2^64-1"], ]; for (const [startArg, bootArg, label] of cases) { const rec = await liveOwnerRefusesEverything(startArg, bootArg, label); assert.ok(rec.start === null || rec.boot === null, `${label}: the malformed value reads as absent`); } }); test("lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities", () => { for (const v of ["1", "1234567890", "18446744073709551615"]) assert.equal(validStart(v), true, v); for (const v of ["", " 1", "1 ", "-1", "1.5", "abc", "1e3", 1, null, undefined, "0", "00", "01", "000", "18446744073709551616", "99999999999999999999", "9".repeat(21)]) assert.equal(validStart(v), false, String(v)); assert.equal(validBoot("97df3044-e52d-45a3-810d-c3f2f54634f4"), true); for (const v of ["", "97df3044", "97DF3044-E52D-45A3-810D-C3F2F54634F4", "97df3044-e52d-45a3-810d-c3f2f54634f4\n", "g7df3044-e52d-45a3-810d-c3f2f54634f4", null, 5]) assert.equal(validBoot(v), false, String(v)); assert.equal(validStart(processStart(process.pid)), true, "this process's real start is valid"); assert.equal(validBoot(bootId()), true, "this host's real boot id is valid"); }); test("lock: a process whose start marker or boot id cannot be read refuses to claim", () => { const dir = journal(); assert.equal(processStart(2 ** 22 - 7), null); assert.throws(() => writePid(dir, 2 ** 22 - 7), (err) => err instanceof DiscordError && /start time or the boot id/.test(err.message)); assert.equal(existsSync(lockPath(dir)), false, "nothing was left behind"); const noBoot = (pid) => ({ start: processStart(pid), boot: null }); assert.throws(() => writePid(dir, process.pid, { identity: noBoot }), /start time or the boot id/); assert.equal(existsSync(lockPath(dir)), false); }); test("lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over", () => { const dir = journal(); writePid(dir, process.pid); const before = JSON.stringify(readPid(dir)); const unreadable = () => ({ start: null, boot: null }); assert.equal(ownerState(readPid(dir), { identity: unreadable }), "unknown"); assert.equal(stopTarget(dir, { identity: unreadable }), null, "no signal target"); assert.throws(() => unlock(dir, { identity: unreadable }), /identity cannot be verified; nothing removed/); assert.equal(JSON.stringify(readPid(dir)), before, "the lock is unchanged"); assert.equal(stopRequested(dir), true); rmSync(stopPath(dir)); assert.throws(() => writePid(dir, process.pid, { identity: unreadable }), /start time or the boot id/, "a claimant without identity cannot claim"); const halfBlind = (pid) => ({ start: processStart(pid), boot: null }); assert.throws(() => writePid(dir, process.pid + 1, { identity: halfBlind }), /start time or the boot id/); // The claimant's own identity is readable (fabricated; the refusal happens before anything is written). const claimantOk = (pid) => (pid === process.pid ? { start: null, boot: null } : { start: "1", boot: bootId() }); assert.throws(() => writePid(dir, process.pid + 1, { identity: claimantOk }), /alive but whose identity cannot be verified/, "a healthy claimant still refuses over an unknown owner"); assert.equal(JSON.stringify(readPid(dir)), before, "still unchanged"); // Identity readable again: one owner, and it is the original. assert.equal(stopTarget(dir), process.pid); assert.throws(() => unlock(dir), /connector is running/); rmSync(stopPath(dir)); clearPid(dir, process.pid); assert.equal(existsSync(lockPath(dir)), false); }); test("lock: four processes racing for the same binding; exactly one claims it and the others refuse", async () => { const dir = journal(); const r = await race(dir, 4, "a"); assert.equal(r.winners.length, 1, JSON.stringify(r.verdicts)); assert.equal(r.losers.length, 3, JSON.stringify(r.verdicts)); const rec = readPid(dir); assert.equal(rec.pid, r.winners[0].pid, "the published owner is the winner"); assert.equal(stopTarget(dir), r.winners[0].pid, "the live winner is the only stop target"); assert.throws(() => writePid(dir, process.pid), /another connector is running/, "a fifth start refuses while the winner holds the lock"); await r.finish(); assert.equal(existsSync(lockPath(dir)), false, "the winner released the lock on exit"); }); test("lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner", async () => { const dir = journal(); const stale = { pid: 2 ** 22 - 7, start: "1", boot: bootId() }; publish(dir, stale); // Several starts race over the stale lock: none may reclaim it. const r1 = await race(dir, 4, "stale"); assert.equal(r1.winners.length, 0, JSON.stringify(r1.verdicts)); assert.equal(r1.losers.length, 4); assert.deepEqual(readPid(dir), stale, "the stale lock is exactly as it was"); await r1.finish(); // Operator cleanup, once. A second unlock finds nothing. assert.deepEqual(unlock(dir), stale); assert.equal(unlock(dir), false); // While STOP stands, a race over the cleared binding produces no owner. const r15 = await race(dir, 3, "gated"); assert.equal(r15.winners.length, 0, JSON.stringify(r15.verdicts)); assert.ok(r15.stopped.length >= 1, "at least the claim that published met STOP and released itself"); assert.equal(r15.stopped.length + r15.losers.length, 3, "the rest refused on the transient lock; none holds"); assert.equal(existsSync(lockPath(dir)), false, "no residue"); await r15.finish(); rmSync(stopPath(dir)); // Now the same contenders race for the cleared binding: one owner. const r2 = await race(dir, 4, "fresh"); assert.equal(r2.winners.length, 1, JSON.stringify(r2.verdicts)); assert.equal(r2.losers.length, 3); const live = readPid(dir); assert.equal(live.pid, r2.winners[0].pid); assert.equal(stopTarget(dir), live.pid); assert.throws(() => unlock(dir), /connector is running/, "unlock never removes a live owner"); await r2.finish(); assert.equal(existsSync(lockPath(dir)), false); }); test("lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue", () => { // Reviewer's schedule (#1509 comment 26132): unlock inspects stale S; C // claims in the gap after inspection; unlock acts; D claims in the gap // too. Required: no live owner is displaced, no residue, at most one owner. const dir = journal(); const worker = fileURLToPath(new URL("../fixtures/claim-worker.mjs", import.meta.url)); const go = join(dir, "go"); writeFileSync(go, ""); const stale = { pid: 2 ** 22 - 7, start: "1", boot: bootId() }; publish(dir, stale); const claims = []; const claim = (tag) => { const r = spawnSync(process.execPath, [worker, dir, go, join(dir, `done-${tag}`)], { encoding: "utf8", timeout: 10_000 }); claims.push({ tag, out: r.stdout.trim(), err: r.stderr.trim(), status: r.status }); return r.stdout.trim(); }; const beforeRemove = () => { // C: the stale lock is still there, so C refuses on the record. assert.equal(claim("C"), "refused"); // Simulate C's record having landed anyway (as if S vanished first), then D. rmSync(lockPath(dir), { recursive: true, force: true }); assert.equal(claim("D"), "stopped", "D published, met STOP, released itself"); assert.equal(existsSync(lockPath(dir)), false, "D left nothing"); // A record that lands right before removal is also not an owner: STOP // was written before it could have published. publish(dir, { pid: process.pid, start: processStart(process.pid), boot: bootId() }); }; const cleared = unlock(dir, { beforeRemove }); assert.deepEqual(cleared, stale, "unlock reports the record it inspected"); assert.equal(existsSync(lockPath(dir)), false, "no lock at the canonical path"); assert.equal(readdirSync(dir).filter((f) => f.startsWith("run.lock")).length, 0, "no residue"); assert.equal(stopTarget(dir), null, "no live owner"); assert.equal(claims.every((c) => c.status === 0), true, JSON.stringify(claims)); assert.equal(stopRequested(dir), true, "STOP stands until the operator removes it"); rmSync(stopPath(dir)); writePid(dir, process.pid); assert.equal(stopTarget(dir), process.pid, "after STOP is removed, one clean claim"); clearPid(dir, process.pid); }); test("notices: a kind is recorded per UTC day and found again", () => { const dir = journal(); assert.equal(noticeOn(dir, "ceiling", "2026-09-13"), false); appendNotice(dir, { kind: "ceiling", date: "2026-09-13", at: "2026-09-13T10:00:00.000Z" }); assert.equal(noticeOn(dir, "ceiling", "2026-09-13"), true); assert.equal(noticeOn(dir, "ceiling", "2026-09-14"), false); assert.equal(noticeOn(dir, "other", "2026-09-13"), false); assert.throws(() => appendNotice(dir, { kind: "ceiling" }), DiscordError); });