test: share node eval helpers

This commit is contained in:
Vincent Koc
2026-05-17 02:48:27 +08:00
parent 11745de9d9
commit 1586085c7f
9 changed files with 81 additions and 69 deletions

View File

@@ -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,
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>(
},
): 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<T>(
})();
console.log(JSON.stringify({ counts, result }));
`,
],
{
cwd: process.cwd(),
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
},
);

View File

@@ -0,0 +1,43 @@
import { execFileSync, spawnSync, type SpawnSyncReturns } from "node:child_process";
type NodeEvalArgsOptions = {
evalFlag?: "--eval" | "-e";
imports?: readonly string[];
};
type ExecNodeEvalOptions = Omit<NonNullable<Parameters<typeof execFileSync>[2]>, "encoding"> &
NodeEvalArgsOptions & {
encoding?: BufferEncoding;
};
type SpawnNodeEvalOptions = Omit<NonNullable<Parameters<typeof spawnSync>[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<string> {
const { evalFlag, imports, ...spawnOptions } = options;
return spawnSync(process.execPath, createNodeEvalArgs(source, { evalFlag, imports }), {
cwd: process.cwd(),
encoding: "utf8",
...spawnOptions,
});
}

View File

@@ -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<string, string> = {}) {
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);

View File

@@ -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", () => {