Files
clawdbot/src/cli/gateway.sigterm.test.ts
2026-01-19 16:35:45 +00:00

172 lines
5.3 KiB
TypeScript

import { spawn } from "node:child_process";
import fs from "node:fs";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
const waitForPortOpen = async (
proc: ReturnType<typeof spawn>,
chunksOut: string[],
chunksErr: string[],
port: number,
timeoutMs: number,
) => {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
if (proc.exitCode !== null) {
const stdout = chunksOut.join("");
const stderr = chunksErr.join("");
throw new Error(
`gateway exited before listening (code=${String(proc.exitCode)} signal=${String(proc.signalCode)})\n` +
`--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`,
);
}
try {
await new Promise<void>((resolve, reject) => {
const socket = net.connect({ host: "127.0.0.1", port });
socket.once("connect", () => {
socket.destroy();
resolve();
});
socket.once("error", (err) => {
socket.destroy();
reject(err);
});
});
return;
} catch {
// keep polling
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
const stdout = chunksOut.join("");
const stderr = chunksErr.join("");
throw new Error(
`timeout waiting for gateway to listen on port ${port}\n` +
`--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`,
);
};
const getFreePort = async () => {
const srv = net.createServer();
await new Promise<void>((resolve) => srv.listen(0, "127.0.0.1", resolve));
const addr = srv.address();
if (!addr || typeof addr === "string") {
srv.close();
throw new Error("failed to bind ephemeral port");
}
await new Promise<void>((resolve) => srv.close(() => resolve()));
return addr.port;
};
describe("gateway SIGTERM", () => {
let child: ReturnType<typeof spawn> | null = null;
afterEach(() => {
if (!child || child.killed) return;
try {
child.kill("SIGKILL");
} catch {
// ignore
}
child = null;
});
it("exits 0 on SIGTERM", { timeout: 180_000 }, async () => {
const port = await getFreePort();
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-gateway-test-"));
const configPath = path.join(stateDir, "clawdbot.json");
fs.writeFileSync(
configPath,
JSON.stringify({ gateway: { mode: "local", port } }, null, 2),
"utf8",
);
const out: string[] = [];
const err: string[] = [];
const nodeBin = process.execPath;
const entryArgs = [
"gateway",
"--port",
String(port),
"--bind",
"loopback",
"--allow-unconfigured",
];
const env = {
...process.env,
CLAWDBOT_NO_RESPAWN: "1",
CLAWDBOT_STATE_DIR: stateDir,
CLAWDBOT_CONFIG_PATH: configPath,
CLAWDBOT_SKIP_CHANNELS: "1",
CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1",
CLAWDBOT_SKIP_CANVAS_HOST: "1",
// Avoid port collisions with other test processes that may also start a gateway server.
CLAWDBOT_BRIDGE_HOST: "127.0.0.1",
CLAWDBOT_BRIDGE_PORT: "0",
};
const bootstrapPath = path.join(stateDir, "clawdbot-entry-bootstrap.mjs");
const runMainPath = path.resolve("src/cli/run-main.ts");
fs.writeFileSync(
bootstrapPath,
[
'import { pathToFileURL } from "node:url";',
'const rawArgs = process.env.CLAWDBOT_ENTRY_ARGS ?? "[]";',
"let entryArgs = [];",
"try {",
" entryArgs = JSON.parse(rawArgs);",
"} catch (err) {",
' console.error("Failed to parse CLAWDBOT_ENTRY_ARGS", err);',
" process.exit(1);",
"}",
"if (!Array.isArray(entryArgs)) entryArgs = [];",
'entryArgs = entryArgs.filter((arg) => typeof arg === "string" && !arg.toLowerCase().includes("node.exe"));',
`const runMainUrl = ${JSON.stringify(pathToFileURL(runMainPath).href)};`,
"const { runCli } = await import(runMainUrl);",
'await runCli(["node", "clawdbot", ...entryArgs]);',
].join("\n"),
"utf8",
);
const childArgs = ["--import", "tsx", bootstrapPath];
env.CLAWDBOT_ENTRY_ARGS = JSON.stringify(entryArgs);
child = spawn(nodeBin, childArgs, {
cwd: process.cwd(),
env,
stdio: ["ignore", "pipe", "pipe"],
});
const proc = child;
if (!proc) throw new Error("failed to spawn gateway");
child.stdout?.setEncoding("utf8");
child.stderr?.setEncoding("utf8");
child.stdout?.on("data", (d) => out.push(String(d)));
child.stderr?.on("data", (d) => err.push(String(d)));
await waitForPortOpen(proc, out, err, port, 150_000);
proc.kill("SIGTERM");
const result = await new Promise<{
code: number | null;
signal: NodeJS.Signals | null;
}>((resolve) => proc.once("exit", (code, signal) => resolve({ code, signal })));
if (result.code !== 0 && !(result.code === null && result.signal === "SIGTERM")) {
const stdout = out.join("");
const stderr = err.join("");
throw new Error(
`expected exit code 0, got code=${String(result.code)} signal=${String(result.signal)}\n` +
`--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`,
);
}
if (result.code === null && result.signal === "SIGTERM") return;
expect(result.signal).toBeNull();
});
});