From fca77dcb19c8e62176bb08b5bb013dae81ac88a0 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 27 May 2026 06:17:22 +0200 Subject: [PATCH] fix(e2e): bound bundled runtime smoke commands --- .../runtime-smoke.mjs | 48 ++++++++++++++----- ...led-plugin-install-uninstall-probe.test.ts | 18 +++++++ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs b/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs index 2c8ff152deb..73b71a1df39 100644 --- a/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs +++ b/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs @@ -21,6 +21,10 @@ const RPC_READY_TIMEOUT_MS = readPositiveInt( process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_RPC_READY_MS, 210000, ); +const COMMAND_TIMEOUT_MS = readPositiveInt( + process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_COMMAND_MS, + 120000, +); function readPositiveInt(raw, fallback) { const parsed = Number.parseInt(String(raw || ""), 10); @@ -38,9 +42,7 @@ function writeJson(file, value) { function manifestPath(pluginDir, pluginRoot) { const candidates = [ - ...(isNonEmptyString(pluginRoot) - ? [path.join(pluginRoot, "openclaw.plugin.json")] - : []), + ...(isNonEmptyString(pluginRoot) ? [path.join(pluginRoot, "openclaw.plugin.json")] : []), path.join(process.cwd(), "dist", "extensions", pluginDir, "openclaw.plugin.json"), path.join(process.cwd(), "dist-runtime", "extensions", pluginDir, "openclaw.plugin.json"), ]; @@ -161,22 +163,47 @@ function formatCapturedOutput(label, buffer) { return `${prefix}${buffer.text}`; } -function runCommand(command, args, options = {}) { +export function runCommand(command, args, options = {}) { return new Promise((resolve, reject) => { + const { timeoutMs = COMMAND_TIMEOUT_MS, ...spawnOptions } = options; const child = childProcess.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], - ...options, + ...spawnOptions, }); let stdout = { text: "", truncatedChars: 0 }; let stderr = { text: "", truncatedChars: 0 }; + let timedOut = false; + let settled = false; child.stdout?.on("data", (chunk) => { stdout = appendBoundedOutput(stdout, chunk); }); child.stderr?.on("data", (chunk) => { stderr = appendBoundedOutput(stderr, chunk); }); - child.on("error", reject); + const clearCommandTimer = timeoutMs + ? setTimeout(() => { + timedOut = true; + child.kill("SIGKILL"); + }, timeoutMs) + : undefined; + child.on("error", (error) => { + if (settled) { + return; + } + settled = true; + if (clearCommandTimer) { + clearTimeout(clearCommandTimer); + } + reject(error); + }); child.on("close", (status, signal) => { + if (settled) { + return; + } + settled = true; + if (clearCommandTimer) { + clearTimeout(clearCommandTimer); + } if (status === 0) { resolve({ stdout: stdout.text, @@ -193,11 +220,10 @@ function runCommand(command, args, options = {}) { .filter(Boolean) .join("\n") .trim(); - reject( - new Error( - `${command} ${args.join(" ")} failed with ${signal || status}${detail ? `\n${detail}` : ""}`, - ), - ); + const outcome = timedOut + ? `timed out after ${timeoutMs}ms` + : `failed with ${signal || status}`; + reject(new Error(`${command} ${args.join(" ")} ${outcome}${detail ? `\n${detail}` : ""}`)); }); }); } diff --git a/test/scripts/bundled-plugin-install-uninstall-probe.test.ts b/test/scripts/bundled-plugin-install-uninstall-probe.test.ts index 5bc5d4bd590..f03ecfb9c24 100644 --- a/test/scripts/bundled-plugin-install-uninstall-probe.test.ts +++ b/test/scripts/bundled-plugin-install-uninstall-probe.test.ts @@ -117,6 +117,24 @@ describe("bundled plugin install/uninstall probe", () => { expect(second).toEqual({ text: "fghij", truncatedChars: 5 }); }); + it("bounds runtime smoke child commands and preserves captured output", async () => { + const runtimeSmoke = await import(pathToFileURL(runtimeSmokePath).href); + const startedAt = Date.now(); + + await expect( + runtimeSmoke.runCommand( + process.execPath, + [ + "-e", + "process.stdout.write('partial\\n'); process.stderr.write('problem\\n'); setInterval(() => {}, 1000);", + ], + { timeoutMs: 200 }, + ), + ).rejects.toThrow(/timed out after 200ms[\s\S]*partial[\s\S]*problem/u); + + expect(Date.now() - startedAt).toBeLessThan(2_500); + }); + it("creates runtime smoke state with OPENCLAW_HOME at the test home", async () => { const runtimeSmoke = await import(pathToFileURL(runtimeSmokePath).href); const env = runtimeSmoke.createIsolatedStateEnv("runtime-env");