diff --git a/scripts/e2e/secret-provider-integrations.mjs b/scripts/e2e/secret-provider-integrations.mjs index 567c9661a5da..ed574f91469c 100644 --- a/scripts/e2e/secret-provider-integrations.mjs +++ b/scripts/e2e/secret-provider-integrations.mjs @@ -20,16 +20,30 @@ const MANUAL_EXEC_TOKEN = "proof-manual-exec-token"; const PLUGIN_EXEC_TOKEN = "proof-plugin-exec-token"; const OPENAI_PROFILE = "openai:secretref-proof"; const OPENAI_LIVE_PROOF_MODEL = "openai/gpt-5.5"; -const COMMAND_TIMEOUT_MS = readPositiveInt(process.env.OPENCLAW_SECRET_PROOF_COMMAND_MS, 120000); -const READY_TIMEOUT_MS = readPositiveInt(process.env.OPENCLAW_SECRET_PROOF_READY_MS, 120000); -const RPC_TIMEOUT_MS = readPositiveInt(process.env.OPENCLAW_SECRET_PROOF_RPC_MS, 15000); +const COMMAND_TIMEOUT_MS = readPositiveInt( + process.env.OPENCLAW_SECRET_PROOF_COMMAND_MS, + 120000, + "OPENCLAW_SECRET_PROOF_COMMAND_MS", +); +const READY_TIMEOUT_MS = readPositiveInt( + process.env.OPENCLAW_SECRET_PROOF_READY_MS, + 120000, + "OPENCLAW_SECRET_PROOF_READY_MS", +); +const RPC_TIMEOUT_MS = readPositiveInt( + process.env.OPENCLAW_SECRET_PROOF_RPC_MS, + 15000, + "OPENCLAW_SECRET_PROOF_RPC_MS", +); const TEARDOWN_GRACE_MS = readPositiveInt( process.env.OPENCLAW_SECRET_PROOF_TEARDOWN_GRACE_MS, 5000, + "OPENCLAW_SECRET_PROOF_TEARDOWN_GRACE_MS", ); const OUTPUT_CAPTURE_LIMIT_BYTES = readPositiveInt( process.env.OPENCLAW_SECRET_PROOF_OUTPUT_BYTES, 4 * 1024 * 1024, + "OPENCLAW_SECRET_PROOF_OUTPUT_BYTES", ); const RESULTS_PATH = process.env.OPENCLAW_SECRET_PROOF_RESULTS_PATH?.trim() || @@ -42,13 +56,19 @@ function requireFullMatrix() { return process.env.OPENCLAW_SECRET_PROOF_FULL === "1"; } -function readPositiveInt(raw, fallback) { +function readPositiveInt(raw, fallback, label) { const text = String(raw ?? "").trim(); - if (!/^\d+$/u.test(text)) { + if (!text) { return fallback; } + if (!/^\d+$/u.test(text)) { + throw new Error(`${label} must be a positive integer. Got: ${JSON.stringify(text)}`); + } const parsed = Number(text); - return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback; + if (!Number.isSafeInteger(parsed) || parsed <= 0) { + throw new Error(`${label} must be a positive integer. Got: ${JSON.stringify(text)}`); + } + return parsed; } function remainingDeadlineMs(started, timeoutMs) { @@ -303,7 +323,7 @@ function runCommand(command, args, options = {}) { } abortSignal?.removeEventListener("abort", abort); removeParentSignalHandlers(); - const result = { code: code ?? 0, signal, stdout: stdout.text(), stderr: stderr.text() }; + const result = { code, signal, stdout: stdout.text(), stderr: stderr.text() }; if (aborted) { reject(new Error(scrub(`command aborted: ${command} ${args.join(" ")}`))); return; @@ -313,6 +333,18 @@ function runCommand(command, args, options = {}) { reject(new Error(scrub(`command timed out: ${command} ${args.join(" ")}`))); return; } + if (result.signal && options.allowFailure !== true) { + reject( + new Error( + scrub( + `command terminated by signal (${result.signal}): ${command} ${args.join(" ")}\n${ + result.stderr || result.stdout + }`, + ), + ), + ); + return; + } if (result.code !== 0 && options.allowFailure !== true) { reject( new Error( diff --git a/test/scripts/secret-provider-integrations.test.ts b/test/scripts/secret-provider-integrations.test.ts index 15ac4207a00d..a7deed5495e9 100644 --- a/test/scripts/secret-provider-integrations.test.ts +++ b/test/scripts/secret-provider-integrations.test.ts @@ -237,6 +237,43 @@ describe("secret provider integration proof harness", () => { } }); + it.runIf(process.platform !== "win32")( + "fails mandatory commands that exit by signal", + async () => { + const proof = await import( + `${pathToFileURL(proofScriptPath).href}?case=signal-${Date.now()}` + ); + + await expect( + proof.runCommand(process.execPath, [ + "--input-type=module", + "--eval", + "process.kill(process.pid, 'SIGTERM');", + ]), + ).rejects.toThrow("command terminated by signal (SIGTERM)"); + }, + ); + + it.each([ + ["OPENCLAW_SECRET_PROOF_COMMAND_MS", "150ms"], + ["OPENCLAW_SECRET_PROOF_READY_MS", "0"], + ["OPENCLAW_SECRET_PROOF_OUTPUT_BYTES", "4mb"], + ])("rejects malformed proof env limit %s=%s", async (name, value) => { + const previous = process.env[name]; + process.env[name] = value; + try { + await expect( + import(`${pathToFileURL(proofScriptPath).href}?case=env-${name}-${Date.now()}`), + ).rejects.toThrow(`${name} must be a positive integer`); + } finally { + if (previous === undefined) { + delete process.env[name]; + } else { + process.env[name] = previous; + } + } + }); + it("fails when proof temp cleanup cannot remove the root", async () => { const proof = await import(`${pathToFileURL(proofScriptPath).href}?case=cleanup-${Date.now()}`); const rmSync = vi.spyOn(fs, "rmSync").mockImplementation(() => {