Ctrl K
ComunidadGuías…

electron/environment-switch.node-test.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import { DataDirLeaseError } from "./data-dir-lease.mjs";
import { reviveAfterFailedSwitch, switchLocalEnvironment, validateTargetDir } from "./environment-switch.mjs";

function realDir() {
  const dir = mkdtempSync(join(tmpdir(), "omb-switch-"));
  return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
}

function makeDeps(overrides = {}) {
  const calls = [];
  const args = {};
  let starts = 0;
  const record = (name) => (...rest) => {
    calls.push(name);
    (args[name] ??= []).push(rest);
  };
  const deps = {
    stopChild: record("stop"),
    releaseLease: record("release"),
    acquireLease: (dir) => {
      record("acquire")(dir);
      return { dir };
    },
    createDir: record("create"),
    startChild: (port) => {
      calls.push("start");
      (args.start ??= []).push([port]);
      starts += 1;
      return { pid: 4000 + starts, port };
    },
    probeReady: (port, pid) => {
      calls.push("probe");
      (args.probe ??= []).push([port, pid]);
      return "ready";
    },
    rollback: record("rollback"),
    persistActive: record("persist"),
    log: () => {},
  };
  return { calls, args, deps: Object.assign(deps, overrides) };
}

test("happy path: calls run in switch order and persistActive is last", async (t) => {
  const { dir, cleanup } = realDir();
  t.after(cleanup);
  const { calls, args, deps } = makeDeps();
  const result = await switchLocalEnvironment({ targetDir: dir, targetId: "env-2", port: 9000, deps });
  assert.deepEqual(result, { ok: true });
  assert.deepEqual(calls, ["stop", "release", "acquire", "start", "probe", "persist"]);
  assert.deepEqual(args.acquire, [[dir]]);
  assert.deepEqual(args.start, [[9000]]);
  assert.deepEqual(args.probe, [[9000, 4001]]);
  assert.deepEqual(args.persist, [["env-2"]]);
  assert.equal(args.rollback, undefined);
});

test("missing parent dir: aborts with unavailable, rolls back, never persists", async (t) => {
  const { dir, cleanup } = realDir();
  t.after(cleanup);
  const { calls, args, deps } = makeDeps();
  const result = await switchLocalEnvironment({ targetDir: join(dir, "gone", ".openmausbot-x"), deps });
  assert.deepEqual(result, { ok: false, error: "unavailable", rolledBack: true });
  assert.deepEqual(calls, ["stop", "release", "rollback"]);
  assert.deepEqual(args.rollback, [[8799]]);
  assert.equal(args.persist, undefined);
  assert.equal(args.start, undefined);
});

test("lease held elsewhere: DataDirLeaseError becomes locked, old lease reacquired, no persist", async (t) => {
  const { dir, cleanup } = realDir();
  t.after(cleanup);
  const { calls, deps } = makeDeps({
    acquireLease: (targetDir) => {
      calls.push("acquire");
      throw new DataDirLeaseError(`OpenMausBot is already using this data directory (process 123). ${targetDir}`);
    },
  });
  const result = await switchLocalEnvironment({ targetDir: dir, targetId: "env-2", deps });
  assert.deepEqual(result, { ok: false, error: "locked", rolledBack: true });
  assert.deepEqual(calls, ["stop", "release", "acquire", "rollback"]);
  assert.ok(!calls.includes("persist"));
  assert.ok(!calls.includes("start"));
});

test("needsCreate target: createDir runs after release and before acquire", async (t) => {
  const { dir, cleanup } = realDir();
  t.after(cleanup);
  const { calls, args, deps } = makeDeps();
  const targetDir = join(dir, ".openmausbot-new");
  const result = await switchLocalEnvironment({ targetDir, targetId: "env-3", deps });
  assert.deepEqual(result, { ok: true });
  assert.deepEqual(calls, ["stop", "release", "create", "acquire", "start", "probe", "persist"]);
  assert.deepEqual(args.create, [[targetDir]]);
});

test("probe reports exited: rollback restarts old child exactly once, no double start", async (t) => {
  const { dir, cleanup } = realDir();
  t.after(cleanup);
  let startCount = 0;
  let stopCount = 0;
  const calls = [];
  const deps = {
    stopChild: () => {
      calls.push("stop");
      stopCount += 1;
    },
    releaseLease: () => calls.push("release"),
    acquireLease: () => {
      calls.push("acquire");
      return {};
    },
    createDir: () => calls.push("create"),
    startChild: (port) => {
      calls.push("start");
      startCount += 1;
      return { pid: 5000 + startCount, port };
    },
    probeReady: () => {
      calls.push("probe");
      return "exited";
    },
    rollback: (port) => {
      calls.push("rollback");
      deps.startChild(port);
    },
    persistActive: () => calls.push("persist"),
    log: () => {},
  };
  const result = await switchLocalEnvironment({ targetDir: dir, targetId: "env-4", deps });
  assert.equal(result.ok, false);
  assert.equal(result.rolledBack, true);
  assert.ok(!calls.includes("persist"));
  assert.equal(startCount, 2);
  assert.equal(stopCount, 1);
  assert.deepEqual(calls, ["stop", "release", "acquire", "start", "probe", "rollback", "start"]);
});

test("validateTargetDir: missing parent is unavailable", () => {
  const fsImpl = { existsSync: (p) => !p.includes("gone") };
  assert.deepEqual(validateTargetDir("/volumes/gone/.openmausbot-x", fsImpl), { ok: false, error: "unavailable" });
});

test("validateTargetDir: absent directory with live parent needs create", () => {
  const fsImpl = { existsSync: (p) => p === "/volumes/live" };
  assert.deepEqual(validateTargetDir("/volumes/live/.openmausbot-x", fsImpl), { ok: true, needsCreate: true });
});

