import test from "node:test"; import assert from "node:assert/strict"; import { EventEmitter } from "node:events"; // #4425: the supervisor now waits for the listen port to free up before respawning. // Point that probe at the no-op port 0 so the restart tests don't open real sockets. process.env.PORT = "0"; // Stub para o processSupervisor: testa a lógica de restart/backoff/MITM sem processos reais. class StubChild extends EventEmitter { pid = 99999; stderr = new EventEmitter(); killed = false; kill(_sig?: string) { this.killed = true; } } function makeChildFactory(exitCodes: (number | null)[]) { let calls = 0; return () => { const child = new StubChild(); const code = exitCodes[calls++] ?? null; // Emite exit no próximo tick para simular processo assíncrono setImmediate(() => child.emit("exit", code)); return child; }; } // --- detectMitmCrash --- test("detectMitmCrash retorna true quando >=2 sinais MITM presentes", async () => { const { detectMitmCrash } = await import("../../bin/cli/runtime/processSupervisor.mjs"); assert.ok(detectMitmCrash(["mitm proxy failed", "certificate error in tls socket"])); assert.ok(detectMitmCrash(["TLS Socket closed", "certificate invalid"])); }); test("detectMitmCrash retorna false com menos de 2 sinais", async () => { const { detectMitmCrash } = await import("../../bin/cli/runtime/processSupervisor.mjs"); assert.ok(!detectMitmCrash(["certificate error"])); assert.ok(!detectMitmCrash(["generic error"])); assert.ok(!detectMitmCrash([])); }); // --- ServerSupervisor: lógica de restart --- test("ServerSupervisor.handleExit com code=0 espontâneo reinicia em vez de sair (#4425)", async () => { const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs"); // waitUntilPortFree no-ops on port 0, so the scheduled restart fires fast and leaks no timer. const origPort = process.env.PORT; process.env.PORT = "0"; const exits: number[] = []; const origExit = process.exit.bind(process); // @ts-ignore process.exit = (code?: number) => { exits.push(code ?? 0); }; const supervisor = new ServerSupervisor({ serverPath: "/fake/server.js", env: {}, maxRestarts: 2, }); let started = 0; // Stub start() so the scheduled restart never spawns the fake server. supervisor.start = () => { started++; return null as any; }; supervisor.handleExit(0); // #4425: a spontaneous code-0 exit (e.g. a systemd MemoryMax cgroup kill, which reports // a clean exit) must NOT terminate the supervisor — it schedules a restart instead. assert.equal(exits.length, 0, "must not process.exit on a spontaneous code-0 exit"); assert.equal(supervisor.restartCount, 1); // Let the scheduled restart fire so no timer leaks past the test. await new Promise((r) => setTimeout(r, 1100)); assert.equal(started, 1, "restart should fire after the backoff delay"); // @ts-ignore process.exit = origExit; if (origPort === undefined) delete process.env.PORT; else process.env.PORT = origPort; }); test("ServerSupervisor.handleExit com isShuttingDown=true chama process.exit imediato", async () => { const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs"); const exits: number[] = []; const origExit = process.exit.bind(process); // @ts-ignore process.exit = (code?: number) => exits.push(code ?? 0); const supervisor = new ServerSupervisor({ serverPath: "/fake/server.js", env: {}, maxRestarts: 2, }); supervisor.isShuttingDown = true; supervisor.handleExit(1); // @ts-ignore process.exit = origExit; assert.equal(exits[0], 1); }); test("ServerSupervisor.handleExit incrementa restartCount e chama start() após delay", async () => { const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs"); let startCalls = 0; const supervisor = new ServerSupervisor({ serverPath: "/fake/server.js", env: {}, maxRestarts: 5, }); supervisor.start = () => { startCalls++; return null as any; }; supervisor.startedAt = Date.now() - 100; // viveu <30s supervisor.handleExit(1); assert.equal(supervisor.restartCount, 1); await new Promise((r) => setTimeout(r, 1100)); // aguarda o delay de 1s assert.equal(startCalls, 1); }); test("ServerSupervisor.handleExit exibe crash log ao reiniciar", async () => { const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs"); const logs: string[] = []; const origErr = console.error.bind(console); console.error = (...args: unknown[]) => logs.push(args.join(" ")); const supervisor = new ServerSupervisor({ serverPath: "/fake/server.js", env: {}, maxRestarts: 5, }); supervisor.start = () => null as any; supervisor.startedAt = Date.now() - 100; supervisor.crashLog = ["line1", "line2"]; supervisor.handleExit(1); console.error = origErr; assert.ok(logs.some((l) => l.includes("line1") || l.includes("crash log"))); await new Promise((r) => setTimeout(r, 1100)); // drain the scheduled restart timer }); test("ServerSupervisor chama onCrashCallback após maxRestarts atingido", async () => { const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs"); let callbackCalled = false; const exits: number[] = []; const origExit = process.exit.bind(process); // @ts-ignore process.exit = (code?: number) => exits.push(code ?? 0); const supervisor = new ServerSupervisor({ serverPath: "/fake/server.js", env: {}, maxRestarts: 2, onCrashCallback: (log: string[]) => { callbackCalled = true; return null; }, }); supervisor.restartCount = 2; // já no limite supervisor.startedAt = Date.now() - 100; supervisor.handleExit(1); // @ts-ignore process.exit = origExit; assert.ok(callbackCalled); assert.equal(exits[0], 1); }); test("ServerSupervisor retorna 'disable-mitm-and-retry' chama start() novamente", async () => { const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs"); let startCalls = 0; const supervisor = new ServerSupervisor({ serverPath: "/fake/server.js", env: {}, maxRestarts: 2, onCrashCallback: () => "disable-mitm-and-retry", }); supervisor.start = () => { startCalls++; return null as any; }; supervisor.restartCount = 2; supervisor.startedAt = Date.now() - 100; supervisor.handleExit(1); assert.equal(startCalls, 1); assert.equal(supervisor.restartCount, 0); // foi resetado }); test("ServerSupervisor reseta restartCount após viver >= RESTART_RESET_MS (#4425: 60s)", async () => { const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs"); const supervisor = new ServerSupervisor({ serverPath: "/fake/server.js", env: {}, maxRestarts: 2, }); supervisor.start = () => null as any; supervisor.restartCount = 2; supervisor.startedAt = Date.now() - 61_000; // #4425: reset window bumped 30s→60s supervisor.handleExit(1); assert.equal(supervisor.restartCount, 1); // reset p/ 0, depois incrementado p/ 1 await new Promise((r) => setTimeout(r, 1100)); // drain the scheduled restart timer }); // --- Node.js v24 compat: process.exit() must receive a number (#3748) --- test("ServerSupervisor.handleExit com string code não passa string para process.exit (#3748)", async () => { const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs"); const exits: Array = []; const origExit = process.exit.bind(process); // @ts-ignore process.exit = (code?: number | string) => exits.push(code); const supervisor = new ServerSupervisor({ serverPath: "/fake/server.js", env: {}, maxRestarts: 0, }); // Simulates the 'error' event on child spawn failure: err.code = 'ENOENT' (string, not number). // maxRestarts=0 → restartCount(0) >= maxRestarts(0) → process.exit() is called immediately. supervisor.startedAt = Date.now() - 100; supervisor.handleExit("ENOENT" as any); // @ts-ignore process.exit = origExit; assert.equal(exits.length, 1, "process.exit deve ser chamado exatamente 1 vez"); assert.equal( typeof exits[0], "number", `process.exit deve receber number, recebeu: ${typeof exits[0]} (${exits[0]})` ); }); // --- pid.mjs multi-service --- test("writePidFile/readPidFile/cleanupPidFile operam por service", async () => { const os = await import("node:os"); const tmpDir = os.default.tmpdir() + "/omniroute-pid-test-" + Date.now(); process.env.DATA_DIR = tmpDir; const { writePidFile, readPidFile, cleanupPidFile } = await import("../../bin/cli/utils/pid.mjs"); writePidFile("server", 12345); assert.equal(readPidFile("server"), 12345); writePidFile("mitm", 99999); assert.equal(readPidFile("mitm"), 99999); // Services são independentes assert.equal(readPidFile("server"), 12345); cleanupPidFile("server"); assert.equal(readPidFile("server"), null); assert.equal(readPidFile("mitm"), 99999); // mitm não foi afetado cleanupPidFile("mitm"); delete process.env.DATA_DIR; });