279 lines
9.0 KiB
TypeScript
279 lines
9.0 KiB
TypeScript
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<number | string | undefined> = [];
|
|
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;
|
|
});
|