dev / templates / bot / test / worker-respawn.test.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 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 — join-worker-room resolves the worker as a sibling
// of process.argv[1], which under `node --test` is this test file).
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("../dist/main.js", import.meta.url).href
);

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" };
  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" };
  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();
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/dev.git