From 87af5763bbc0bd1febc08d78eedc7fafd9eb5225 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 2 Oct 2026 12:26:40 -0700 Subject: [PATCH] fix(test): apply Node shutdown policy to built CLI JSON fixtures (#163630) runBuiltCli spawned the built CLI with Node 24 defaults, so concurrent Sparkplug and Maglev stayed enabled in the child. Under CPU contention process.exit can then deadlock joining a background compiler that waits for a main-thread GC; spawnSync times out and reports status null after the CLI already printed its correct output. Gateway e2e shard 4/4 reproduced this in test/cli-json-stdout.gateway.e2e.test.ts. Prepend the shared resolveVitestNodeArgs policy (skipped under Bun), as runCliProcessChild already does, and assert in the gateway suite's preload that children run without concurrent Sparkplug. --- test/cli-json-stdout.gateway.e2e.test.ts | 4 ++++ test/cli-json-stdout.test-support.ts | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/test/cli-json-stdout.gateway.e2e.test.ts b/test/cli-json-stdout.gateway.e2e.test.ts index 802cc43547e9..32b4b6f10023 100644 --- a/test/cli-json-stdout.gateway.e2e.test.ts +++ b/test/cli-json-stdout.gateway.e2e.test.ts @@ -91,10 +91,12 @@ describe("cli json stdout contract", () => { const configPath = path.join(tempHome, "missing-openclaw.json"); const stateDir = path.join(tempHome, "isolated-state"); const gatewayError = "AUTOQA_INJECTED_GATEWAY_FAILURE"; + const concurrentSparkplug = "AUTOQA_CONCURRENT_SPARKPLUG"; const preload = Buffer.from( [ 'import net from "node:net";', `net.Socket.prototype.connect = function () { throw new Error(${JSON.stringify(gatewayError)}); };`, + `if (!process.versions.bun && !process.execArgv.includes("--no-concurrent-sparkplug")) process.stderr.write(${JSON.stringify(`${concurrentSparkplug}\n`)});`, ...("configReadFailure" in testCase ? [ 'import fs from "node:fs";', @@ -129,6 +131,8 @@ describe("cli json stdout contract", () => { { execArgv: [`--import=data:text/javascript;base64,${preload}`] }, ); + // Concurrent Sparkplug can deadlock process.exit, so the child times out with status null. + expect(result.stderr).not.toContain(concurrentSparkplug); expect(result.status, result.stderr).toBe(1); expect(result.stdout, result.stderr).not.toContain("\u001B"); expect(result.stdout, result.stderr).not.toContain("\u0007"); diff --git a/test/cli-json-stdout.test-support.ts b/test/cli-json-stdout.test-support.ts index 47b433eff8fb..037c8d63c776 100644 --- a/test/cli-json-stdout.test-support.ts +++ b/test/cli-json-stdout.test-support.ts @@ -1,5 +1,6 @@ import { spawnSync } from "node:child_process"; import path from "node:path"; +import { resolveVitestNodeArgs } from "../scripts/lib/vitest-process-env.mts"; export function runBuiltCli( tempHome: string, @@ -20,7 +21,10 @@ export function runBuiltCli( Object.assign(env, envOverrides); const entry = path.resolve(process.cwd(), "openclaw.mjs"); - return spawnSync(process.execPath, [...(options.execArgv ?? []), entry, ...args], { + // Without the runner's V8 policy, process.exit can deadlock joining a background + // compiler that waits for main-thread GC; spawnSync then reports status null. + const nodeArgs = process.versions.bun ? [] : resolveVitestNodeArgs(env); + return spawnSync(process.execPath, [...nodeArgs, ...(options.execArgv ?? []), entry, ...args], { cwd: process.cwd(), env, encoding: "utf8",