Files
stack/packages/discord/tests/recover.test.mjs
T
jason.woltjeandClaude Fable 5.1 436ba6ed6b feat(discord): systemd user service with a supervised run; brakes exit 3 and are never retried (#1509)
QUEUE row 17, MVP iteration 2. scripts/discord-service.sh renders and
installs mosaic-discord@<binding> from packages/discord/systemd/. The
unit's main process is `run --supervised`, which applies the new recover
policy first: a lock whose owner is gone is cleared and only the STOP
written for that is removed; an operator STOP or a held binding refuses
with exit 3, which RestartPreventExitStatus never retries. `recover` is
also a CLI verb. First cut used ExecStartPre and looped live, since systemd
honours the never-retry status only from the main process; replaced and
re-verified before any message traffic. Suite 40/40, 95 node tests.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-13 14:39:11 -05:00

193 lines
8.2 KiB
JavaScript

// `recover`: the supervised pre-start behind the service unit. It may clear
// a lock whose owner is gone and remove only the STOP it wrote for that;
// every operator brake, and every held binding, refuses with exit 3.
import { test } from "node:test";
import assert from "node:assert/strict";
import { writeFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { spawn, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import {
ensureJournal, recover, requestStop, readStop, stopRequested, stopPath, lockPath, ownerPath, writePid, BRAKE_EXIT,
RECOVER_REASON,
} from "../src/journal.mjs";
import { DiscordError } from "../src/errors.mjs";
import { makeRoot, makeRepo, makeDeployment } from "./helpers.mjs";
const cli = fileURLToPath(new URL("../src/cli.mjs", import.meta.url));
const worker = fileURLToPath(new URL("../fixtures/legacy-owner-worker.mjs", import.meta.url));
const BOOT = "01234567-89ab-cdef-0123-456789abcdef";
function journal() {
const dir = join(makeRoot(), "journal");
ensureJournal(dir);
return dir;
}
function deadPid() {
const r = spawnSync(process.execPath, ["-e", "process.stdout.write(String(process.pid))"], { encoding: "utf8" });
return Number(r.stdout);
}
function publish(dir, rec) {
mkdirSync(lockPath(dir), { recursive: true });
writeFileSync(ownerPath(dir), JSON.stringify(rec) + "\n", { mode: 0o600 });
}
const brake = (re) => (err) => err instanceof DiscordError && err.exitCode === BRAKE_EXIT && re.test(err.message);
// A live owner with the given identity arguments, held until `release`.
function holdLock(dir, startArg, bootArg) {
const done = join(dir, `done-${startArg}-${bootArg}`);
const child = spawn(process.execPath, [worker, dir, done, startArg, bootArg], { stdio: ["ignore", "pipe", "inherit"] });
const published = new Promise((resolve) => {
let out = "";
child.stdout.on("data", (d) => { out += d; if (out.includes("legacy-published")) resolve(); });
});
const closed = new Promise((resolve) => child.on("close", resolve));
return { published, release: () => { writeFileSync(done, ""); return closed; } };
}
test("recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent", () => {
const dir = journal();
assert.equal(recover(dir), "clean");
assert.equal(stopRequested(dir), false);
publish(dir, { pid: deadPid(), start: "1", boot: BOOT, at: "x" });
assert.equal(recover(dir), "cleared");
assert.equal(existsSync(lockPath(dir)), false, "dead owner's lock removed");
assert.equal(stopRequested(dir), false, "the STOP written for the unlock is gone again");
mkdirSync(lockPath(dir));
assert.equal(recover(dir), "cleared", "a lock without a record (interrupted start) is cleared too");
assert.equal(existsSync(lockPath(dir)), false);
assert.equal(stopRequested(dir), false);
assert.equal(recover(dir), "clean");
});
test("recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says", () => {
const dir = journal();
requestStop(dir, "cli stop");
assert.throws(() => recover(dir), brake(/STOP is present/));
assert.equal(stopRequested(dir), true);
publish(dir, { pid: deadPid(), start: "1", boot: BOOT, at: "x" });
assert.throws(() => recover(dir), brake(/STOP is present/));
assert.equal(existsSync(lockPath(dir)), true, "the lock is not touched behind a brake");
assert.equal(readStop(dir).length, 1, "nothing appended to STOP");
// A STOP that recover itself wrote and then failed to remove is ours, but
// only when it is the whole file.
const own = journal();
requestStop(own, RECOVER_REASON);
assert.equal(recover(own), "clean");
assert.equal(stopRequested(own), false);
requestStop(own, RECOVER_REASON);
requestStop(own, "unlock");
assert.throws(() => recover(own), brake(/STOP is present/));
assert.equal(readStop(own).length, 2);
// A line this code did not write is never ours.
const foreign = journal();
writeFileSync(stopPath(foreign), "stop\n", { mode: 0o600 });
assert.throws(() => recover(foreign), brake(/STOP is present/));
assert.deepEqual(readStop(foreign), [{ reason: null }]);
});
test("recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused", () => {
const dir = journal();
publish(dir, { pid: deadPid(), start: "1", boot: BOOT, at: "x" });
assert.throws(
() => recover(dir, { beforeRemove: () => requestStop(dir, "cli stop") }),
brake(/written by an operator while recovering/),
);
assert.equal(existsSync(lockPath(dir)), false, "the dead lock was removed before the brake was seen");
assert.deepEqual(readStop(dir).map((l) => l.reason), [RECOVER_REASON, "cli stop"]);
assert.throws(() => recover(dir), brake(/STOP is present/), "and it stays refused afterwards");
});
test("recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record", async () => {
const live = journal();
const held = holdLock(live, "real", "real");
await held.published;
try {
assert.throws(() => recover(live), brake(/another connector is running/));
assert.equal(stopRequested(live), false, "recover never brakes a running connector");
assert.equal(existsSync(ownerPath(live)), true);
} finally {
await held.release();
}
const unknown = journal();
const legacy = holdLock(unknown, "real", "-");
await legacy.published;
try {
assert.throws(() => recover(unknown), brake(/cannot be verified/));
assert.equal(stopRequested(unknown), false);
assert.equal(existsSync(ownerPath(unknown)), true);
} finally {
await legacy.release();
}
const invalid = journal();
mkdirSync(lockPath(invalid));
writeFileSync(ownerPath(invalid), "{not json", { mode: 0o600 });
assert.throws(() => recover(invalid), brake(/cannot be read/));
assert.equal(stopRequested(invalid), false);
assert.equal(readFileSync(ownerPath(invalid), "utf8"), "{not json");
// Our own live claim is a held binding too.
const mine = journal();
writePid(mine, process.pid);
assert.throws(() => recover(mine), brake(/another connector is running/));
assert.equal(stopRequested(mine), false);
});
test("cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3", () => {
const root = makeRoot();
const repo = makeRepo(root);
const dep = makeDeployment(root);
const run = (args) => spawnSync(process.execPath, [cli, ...args, "--config", dep.config, "--repo", repo], { encoding: "utf8" });
let r = run(["recover", "test-seat"]);
assert.equal(r.status, 0, r.stderr);
assert.match(r.stdout, /ready to run/);
r = run(["stop", "test-seat"]);
assert.equal(r.status, 0, r.stderr);
r = run(["recover", "test-seat"]);
assert.equal(r.status, 3, r.stderr);
assert.match(r.stderr, /the brake is on/);
r = run(["run", "test-seat"]);
assert.equal(r.status, 3, r.stderr);
const journalDir = join(dep.dataRoot, "discord", "test-seat");
unlinkSync(stopPath(journalDir));
publish(journalDir, { pid: deadPid(), start: "1", boot: BOOT, at: "x" });
r = run(["recover", "test-seat"]);
assert.equal(r.status, 0, r.stderr);
assert.match(r.stdout, /was removed; STOP is absent/);
assert.equal(existsSync(lockPath(journalDir)), false);
assert.equal(stopRequested(journalDir), false);
r = run(["recover"]);
assert.equal(r.status, 4);
// The service unit's form: the same policy inside the main process, so
// the exit status is the one systemd never retries.
r = run(["stop", "test-seat"]);
assert.equal(r.status, 0, r.stderr);
r = run(["run", "test-seat", "--supervised"]);
assert.equal(r.status, 3, r.stderr);
assert.match(r.stderr, /the brake is on/);
assert.equal(stopRequested(journalDir), true, "the operator's STOP stays");
unlinkSync(stopPath(journalDir));
publish(journalDir, { pid: process.pid, at: "x" });
r = run(["run", "test-seat", "--supervised"]);
assert.equal(r.status, 3, r.stderr);
assert.match(r.stderr, /cannot be verified/);
assert.equal(existsSync(ownerPath(journalDir)), true, "a held binding is left alone");
assert.equal(stopRequested(journalDir), false);
r = run(["stop", "test-seat", "--supervised"]);
assert.equal(r.status, 4);
});