From a0d2febe6bf4d114e34224856ee007bcba9d8fc1 Mon Sep 17 00:00:00 2001 From: Evan Newman Date: Sun, 31 May 2026 06:59:12 -0230 Subject: [PATCH] fix(scripts): timeout crabbox wrapper sanity checks Add bounded timeouts for Crabbox wrapper sanity probes so a stale or hung selected binary cannot block the wrapper indefinitely. The wrapper now maps timed-out sanity probes to a deterministic failure and keeps provider/help parsing behavior intact. Also add regression coverage for a binary whose `--version` probe hangs while `run --help` still responds. Co-authored-by: Evan Newman Co-authored-by: Peter Steinberger --- scripts/crabbox-wrapper.mjs | 5 ++++- test/scripts/crabbox-wrapper.test.ts | 33 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/scripts/crabbox-wrapper.mjs b/scripts/crabbox-wrapper.mjs index a9496a07996..b33ec2c880c 100755 --- a/scripts/crabbox-wrapper.mjs +++ b/scripts/crabbox-wrapper.mjs @@ -175,9 +175,12 @@ function checkedOutput(command, commandArgs) { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsVerbatimArguments: invocation.windowsVerbatimArguments, + timeout: 5_000, + killSignal: "SIGKILL", }); + const timedOut = result.error?.name === "Error" && result.signal === "SIGKILL"; return { - status: result.status ?? 1, + status: timedOut ? 124 : (result.status ?? 1), text: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim(), stdout: (result.stdout ?? "").trim(), }; diff --git a/test/scripts/crabbox-wrapper.test.ts b/test/scripts/crabbox-wrapper.test.ts index 468ec2fa6cd..e3150b94860 100644 --- a/test/scripts/crabbox-wrapper.test.ts +++ b/test/scripts/crabbox-wrapper.test.ts @@ -137,6 +137,27 @@ function writeFakeCrabbox(binDir: string, helpText: string): string { return crabboxPath; } +function makeSlowVersionCrabbox(helpText: string): string { + const binDir = mkdtempSync(path.join(tmpdir(), "openclaw-slow-crabbox-")); + tempDirs.push(binDir); + const crabboxPath = path.join(binDir, "crabbox"); + + const script = [ + "#!/usr/bin/env node", + "const args = process.argv.slice(2);", + 'if (args[0] === "--version") { setTimeout(() => process.exit(0), 6_000); }', + `else if (args[0] === "run" && args[1] === "--help") { process.stdout.write(${JSON.stringify(helpText)}); }`, + ].join("\n"); + writeFileSync(crabboxPath, `${script}\n`, "utf8"); + writeFileSync( + `${crabboxPath}.cmd`, + `@echo off\r\n"${process.execPath}" "%~dp0crabbox" %*\r\n`, + "utf8", + ); + chmodSync(crabboxPath, 0o755); + return binDir; +} + function shellSingleQuote(value: string): string { return `'${value.replaceAll("'", "'\\''")}'`; } @@ -1599,6 +1620,18 @@ describe.concurrent("scripts/crabbox-wrapper", () => { expect(result.stderr).toContain("selected binary does not advertise provider bogus"); }); + it("times out hung sanity probes before rejecting the selected binary", () => { + const helpText = "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n"; + const result = runWrapper(helpText, ["--version"], { + extraPathEntries: [makeSlowVersionCrabbox(helpText)], + }); + + expect(result.error).toBeUndefined(); + expect(result.status).toBe(2); + expect(result.stderr).toContain("version=unknown"); + expect(result.stderr).toContain("selected binary failed basic --version/--help sanity checks"); + }); + it("parses provider choices from the --provider flag help format", () => { const result = runWrapper( "Usage: crabbox run [options]\n --provider hetzner|aws|local-container|blacksmith-testbox|cloudflare\n",