// The respawn race: a worker crash schedules a backoff respawn; a close()
// inside that window must cancel it — no orphan worker may ever be forked.
// Runs against the committed dist. The forked target is test/worker.js (a stub
// beside this file), named through cfg.workerPath — the override seam
// join-worker-room checks before deriving stack-b/dist/worker.js from the
// running entry script. The derivation ITSELF is gated in
// deploy/check-dist.sh, against both deployment shapes; what this file is
// about is the respawn/close race, and a stub is the only way to crash a
// worker on demand.
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
const { joinWorkerRoom } = await import(
new URL("../stack-a/dist/main.js", import.meta.url).href
);
const WORKER = new URL("./worker.js", import.meta.url).pathname;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function waitForSpawns(logFile, n, timeoutMs) {
const t0 = Date.now();
for (;;) {
const lines = existsSync(logFile)
? readFileSync(logFile, "utf8").split("\n").filter(Boolean)
: [];
if (lines.length >= n) return lines;
if (Date.now() - t0 > timeoutMs) return lines;
await sleep(50);
}
}
test("close() during the respawn backoff forks no orphan worker", async () => {
const dataDir = mkdtempSync(join(tmpdir(), "bot-respawn-"));
const cfg = { dataDir, origin: "https://ardegazu.ro", workerPath: WORKER };
const log = () => {};
// job.dataDir = <cfg.dataDir>/rooms/<key with | -> ->
const key = "chat|deadbeefcafe0000";
const logFile = join(dataDir, "rooms", "chat-deadbeefcafe0000", "spawn.log");
const handle = await joinWorkerRoom(
cfg, "chat", "secret", "crash", "nick", key, "human", log
);
// spawn #1 runs and crashes (exit 1) → the exit handler schedules the
// respawn at backoffDelay(1) = 4000 ms
const first = await waitForSpawns(logFile, 1, 3000);
assert.equal(first.length, 1, "the first spawn must have happened");
await sleep(300); // let the exit handler run and arm the timer
await handle.close(); // inside the 4 s backoff window
await sleep(4800); // ride out the full backoff
const after = await waitForSpawns(logFile, 1, 0);
assert.equal(
after.length, 1,
`respawn fired after close() — orphan worker forked (${after.length} spawns)`
);
});
test("respawn still happens when the room stays open", async () => {
const dataDir = mkdtempSync(join(tmpdir(), "bot-respawn-"));
const cfg = { dataDir, origin: "https://ardegazu.ro", workerPath: WORKER };
const key = "chat|deadbeefcafe0001";
const logFile = join(dataDir, "rooms", "chat-deadbeefcafe0001", "spawn.log");
const handle = await joinWorkerRoom(
cfg, "chat", "secret", "crash", "nick", key, "human", () => {}
);
// crash → backoff 4 s → respawn #2 (which also crashes; close before #3)
const lines = await waitForSpawns(logFile, 2, 8000);
assert.equal(lines.length, 2, "the backoff respawn must still fire");
await handle.close();
});