mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix(e2e): keep mac smoke commands bounded without timeout
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Crabbox: bootstrap raw AWS macOS JavaScript commands launched through `/usr/bin/env` so native mac runners without preinstalled Node, Corepack, or pnpm can still run wrapped Node and pnpm proof.
|
||||
- macOS: let app packaging fall back to `corepack pnpm` when a fresh native runner has Node/Corepack but no pnpm shim on `PATH`.
|
||||
- E2E: keep package/onboarding/plugin smoke commands bounded on macOS shells that have Node but no GNU `timeout` or `gtimeout` binary.
|
||||
|
||||
## 2026.5.26
|
||||
|
||||
|
||||
@@ -52,7 +52,83 @@ openclaw_e2e_maybe_timeout() {
|
||||
timeout_bin="gtimeout"
|
||||
fi
|
||||
if [ -z "$timeout_bin" ]; then
|
||||
echo "timeout or gtimeout is required for OpenClaw E2E command timeout $timeout_value" >&2
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
echo "timeout command not found; using Node watchdog for OpenClaw E2E command timeout $timeout_value" >&2
|
||||
node - "$timeout_value" "$@" <<'NODE'
|
||||
const [, , timeoutValue, command, ...args] = process.argv;
|
||||
const parseTimeoutMs = (value) => {
|
||||
const match = /^([0-9]+(?:\.[0-9]+)?)(ms|s|m|h)?$/u.exec(String(value ?? "").trim());
|
||||
if (!match) {
|
||||
throw new Error(`unsupported timeout value: ${value}`);
|
||||
}
|
||||
const amount = Number(match[1]);
|
||||
const unit = match[2] ?? "s";
|
||||
const multiplier = unit === "ms" ? 1 : unit === "s" ? 1_000 : unit === "m" ? 60_000 : 3_600_000;
|
||||
return Math.max(1, Math.ceil(amount * multiplier));
|
||||
};
|
||||
if (!command) {
|
||||
console.error("missing command for Node watchdog");
|
||||
process.exit(1);
|
||||
}
|
||||
const { spawn } = await import("node:child_process");
|
||||
let timeoutMs;
|
||||
try {
|
||||
timeoutMs = parseTimeoutMs(timeoutValue);
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
const child = spawn(command, args, {
|
||||
detached: process.platform !== "win32",
|
||||
stdio: "inherit",
|
||||
});
|
||||
let timedOut = false;
|
||||
const killTarget = process.platform === "win32" ? child.pid : -child.pid;
|
||||
const killChild = (signal) => {
|
||||
if (!child.pid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
process.kill(killTarget, signal);
|
||||
} catch {
|
||||
try {
|
||||
child.kill(signal);
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
console.error(`OpenClaw E2E command timed out after ${timeoutValue}`);
|
||||
killChild("SIGTERM");
|
||||
setTimeout(() => killChild("SIGKILL"), 30_000).unref();
|
||||
}, timeoutMs);
|
||||
const forwardSignal = (signal) => {
|
||||
killChild(signal);
|
||||
};
|
||||
process.once("SIGINT", forwardSignal);
|
||||
process.once("SIGTERM", forwardSignal);
|
||||
child.on("exit", (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
if (timedOut) {
|
||||
process.exit(124);
|
||||
}
|
||||
if (code !== null) {
|
||||
process.exit(code);
|
||||
}
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
clearTimeout(timer);
|
||||
console.error(error.message);
|
||||
process.exit(127);
|
||||
});
|
||||
NODE
|
||||
return
|
||||
fi
|
||||
echo "timeout command not found and Node is unavailable; cannot bound OpenClaw E2E command after $timeout_value" >&2
|
||||
return 127
|
||||
fi
|
||||
if "$timeout_bin" --kill-after=1s 1s true >/dev/null 2>&1; then
|
||||
|
||||
@@ -241,18 +241,18 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("fails package installs when no timeout binary is available", () => {
|
||||
it("uses the Node watchdog when timeout is unavailable", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-no-timeout-"));
|
||||
try {
|
||||
const npmArgsPath = path.join(tempDir, "npm-args.txt");
|
||||
const logPath = path.join(tempDir, "install.log");
|
||||
const packagePath = path.join(tempDir, "openclaw.tgz");
|
||||
const nodeBinDir = path.dirname(process.execPath);
|
||||
fs.writeFileSync(packagePath, "");
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, "npm"),
|
||||
["#!/bin/sh", "set -eu", 'printf "%s\\n" "$*" >"$OPENCLAW_TEST_NPM_ARGS"', ""].join("\n"),
|
||||
);
|
||||
fs.symlinkSync("/bin/cat", path.join(tempDir, "cat"));
|
||||
fs.chmodSync(path.join(tempDir, "npm"), 0o755);
|
||||
|
||||
const result = spawnSync(
|
||||
@@ -269,7 +269,7 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: tempDir,
|
||||
PATH: `${tempDir}:${nodeBinDir}`,
|
||||
OPENCLAW_CURRENT_PACKAGE_TGZ: packagePath,
|
||||
OPENCLAW_E2E_NPM_INSTALL_TIMEOUT: "42s",
|
||||
OPENCLAW_TEST_NPM_ARGS: npmArgsPath,
|
||||
@@ -277,15 +277,46 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain(
|
||||
"timeout or gtimeout is required for OpenClaw E2E command timeout 42s",
|
||||
expect(result.status).toBe(0);
|
||||
expect(fs.readFileSync(logPath, "utf8")).toContain("using Node watchdog");
|
||||
expect(fs.readFileSync(npmArgsPath, "utf8").trim()).toBe(
|
||||
`install -g ${packagePath} --no-fund --no-audit`,
|
||||
);
|
||||
expect(result.stderr).toContain("npm install failed for fixture package");
|
||||
expect(fs.readFileSync(logPath, "utf8")).toContain(
|
||||
"timeout or gtimeout is required for OpenClaw E2E command timeout 42s",
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("bounds commands with the Node watchdog when timeout is unavailable", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-node-watchdog-"));
|
||||
try {
|
||||
const nodeBinDir = path.dirname(process.execPath);
|
||||
const startedAt = Date.now();
|
||||
const result = spawnSync(
|
||||
"/bin/bash",
|
||||
[
|
||||
"-c",
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${shellQuote(helperPath)}`,
|
||||
`openclaw_e2e_maybe_timeout 200ms ${shellQuote(process.execPath)} -e ${shellQuote("setInterval(() => {}, 1000)")}`,
|
||||
].join("; "),
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${tempDir}:${nodeBinDir}`,
|
||||
},
|
||||
timeout: 5_000,
|
||||
},
|
||||
);
|
||||
expect(fs.existsSync(npmArgsPath)).toBe(false);
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
|
||||
expect(result.status).toBe(124);
|
||||
expect(elapsedMs).toBeLessThan(4_000);
|
||||
expect(result.stderr).toContain("using Node watchdog");
|
||||
expect(result.stderr).toContain("OpenClaw E2E command timed out after 200ms");
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user