Files
openclaw/test/scripts/plugin-lifecycle-measure.test.ts
2026-05-26 21:22:01 +02:00

177 lines
5.4 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
const tempDirs: string[] = [];
const scriptPath = "scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs";
const hasTimeoutCommand =
process.platform === "linux" &&
spawnSync("bash", ["-lc", "command -v timeout >/dev/null 2>&1"]).status === 0;
function makeTempDir(): string {
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-lifecycle-measure-"));
tempDirs.push(dir);
return dir;
}
function pidExists(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
function waitForPidExit(pid: number, timeoutMs: number): boolean {
const waitBuffer = new SharedArrayBuffer(4);
const waitView = new Int32Array(waitBuffer);
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (!pidExists(pid)) {
return true;
}
Atomics.wait(waitView, 0, 0, 25);
}
return !pidExists(pid);
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
describe("plugin lifecycle resource sampler", () => {
it("configures a phase timeout with process-group cleanup", () => {
const script = readFileSync(scriptPath, "utf8");
expect(script).toContain("OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS");
expect(script).toContain("OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS");
expect(script).toContain("detached: true");
expect(script).toContain('process.kill(-child.pid, signal)');
expect(script).toContain('const summarySignal = timedOut ? "timeout"');
expect(script).toContain("process.exit(124)");
});
it.runIf(process.platform === "linux")(
"times out wedged phases and records the timeout signal",
() => {
const dir = makeTempDir();
const summary = path.join(dir, "summary.tsv");
const result = spawnSync(
"node",
[scriptPath, summary, "wedged", "--", "node", "-e", "setInterval(() => {}, 1000)"],
{
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "150",
OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "50",
},
timeout: 5000,
},
);
expect(result.status).toBe(124);
expect(result.stdout).toContain("signal=timeout");
expect(readFileSync(summary, "utf8")).toMatch(/^wedged\t\d+\t[\d.]+\t\d+\t[\d.]+\ttimeout$/mu);
},
);
it.runIf(process.platform === "linux")(
"kills stubborn descendants after the timeout grace period",
() => {
const dir = makeTempDir();
const summary = path.join(dir, "summary.tsv");
const pidFile = path.join(dir, "descendant.pid");
let descendantPid = 0;
try {
const result = spawnSync(
"node",
[
scriptPath,
summary,
"stubborn-descendant",
"--",
"bash",
"-lc",
'bash -c \'trap "" TERM; printf "%s\\n" "$$" >"$PID_FILE"; while :; do sleep 1; done\' & wait',
],
{
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "150",
OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "100",
PID_FILE: pidFile,
},
timeout: 5000,
},
);
descendantPid = Number.parseInt(readFileSync(pidFile, "utf8"), 10);
expect(result.status).toBe(124);
expect(result.stdout).toContain("signal=timeout");
expect(readFileSync(summary, "utf8")).toMatch(
/^stubborn-descendant\t\d+\t[\d.]+\t\d+\t[\d.]+\ttimeout$/mu,
);
expect(waitForPidExit(descendantPid, 1000)).toBe(true);
} finally {
if (descendantPid > 0 && pidExists(descendantPid)) {
process.kill(descendantPid, "SIGKILL");
}
}
},
);
it.runIf(hasTimeoutCommand)("forwards external termination to the measured process group", () => {
const dir = makeTempDir();
const summary = path.join(dir, "summary.tsv");
const pidFile = path.join(dir, "descendant.pid");
let descendantPid = 0;
try {
const result = spawnSync(
"timeout",
[
"--kill-after=1s",
"0.2s",
"node",
scriptPath,
summary,
"external-stop",
"--",
"bash",
"-lc",
'bash -c \'trap "" TERM; printf "%s\\n" "$$" >"$PID_FILE"; while :; do sleep 1; done\' & wait',
],
{
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "5000",
OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "100",
PID_FILE: pidFile,
},
timeout: 5000,
},
);
descendantPid = Number.parseInt(readFileSync(pidFile, "utf8"), 10);
expect(result.status).toBe(124);
expect(waitForPidExit(descendantPid, 1000)).toBe(true);
} finally {
if (descendantPid > 0 && pidExists(descendantPid)) {
process.kill(descendantPid, "SIGKILL");
}
}
});
});