test("validateTargetDir: existing directory needs no create", () => {
  const fsImpl = { existsSync: () => true };
  assert.deepEqual(validateTargetDir("/volumes/live/.openmausbot-x", fsImpl), { ok: true, needsCreate: false });
});

test("validateTargetDir: default fs sees real temp directories", (t) => {
  const { dir, cleanup } = realDir();
  t.after(cleanup);
  assert.deepEqual(validateTargetDir(dir), { ok: true, needsCreate: false });
  assert.deepEqual(validateTargetDir(join(dir, "child")), { ok: true, needsCreate: true });
  assert.deepEqual(validateTargetDir(join(dir, "gone", "child")), { ok: false, error: "unavailable" });
});

test("persistActive failure rolls back and reports persist-failed, never success", async (t) => {
  const { dir, cleanup } = realDir();
  t.after(cleanup);
  const calls = [];
  const deps = {
    stopChild: () => { calls.push("stop"); },
    releaseLease: () => { calls.push("release"); },
    acquireLease: () => { calls.push("acquire"); return {}; },
    createDir: () => { calls.push("create"); },
    startChild: () => { calls.push("start"); return { pid: 6001, port: 8799 }; },
    probeReady: () => { calls.push("probe"); return "ready"; },
    rollback: () => { calls.push("rollback"); },
    persistActive: () => { calls.push("persist"); throw new Error("disk full"); },
    log: () => {},
  };
  const result = await switchLocalEnvironment({ targetDir: dir, targetId: "env-5", deps });
  assert.deepEqual(result, { ok: false, error: "persist-failed", rolledBack: true });
  assert.deepEqual(calls, ["stop", "release", "acquire", "start", "probe", "persist", "rollback"]);
});

test("a rollback that fails reports rolledBack:false", async (t) => {
  const { dir, cleanup } = realDir();
  t.after(cleanup);
  const calls = [];
  const deps = {
    stopChild: () => { calls.push("stop"); },
    releaseLease: () => { calls.push("release"); },
    acquireLease: () => { calls.push("acquire"); throw new Error("held"); },
    createDir: () => { calls.push("create"); },
    startChild: () => { calls.push("start"); return { pid: 6002, port: 8799 }; },
    probeReady: () => { calls.push("probe"); return "ready"; },
    rollback: () => { calls.push("rollback"); throw new Error("rollback boom"); },
    persistActive: () => { calls.push("persist"); },
    log: () => {},
  };
  const result = await switchLocalEnvironment({ targetDir: dir, targetId: "env-6", deps });
  assert.deepEqual(result, { ok: false, error: "locked", rolledBack: false });
  assert.ok(calls.includes("rollback"));
  assert.ok(!calls.includes("persist"));
});

test("stopChild throwing aborts the switch before release or acquire", async (t) => {
  const { dir, cleanup } = realDir();
  t.after(cleanup);
  const calls = [];
  const deps = {
    stopChild: () => { calls.push("stop"); throw new Error("the server child did not stop"); },
    releaseLease: () => calls.push("release"),
    acquireLease: () => calls.push("acquire"),
    createDir: () => calls.push("create"),
    startChild: () => calls.push("start"),
    probeReady: () => calls.push("probe"),
    rollback: () => calls.push("rollback"),
    persistActive: () => calls.push("persist"),
    log: () => {},
  };
  await assert.rejects(() => switchLocalEnvironment({ targetDir: dir, targetId: "env-7", deps }), /did not stop/);
  assert.deepEqual(calls, ["stop"]);
});

function reviveDeps({ held = false, acquireFailures = 0, startResult = { pid: 1 } } = {}) {
  const calls = [];
  let lease = held;
  let failures = acquireFailures;
  return {
    calls,
    deps: {
      previousDir: "/data/previous",
      hasLease: () => lease,
      acquireLease: async (dir) => {
        calls.push(`acquire ${dir}`);
        if (failures-- > 0) throw new DataDirLeaseError("held elsewhere");
        lease = true;
      },
      startChild: async (dir) => { calls.push(`start ${dir}`); return startResult; },
      log: () => {},
      sleep: async (ms) => { calls.push(`sleep ${ms}`); },
    },
  };
}

test("revive takes the previous folder's lease back before starting when a rollback left none", async () => {
  const { calls, deps } = reviveDeps();
  assert.equal(await reviveAfterFailedSwitch(deps), true);
  assert.deepEqual(calls, ["acquire /data/previous", "start /data/previous"]);
});

test("revive starts on the previous folder without a new lease when one is still held", async () => {
  const { calls, deps } = reviveDeps({ held: true });
  assert.equal(await reviveAfterFailedSwitch(deps), true);
  assert.deepEqual(calls, ["start /data/previous"]);
});

test("revive retries a lease that is briefly unavailable, then gives up without forcing it", async () => {
  const brief = reviveDeps({ acquireFailures: 2 });
  assert.equal(await reviveAfterFailedSwitch(brief.deps), true);
  assert.deepEqual(brief.calls, ["acquire /data/previous", "sleep 1000", "acquire /data/previous", "sleep 2000", "acquire /data/previous", "start /data/previous"]);
  const held = reviveDeps({ acquireFailures: 99 });
  assert.equal(await reviveAfterFailedSwitch(held.deps), false);
  assert.equal(held.calls.filter((call) => call.startsWith("acquire")).length, 4);
  assert.equal(held.calls.some((call) => call.startsWith("start")), false);
});

test("revive does not retry a start that returned no child", async () => {
  const { calls, deps } = reviveDeps({ held: true, startResult: null });
  assert.equal(await reviveAfterFailedSwitch(deps), false);
  assert.deepEqual(calls, ["start /data/previous"]);
});