From 0028c2f793fccb14af90bef1cc98dd7c2728c33f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 27 May 2026 01:10:50 +0200 Subject: [PATCH] fix(e2e): require bounded helper timeouts --- scripts/lib/openclaw-e2e-instance.sh | 5 +- test/scripts/openclaw-e2e-instance.test.ts | 76 ++++++++++++++++++++-- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/scripts/lib/openclaw-e2e-instance.sh b/scripts/lib/openclaw-e2e-instance.sh index 5dbb53030b7c..bff1fce96948 100644 --- a/scripts/lib/openclaw-e2e-instance.sh +++ b/scripts/lib/openclaw-e2e-instance.sh @@ -52,9 +52,8 @@ openclaw_e2e_maybe_timeout() { timeout_bin="gtimeout" fi if [ -z "$timeout_bin" ]; then - echo "timeout command not found; running OpenClaw E2E command without timeout $timeout_value" >&2 - "$@" - return + echo "timeout or gtimeout is required for OpenClaw E2E command timeout $timeout_value" >&2 + return 127 fi if "$timeout_bin" --kill-after=1s 1s true >/dev/null 2>&1; then "$timeout_bin" --kill-after=30s "$timeout_value" "$@" diff --git a/test/scripts/openclaw-e2e-instance.test.ts b/test/scripts/openclaw-e2e-instance.test.ts index 522a0dc3f0c5..69934701bb74 100644 --- a/test/scripts/openclaw-e2e-instance.test.ts +++ b/test/scripts/openclaw-e2e-instance.test.ts @@ -180,7 +180,68 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => { } }); - it("runs package installs without a wrapper when timeout is unavailable", () => { + it("uses gtimeout when GNU timeout is not on PATH", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-gtimeout-")); + try { + const timeoutArgsPath = path.join(tempDir, "timeout-args.txt"); + const npmArgsPath = path.join(tempDir, "npm-args.txt"); + const logPath = path.join(tempDir, "install.log"); + const packagePath = path.join(tempDir, "openclaw.tgz"); + fs.writeFileSync(packagePath, ""); + fs.writeFileSync( + path.join(tempDir, "gtimeout"), + [ + "#!/bin/bash", + "set -euo pipefail", + 'printf "%s\\n" "$*" >"$OPENCLAW_TEST_TIMEOUT_ARGS"', + 'while [ "$#" -gt 0 ] && [ "$1" != "npm" ]; do shift; done', + 'exec "$@"', + "", + ].join("\n"), + ); + fs.writeFileSync( + path.join(tempDir, "npm"), + ["#!/bin/sh", "set -eu", 'printf "%s\\n" "$*" >"$OPENCLAW_TEST_NPM_ARGS"', ""].join("\n"), + ); + fs.chmodSync(path.join(tempDir, "gtimeout"), 0o755); + fs.chmodSync(path.join(tempDir, "npm"), 0o755); + + const result = spawnSync( + "/bin/bash", + [ + "-c", + [ + "set -euo pipefail", + `source ${shellQuote(helperPath)}`, + `openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")}`, + ].join("; "), + ], + { + encoding: "utf8", + env: { + ...process.env, + PATH: tempDir, + OPENCLAW_CURRENT_PACKAGE_TGZ: packagePath, + OPENCLAW_E2E_NPM_INSTALL_TIMEOUT: "42s", + OPENCLAW_TEST_TIMEOUT_ARGS: timeoutArgsPath, + OPENCLAW_TEST_NPM_ARGS: npmArgsPath, + }, + }, + ); + + expect(result.status).toBe(0); + expect(fs.readFileSync(timeoutArgsPath, "utf8").trim()).toBe( + `--kill-after=30s 42s npm install -g ${packagePath} --no-fund --no-audit`, + ); + expect(fs.readFileSync(npmArgsPath, "utf8").trim()).toBe( + `install -g ${packagePath} --no-fund --no-audit`, + ); + } finally { + fs.rmSync(tempDir, { force: true, recursive: true }); + } + }); + + it("fails package installs when no timeout binary is available", () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-e2e-instance-no-timeout-")); try { const npmArgsPath = path.join(tempDir, "npm-args.txt"); @@ -191,6 +252,7 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => { 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( @@ -215,11 +277,15 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => { }, ); - expect(result.status).toBe(0); - expect(fs.readFileSync(logPath, "utf8")).toContain("timeout command not found"); - expect(fs.readFileSync(npmArgsPath, "utf8").trim()).toBe( - `install -g ${packagePath} --no-fund --no-audit`, + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "timeout or gtimeout is required for OpenClaw E2E command timeout 42s", ); + 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", + ); + expect(fs.existsSync(npmArgsPath)).toBe(false); } finally { fs.rmSync(tempDir, { force: true, recursive: true }); }