mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 08:37:28 +00:00
test: replace slow gateway SIGTERM integration coverage
This commit is contained in:
@@ -91,6 +91,42 @@ function createRuntimeWithExitSignal(exitCallOrder?: string[]) {
|
||||
}
|
||||
|
||||
describe("runGatewayLoop", () => {
|
||||
it("exits 0 on SIGTERM after graceful close", async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
await withIsolatedSignals(async () => {
|
||||
const close = vi.fn(async () => {});
|
||||
let resolveStarted: (() => void) | null = null;
|
||||
const started = new Promise<void>((resolve) => {
|
||||
resolveStarted = resolve;
|
||||
});
|
||||
const start = vi.fn(async () => {
|
||||
resolveStarted?.();
|
||||
return { close };
|
||||
});
|
||||
const { runtime, exited } = createRuntimeWithExitSignal();
|
||||
|
||||
vi.resetModules();
|
||||
const { runGatewayLoop } = await import("./run-loop.js");
|
||||
const _loopPromise = runGatewayLoop({
|
||||
start: start as unknown as Parameters<typeof runGatewayLoop>[0]["start"],
|
||||
runtime: runtime as unknown as Parameters<typeof runGatewayLoop>[0]["runtime"],
|
||||
});
|
||||
|
||||
await started;
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
|
||||
process.emit("SIGTERM");
|
||||
|
||||
await expect(exited).resolves.toBe(0);
|
||||
expect(close).toHaveBeenCalledWith({
|
||||
reason: "gateway stopping",
|
||||
restartExpectedMs: null,
|
||||
});
|
||||
expect(runtime.exit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("restarts after SIGUSR1 even when drain times out, and resets lanes for the new iteration", async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
|
||||
@@ -1,161 +1,8 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
const waitForReady = async (
|
||||
proc: ReturnType<typeof spawn>,
|
||||
chunksOut: string[],
|
||||
chunksErr: string[],
|
||||
timeoutMs: number,
|
||||
) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
const stdout = chunksOut.join("");
|
||||
const stderr = chunksErr.join("");
|
||||
cleanup();
|
||||
reject(
|
||||
new Error(
|
||||
`timeout waiting for gateway to start\n` +
|
||||
`--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`,
|
||||
),
|
||||
);
|
||||
}, timeoutMs);
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer);
|
||||
proc.off("exit", onExit);
|
||||
proc.off("message", onMessage);
|
||||
proc.stdout?.off("data", onStdout);
|
||||
};
|
||||
|
||||
const onExit = () => {
|
||||
const stdout = chunksOut.join("");
|
||||
const stderr = chunksErr.join("");
|
||||
cleanup();
|
||||
reject(
|
||||
new Error(
|
||||
`gateway exited before ready (code=${String(proc.exitCode)} signal=${String(proc.signalCode)})\n` +
|
||||
`--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const onMessage = (msg: unknown) => {
|
||||
if (msg && typeof msg === "object" && "ready" in msg) {
|
||||
cleanup();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const onStdout = (chunk: unknown) => {
|
||||
if (String(chunk).includes("READY")) {
|
||||
cleanup();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
proc.once("exit", onExit);
|
||||
proc.on("message", onMessage);
|
||||
proc.stdout?.on("data", onStdout);
|
||||
});
|
||||
};
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
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 stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-gateway-test-"));
|
||||
const out: string[] = [];
|
||||
const err: string[] = [];
|
||||
|
||||
const nodeBin = process.execPath;
|
||||
const env = {
|
||||
...process.env,
|
||||
OPENCLAW_NO_RESPAWN: "1",
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
OPENCLAW_SKIP_CHANNELS: "1",
|
||||
OPENCLAW_SKIP_GMAIL_WATCHER: "1",
|
||||
OPENCLAW_SKIP_CRON: "1",
|
||||
OPENCLAW_SKIP_BROWSER_CONTROL_SERVER: "1",
|
||||
OPENCLAW_SKIP_CANVAS_HOST: "1",
|
||||
};
|
||||
const bootstrapPath = path.join(stateDir, "openclaw-entry-bootstrap.cjs");
|
||||
const runLoopPath = path.resolve("src/cli/gateway-cli/run-loop.ts");
|
||||
const jitiPath = require.resolve("jiti");
|
||||
fs.writeFileSync(
|
||||
bootstrapPath,
|
||||
[
|
||||
`const jiti = require(${JSON.stringify(jitiPath)})(__filename);`,
|
||||
`const { runGatewayLoop } = jiti(${JSON.stringify(runLoopPath)});`,
|
||||
"(async () => {",
|
||||
" await runGatewayLoop({",
|
||||
" start: async () => {",
|
||||
' process.stdout.write("READY\\\\n");',
|
||||
" if (process.send) process.send({ ready: true });",
|
||||
" const keepAlive = setInterval(() => {}, 1000);",
|
||||
" return { close: async () => clearInterval(keepAlive) };",
|
||||
" },",
|
||||
" runtime: { exit: (code) => process.exit(code) },",
|
||||
" });",
|
||||
"})().catch((err) => {",
|
||||
" console.error(err);",
|
||||
" process.exitCode = 1;",
|
||||
"});",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
const childArgs = [bootstrapPath];
|
||||
|
||||
child = spawn(nodeBin, childArgs, {
|
||||
cwd: process.cwd(),
|
||||
env,
|
||||
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
||||
});
|
||||
|
||||
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 waitForReady(proc, out, err, 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();
|
||||
it.skip("covered by runGatewayLoop signal tests in src/cli/gateway-cli/run-loop.test.ts", () => {
|
||||
// Kept as a placeholder to document why the old child-process integration
|
||||
// case was retired: it duplicated run-loop signal coverage at high runtime cost.
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user