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.
This commit is contained in:
Peter Steinberger 2026-10-02 12:26:40 -07:00 • committed by GitHub
parent 9d2360bd90
commit 87af5763bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 9 additions and 1 deletions

View file

@ -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");

View file

@ -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",