From 1586085c7fcbd78870bbede336cdd23a1cf0abda Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 17 May 2026 02:48:27 +0800 Subject: [PATCH] test: share node eval helpers --- packages/sdk/src/package.e2e.test.ts | 3 +- .../net/proxy/external-proxy.e2e.test.ts | 15 +++---- .../net/undici-global-dispatcher.test.ts | 8 +--- src/plugin-sdk/fetch-runtime.test.ts | 8 +--- .../loader.git-path-regression.test.ts | 5 +-- src/test-utils/fs-scan-assertions.ts | 12 ++---- src/test-utils/node-process.ts | 43 +++++++++++++++++++ test/scripts/parallels-smoke-model.test.ts | 33 ++++---------- test/vitest-unit-fast-config.test.ts | 23 +++++----- 9 files changed, 81 insertions(+), 69 deletions(-) create mode 100644 src/test-utils/node-process.ts diff --git a/packages/sdk/src/package.e2e.test.ts b/packages/sdk/src/package.e2e.test.ts index d039e351040..a5aee684083 100644 --- a/packages/sdk/src/package.e2e.test.ts +++ b/packages/sdk/src/package.e2e.test.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; +import { createNodeEvalArgs } from "../../../src/test-utils/node-process.js"; type CommandResult = { stdout: string; @@ -104,7 +105,7 @@ describe("OpenClaw SDK package e2e", () => { }); if (event.type !== "run.started") throw new Error("unexpected event normalization"); `; - await runCommand(process.execPath, ["--input-type=module", "-e", importScript], { + await runCommand(process.execPath, createNodeEvalArgs(importScript, { evalFlag: "-e" }), { cwd: tempDir, }); }); diff --git a/src/infra/net/proxy/external-proxy.e2e.test.ts b/src/infra/net/proxy/external-proxy.e2e.test.ts index 9834188f59d..9d864950ab0 100644 --- a/src/infra/net/proxy/external-proxy.e2e.test.ts +++ b/src/infra/net/proxy/external-proxy.e2e.test.ts @@ -8,6 +8,7 @@ import type { Duplex } from "node:stream"; import { afterEach, describe, expect, it } from "vitest"; import { WebSocketServer } from "ws"; import { withTempDir } from "../../../test-helpers/temp-dir.js"; +import { createNodeEvalArgs } from "../../../test-utils/node-process.js"; import { resolveSystemBin } from "../../resolve-system-bin.js"; import { resolvePreferredOpenClawTmpDir } from "../../tmp-openclaw-dir.js"; @@ -248,15 +249,11 @@ async function runNodeModule( stdout: string; stderr: string; }> { - const child = spawn( - process.execPath, - ["--import", "tsx", "--input-type=module", "--eval", source], - { - cwd: process.cwd(), - env, - stdio: ["ignore", "pipe", "pipe"], - }, - ); + const child = spawn(process.execPath, createNodeEvalArgs(source, { imports: ["tsx"] }), { + cwd: process.cwd(), + env, + stdio: ["ignore", "pipe", "pipe"], + }); let stdout = ""; let stderr = ""; diff --git a/src/infra/net/undici-global-dispatcher.test.ts b/src/infra/net/undici-global-dispatcher.test.ts index ced3b1fcf9f..8e7749acb9f 100644 --- a/src/infra/net/undici-global-dispatcher.test.ts +++ b/src/infra/net/undici-global-dispatcher.test.ts @@ -1,7 +1,7 @@ -import { execFileSync } from "node:child_process"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { execNodeEvalSync } from "../../test-utils/node-process.js"; const { Agent, @@ -196,11 +196,7 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { delete env[key]; } - const output = execFileSync( - process.execPath, - ["--import", "tsx", "--input-type=module", "--eval", source], - { cwd: process.cwd(), encoding: "utf8", env }, - ); + const output = execNodeEvalSync(source, { env, imports: ["tsx"] }); expect(output.trim()).toBe("ok"); }); diff --git a/src/plugin-sdk/fetch-runtime.test.ts b/src/plugin-sdk/fetch-runtime.test.ts index 13d89648e5b..e247658839d 100644 --- a/src/plugin-sdk/fetch-runtime.test.ts +++ b/src/plugin-sdk/fetch-runtime.test.ts @@ -1,7 +1,7 @@ -import { execFileSync } from "node:child_process"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; +import { execNodeEvalSync } from "../test-utils/node-process.js"; describe("plugin SDK fetch runtime", () => { it("does not initialize the undici global dispatcher on import", () => { @@ -27,11 +27,7 @@ describe("plugin SDK fetch runtime", () => { delete env[key]; } - const output = execFileSync( - process.execPath, - ["--import", "tsx", "--input-type=module", "--eval", source], - { cwd: process.cwd(), encoding: "utf8", env }, - ); + const output = execNodeEvalSync(source, { env, imports: ["tsx"] }); expect(output.trim()).toBe("ok"); }); diff --git a/src/plugins/loader.git-path-regression.test.ts b/src/plugins/loader.git-path-regression.test.ts index 80abcea17a6..b02dea475d9 100644 --- a/src/plugins/loader.git-path-regression.test.ts +++ b/src/plugins/loader.git-path-regression.test.ts @@ -1,7 +1,7 @@ -import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; +import { execNodeEvalSync } from "../test-utils/node-process.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir, @@ -85,9 +85,8 @@ export const copiedRuntimeMarker = { dep: mod.copiedRuntimeMarker?.resolveOutboundSendDep?.(), })); `; - const raw = execFileSync(process.execPath, ["--input-type=module", "--eval", script], { + const raw = execNodeEvalSync(script, { cwd: process.cwd(), - encoding: "utf-8", }); const result = JSON.parse(raw) as { withoutAliasThrew: boolean; diff --git a/src/test-utils/fs-scan-assertions.ts b/src/test-utils/fs-scan-assertions.ts index 25e5df15ec5..4b311373514 100644 --- a/src/test-utils/fs-scan-assertions.ts +++ b/src/test-utils/fs-scan-assertions.ts @@ -1,6 +1,6 @@ -import { spawnSync } from "node:child_process"; import fs from "node:fs"; import { expect, vi } from "vitest"; +import { spawnNodeEvalSync } from "./node-process.js"; type FsScanCounter = "existsSync" | "readdirSync" | "statSync"; @@ -63,12 +63,8 @@ export function expectNoNodeFsScans( }, ): T { const counters = options?.counters ?? ["existsSync", "readdirSync"]; - const result = spawnSync( - process.execPath, - [ - "--input-type=module", - "--eval", - ` + const result = spawnNodeEvalSync( + ` import fs from "node:fs"; import { syncBuiltinESMExports } from "node:module"; const counts = ${JSON.stringify(Object.fromEntries(counters.map((name) => [name, 0])))}; @@ -88,10 +84,8 @@ export function expectNoNodeFsScans( })(); console.log(JSON.stringify({ counts, result })); `, - ], { cwd: process.cwd(), - encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }, ); diff --git a/src/test-utils/node-process.ts b/src/test-utils/node-process.ts new file mode 100644 index 00000000000..8544844bceb --- /dev/null +++ b/src/test-utils/node-process.ts @@ -0,0 +1,43 @@ +import { execFileSync, spawnSync, type SpawnSyncReturns } from "node:child_process"; + +type NodeEvalArgsOptions = { + evalFlag?: "--eval" | "-e"; + imports?: readonly string[]; +}; + +type ExecNodeEvalOptions = Omit[2]>, "encoding"> & + NodeEvalArgsOptions & { + encoding?: BufferEncoding; + }; + +type SpawnNodeEvalOptions = Omit[2]>, "encoding"> & + NodeEvalArgsOptions & { + encoding?: BufferEncoding; + }; + +export function createNodeEvalArgs(source: string, options: NodeEvalArgsOptions = {}): string[] { + const args = (options.imports ?? []).flatMap((specifier) => ["--import", specifier]); + args.push("--input-type=module", options.evalFlag ?? "--eval", source); + return args; +} + +export function execNodeEvalSync(source: string, options: ExecNodeEvalOptions = {}): string { + const { evalFlag, imports, ...execOptions } = options; + return execFileSync(process.execPath, createNodeEvalArgs(source, { evalFlag, imports }), { + cwd: process.cwd(), + encoding: "utf8", + ...execOptions, + }); +} + +export function spawnNodeEvalSync( + source: string, + options: SpawnNodeEvalOptions = {}, +): SpawnSyncReturns { + const { evalFlag, imports, ...spawnOptions } = options; + return spawnSync(process.execPath, createNodeEvalArgs(source, { evalFlag, imports }), { + cwd: process.cwd(), + encoding: "utf8", + ...spawnOptions, + }); +} diff --git a/test/scripts/parallels-smoke-model.test.ts b/test/scripts/parallels-smoke-model.test.ts index 4d8f7bb16cd..11bc710bd20 100644 --- a/test/scripts/parallels-smoke-model.test.ts +++ b/test/scripts/parallels-smoke-model.test.ts @@ -1,8 +1,8 @@ -import { execFileSync, spawnSync } from "node:child_process"; import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { execNodeEvalSync, spawnNodeEvalSync } from "../../src/test-utils/node-process.js"; const WRAPPERS = { linux: "scripts/e2e/parallels-linux-smoke.sh", @@ -46,10 +46,7 @@ function countNonEmptyLines(value: string): number { } function runTsEval(source: string, env: Record = {}) { - return execFileSync("node", ["--import", "tsx", "--input-type=module", "--eval", source], { - encoding: "utf8", - env: { ...process.env, ...env }, - }); + return execNodeEvalSync(source, { env: { ...process.env, ...env }, imports: ["tsx"] }); } function resolveProviderAuth( @@ -313,32 +310,18 @@ console.log(JSON.stringify(result)); }); it("rejects invalid providers and missing keys before touching guests", () => { - const invalidProvider = spawnSync( - "node", - [ - "--import", - "tsx", - "--input-type=module", - "--eval", - `import { parseProvider } from "./${TS_PATHS.common}"; parseProvider("bogus");`, - ], - { encoding: "utf8", env: process.env }, + const invalidProvider = spawnNodeEvalSync( + `import { parseProvider } from "./${TS_PATHS.common}"; parseProvider("bogus");`, + { env: process.env, imports: ["tsx"] }, ); expect(invalidProvider.status).toBe(1); expect(invalidProvider.stderr).toContain("invalid --provider: bogus"); - const missingKey = spawnSync( - "node", - [ - "--import", - "tsx", - "--input-type=module", - "--eval", - `import { resolveProviderAuth } from "./${TS_PATHS.common}"; resolveProviderAuth({ provider: "openai", apiKeyEnv: "PARALLELS_TEST_MISSING_KEY" });`, - ], + const missingKey = spawnNodeEvalSync( + `import { resolveProviderAuth } from "./${TS_PATHS.common}"; resolveProviderAuth({ provider: "openai", apiKeyEnv: "PARALLELS_TEST_MISSING_KEY" });`, { - encoding: "utf8", env: { ...process.env, PARALLELS_TEST_MISSING_KEY: "" }, + imports: ["tsx"], }, ); expect(missingKey.status).toBe(1); diff --git a/test/vitest-unit-fast-config.test.ts b/test/vitest-unit-fast-config.test.ts index ef121782cd3..e85c14577b9 100644 --- a/test/vitest-unit-fast-config.test.ts +++ b/test/vitest-unit-fast-config.test.ts @@ -1,5 +1,5 @@ -import { spawnSync } from "node:child_process"; import { describe, expect, it } from "vitest"; +import { spawnNodeEvalSync } from "../src/test-utils/node-process.js"; import { createCommandsLightVitestConfig } from "./vitest/vitest.commands-light.config.ts"; import { createPluginSdkLightVitestConfig } from "./vitest/vitest.plugin-sdk-light.config.ts"; import { @@ -62,17 +62,20 @@ describe("unit-fast vitest lane", () => { await import("./test/vitest/vitest.unit-fast.config.ts?io-probe=" + Date.now()); console.log(readdirSyncCalls); `; - const result = spawnSync( - process.execPath, - ["--import", "tsx", "--input-type=module", "-e", script], - { - cwd: process.cwd(), - encoding: "utf8", - }, - ); + const result = spawnNodeEvalSync(script, { + env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }, + evalFlag: "-e", + imports: ["tsx"], + }); expect(result.status, result.stderr).toBe(0); - expect(Number(result.stdout.trim())).toBeLessThan(20); + const numericOutputLines = result.stdout + .trim() + .split(/\r?\n/u) + .map((line) => Number(line.trim())) + .filter(Number.isFinite); + expect(numericOutputLines.length, result.stdout).toBeGreaterThan(0); + expect(numericOutputLines.at(-1)).toBeLessThan(20); }); it("runs cache-friendly tests without the reset-heavy runner or runtime setup", () => {