fix: route pnpm test wrappers through the active runner (#60153)

This commit is contained in:
Peter Steinberger
2026-04-03 21:53:18 +09:00
parent ab57d24f79
commit 0c0d84fbd9
4 changed files with 139 additions and 13 deletions

53
scripts/pnpm-runner.mjs Normal file
View File

@@ -0,0 +1,53 @@
import path from "node:path";
const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>%\r\n]/;
function isPnpmExecPath(value) {
return /^pnpm(?:-cli)?(?:\.(?:c?js|cmd|exe))?$/.test(path.basename(value).toLowerCase());
}
function escapeForCmdExe(arg) {
if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(arg)) {
throw new Error(`unsafe Windows cmd.exe argument detected: ${JSON.stringify(arg)}`);
}
const escaped = arg.replace(/\^/g, "^^");
if (!escaped.includes(" ") && !escaped.includes('"')) {
return escaped;
}
return `"${escaped.replace(/"/g, '""')}"`;
}
function buildCmdExeCommandLine(command, args) {
return [escapeForCmdExe(command), ...args.map(escapeForCmdExe)].join(" ");
}
export function resolvePnpmRunner(params = {}) {
const pnpmArgs = params.pnpmArgs ?? [];
const npmExecPath = params.npmExecPath ?? process.env.npm_execpath;
const nodeExecPath = params.nodeExecPath ?? process.execPath;
const platform = params.platform ?? process.platform;
const comSpec = params.comSpec ?? process.env.ComSpec ?? "cmd.exe";
if (typeof npmExecPath === "string" && npmExecPath.length > 0 && isPnpmExecPath(npmExecPath)) {
return {
command: nodeExecPath,
args: [npmExecPath, ...pnpmArgs],
shell: false,
};
}
if (platform === "win32") {
return {
command: comSpec,
args: ["/d", "/s", "/c", buildCmdExeCommandLine("pnpm.cmd", pnpmArgs)],
shell: false,
windowsVerbatimArguments: true,
};
}
return {
command: "pnpm",
args: pnpmArgs,
shell: false,
};
}

View File

@@ -1,4 +1,5 @@
import { spawn } from "node:child_process";
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
const forwardedArgs = [];
let quietOverride;
@@ -24,15 +25,15 @@ const env = {
OPENCLAW_LIVE_TEST_QUIET: quietOverride ?? process.env.OPENCLAW_LIVE_TEST_QUIET ?? "1",
};
const command = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
const child = spawn(
command,
["exec", "vitest", "run", "--config", "vitest.live.config.ts", ...forwardedArgs],
{
stdio: "inherit",
env,
},
);
const pnpmRunner = resolvePnpmRunner({
pnpmArgs: ["exec", "vitest", "run", "--config", "vitest.live.config.ts", ...forwardedArgs],
});
const child = spawn(pnpmRunner.command, pnpmRunner.args, {
stdio: "inherit",
env: pnpmRunner.env ?? env,
shell: pnpmRunner.shell,
windowsVerbatimArguments: pnpmRunner.windowsVerbatimArguments,
});
child.on("exit", (code, signal) => {
if (signal) {

View File

@@ -1,9 +1,10 @@
import { spawn } from "node:child_process";
import { acquireLocalHeavyCheckLockSync } from "./lib/local-heavy-check-runtime.mjs";
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
import { buildVitestArgs } from "./test-projects.test-support.mjs";
const command = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
const vitestArgs = buildVitestArgs(process.argv.slice(2));
const pnpmRunner = resolvePnpmRunner({ pnpmArgs: vitestArgs });
const releaseLock = acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env: process.env,
@@ -20,10 +21,11 @@ const releaseLockOnce = () => {
releaseLock();
};
const child = spawn(command, vitestArgs, {
const child = spawn(pnpmRunner.command, pnpmRunner.args, {
stdio: "inherit",
env: process.env,
shell: process.platform === "win32",
env: pnpmRunner.env ?? process.env,
shell: pnpmRunner.shell,
windowsVerbatimArguments: pnpmRunner.windowsVerbatimArguments,
});
child.on("exit", (code, signal) => {

View File

@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import { resolvePnpmRunner } from "../../scripts/pnpm-runner.mjs";
describe("resolvePnpmRunner", () => {
it("uses npm_execpath when it points to pnpm", () => {
expect(
resolvePnpmRunner({
npmExecPath: "/home/test/.cache/node/corepack/v1/pnpm/10.32.1/bin/pnpm.cjs",
nodeExecPath: "/usr/local/bin/node",
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "/usr/local/bin/node",
args: [
"/home/test/.cache/node/corepack/v1/pnpm/10.32.1/bin/pnpm.cjs",
"exec",
"vitest",
"run",
],
shell: false,
});
});
it("falls back to bare pnpm on non-Windows when npm_execpath is missing", () => {
expect(
resolvePnpmRunner({
npmExecPath: "",
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "pnpm",
args: ["exec", "vitest", "run"],
shell: false,
});
});
it("wraps pnpm.cmd via cmd.exe on Windows when npm_execpath is unavailable", () => {
expect(
resolvePnpmRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
npmExecPath: "",
pnpmArgs: ["exec", "vitest", "run", "-t", "path with spaces"],
platform: "win32",
}),
).toEqual({
command: "C:\\Windows\\System32\\cmd.exe",
args: ["/d", "/s", "/c", 'pnpm.cmd exec vitest run -t "path with spaces"'],
shell: false,
windowsVerbatimArguments: true,
});
});
it("escapes caret arguments for Windows cmd.exe", () => {
expect(
resolvePnpmRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
npmExecPath: "",
pnpmArgs: ["exec", "vitest", "-t", "@scope/pkg@^1.2.3"],
platform: "win32",
}),
).toEqual({
command: "C:\\Windows\\System32\\cmd.exe",
args: ["/d", "/s", "/c", "pnpm.cmd exec vitest -t @scope/pkg@^^1.2.3"],
shell: false,
windowsVerbatimArguments: true,
});
});
});