openclaw/test/scripts/pnpm-runner.test.ts
Vincent Koc cf3af204b2
refactor(test): drop empty pnpm PATH fixture (#161564)
Remove a case that claims relative PATH lookup but creates no executable and only repeats the existing missing-launcher fallback. Keep real child-process PATH precedence, environment and platform coverage.

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-09-30 04:49:24 +00:00

482 lines
16 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { createPnpmRunnerSpawnSpec, resolvePnpmRunner } from "../../scripts/pnpm-runner.mts";
import { buildCmdExeCommandLine } from "../../scripts/windows-cmd-helpers.mjs";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
describe("resolvePnpmRunner", () => {
const posixIt = process.platform === "win32" ? it.skip : it;
const tempDirs = useAutoCleanupTempDirTracker(afterAll);
let fixturesRoot: string;
beforeAll(() => {
fixturesRoot = tempDirs.make("pnpm-runner-");
const writeLauncher = (relativePath: string, source: string | Buffer, mode?: number) => {
const file = path.join(fixturesRoot, relativePath);
mkdirSync(path.dirname(file), { recursive: true });
writeFileSync(file, source);
if (mode !== undefined) {
chmodSync(file, mode);
}
};
for (const name of ["pnpm-cli.cjs", "pnpm.cjs", "pnpm.mjs"]) {
writeLauncher(
`env/${name}`,
`console.log(JSON.stringify({
entrypoint: process.argv[1],
npmExecPath: process.env.npm_execpath ?? null,
args: process.argv.slice(2),
}));\n`,
);
}
writeLauncher("js/pnpm.cjs", "console.log('pnpm');\n");
writeLauncher("shebang/pnpm", "#!/usr/bin/env node\nconsole.log('pnpm');\n");
const elfHeader = Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
for (const name of ["pnpm", "pnpm-native"]) {
writeLauncher(`native/${name}`, elfHeader, 0o755);
}
writeLauncher("not-executable/pnpm", elfHeader, 0o644);
writeLauncher("shell/pnpm", '#!/bin/sh\nprintf "%s\\n" "$@"\n', 0o755);
writeLauncher("parent/pnpm", "#!/usr/bin/env node\n", 0o755);
for (const name of ["corepack/corepack", "path/pnpm", "path/corepack"]) {
writeLauncher(name, "#!/bin/sh\nexit 0\n", 0o755);
}
writeLauncher("windows/corepack.cmd", "@exit /b 0\r\n");
for (const [file, marker, exitCode] of [
["cwd/pnpm", "cwd", 7],
["other/pnpm", "other", 0],
["corepack-only/corepack", "corepack", 9],
] as const) {
writeLauncher(
`empty-path/${file}`,
`#!/bin/sh\nprintf '%s\n' '${marker}' "$@"\nexit ${exitCode}\n`,
0o755,
);
}
});
afterEach(() => vi.unstubAllEnvs());
it.each([
{ name: "supplied env", env: "selected", override: false, entrypoint: "pnpm.cjs" },
{ name: "explicit override", env: "selected", override: true, entrypoint: "pnpm.mjs" },
{ name: "default process env", env: "default", override: false, entrypoint: "pnpm-cli.cjs" },
{ name: "empty env with override", env: "empty", override: true, entrypoint: "pnpm.mjs" },
])("executes with $name at the child boundary", ({ env: envMode, override, entrypoint }) => {
const tempDir = path.join(fixturesRoot, "env");
const parentPath = path.join(tempDir, "pnpm-cli.cjs");
const selectedPath = path.join(tempDir, "pnpm.cjs");
const overridePath = path.join(tempDir, "pnpm.mjs");
const env =
envMode === "selected"
? { npm_execpath: selectedPath }
: envMode === "empty"
? {}
: undefined;
const params = {
cwd: tempDir,
env,
npmExecPath: override ? overridePath : undefined,
pnpmArgs: ["literal & argument"],
stdio: "pipe",
};
// Windows worker env copies are case-sensitive. Set the default environment
// in a real main thread so launcher selection and child inheritance agree.
const result = spawnSync(process.execPath, ["--input-type=module", "-"], {
encoding: "utf8",
input: `
import { spawnSync } from "node:child_process";
process.env.npm_execpath = ${JSON.stringify(parentPath)};
const { createPnpmRunnerSpawnSpec } = await import(${JSON.stringify(new URL("../../scripts/pnpm-runner.mts", import.meta.url).href)});
const spec = createPnpmRunnerSpawnSpec(${JSON.stringify(params)});
const result = spawnSync(spec.command, spec.args, { ...spec.options, encoding: "utf8" });
if (result.error) throw result.error;
process.stdout.write(result.stdout);
process.stderr.write(result.stderr);
process.exitCode = result.status ?? 1;
`,
});
expect(result.status, result.stderr).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({
entrypoint: path.join(tempDir, entrypoint),
npmExecPath: envMode === "selected" ? selectedPath : envMode === "empty" ? null : parentPath,
args: ["literal & argument"],
});
});
it("uses npm_execpath when it points to a shebang pnpm script", () => {
const tempDir = path.join(fixturesRoot, "shebang");
const npmExecPath = path.join(tempDir, "pnpm");
expect(
resolvePnpmRunner({
npmExecPath,
nodeExecPath: "/usr/local/bin/node",
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "/usr/local/bin/node",
args: [npmExecPath, "exec", "vitest", "run"],
shell: false,
});
});
it("prepends node args when launching pnpm through node", () => {
const tempDir = path.join(fixturesRoot, "js");
const npmExecPath = path.join(tempDir, "pnpm.cjs");
expect(
resolvePnpmRunner({
npmExecPath,
nodeArgs: ["--no-maglev"],
nodeExecPath: "/usr/local/bin/node",
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "/usr/local/bin/node",
args: ["--no-maglev", npmExecPath, "exec", "vitest", "run"],
shell: false,
});
});
it.each(["pnpm", "pnpm-native"])("executes native %s from npm_execpath directly", (basename) => {
const tempDir = path.join(fixturesRoot, "native");
const npmExecPath = path.join(tempDir, basename);
expect(
resolvePnpmRunner({
npmExecPath,
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: npmExecPath,
args: ["exec", "vitest", "run"],
shell: false,
});
});
posixIt("falls back to bare pnpm when native npm_execpath is not executable", () => {
const tempDir = path.join(fixturesRoot, "not-executable");
const npmExecPath = path.join(tempDir, "pnpm");
expect(
resolvePnpmRunner({
npmExecPath,
env: { PATH: "" },
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "pnpm",
args: ["exec", "vitest", "run"],
shell: false,
});
});
it.each(["pnpm.exe", "pnpm-native.exe"])("executes %s directly on Windows", (basename) => {
const npmExecPath = `C:\\Users\\test\\AppData\\Local\\pnpm\\${basename}`;
expect(
resolvePnpmRunner({
npmExecPath,
nodeArgs: ["--no-maglev"],
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
pnpmArgs: ["exec", "vitest", "run"],
platform: "win32",
}),
).toEqual({
command: npmExecPath,
args: ["exec", "vitest", "run"],
shell: false,
});
});
posixIt("executes a shell npm_execpath with its own interpreter", () => {
const tempDir = path.join(fixturesRoot, "shell");
const npmExecPath = path.join(tempDir, "pnpm");
const spec = createPnpmRunnerSpawnSpec({
npmExecPath,
pnpmArgs: ["literal & argument"],
stdio: "pipe",
});
const result = spawnSync(spec.command, spec.args, { ...spec.options, encoding: "utf8" });
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toBe("literal & argument\n");
});
it("uses pnpm.cjs through node for Windows-style paths", () => {
const tempDir = path.join(fixturesRoot, "js");
const npmExecPath = path.join(tempDir, "pnpm.cjs");
expect(
resolvePnpmRunner({
npmExecPath,
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
pnpmArgs: ["exec", "vitest", "run"],
platform: "win32",
}),
).toEqual({
command: "C:\\Program Files\\nodejs\\node.exe",
args: [npmExecPath, "exec", "vitest", "run"],
shell: false,
});
});
it("falls back to pnpm.cmd on Windows for a missing legacy pnpm 10 JS entrypoint", () => {
expect(
resolvePnpmRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
env: { PATH: "" },
npmExecPath:
"C:\\Users\\test\\AppData\\Local\\Temp\\cache\\corepack\\v1\\pnpm\\10.32.1\\bin\\pnpm.mjs",
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
pnpmArgs: ["exec", "vitest", "run"],
platform: "win32",
}),
).toEqual({
command: "C:\\Windows\\System32\\cmd.exe",
args: ["/d", "/s", "/c", "pnpm.cmd exec vitest run"],
shell: false,
windowsVerbatimArguments: true,
});
});
it("wraps an explicit pnpm.cmd path via cmd.exe on Windows", () => {
expect(
resolvePnpmRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
npmExecPath: "C:\\Program Files\\pnpm\\pnpm.cmd",
pnpmArgs: ["exec", "vitest", "run", "-t", "path with spaces"],
platform: "win32",
}),
).toEqual({
command: "C:\\Windows\\System32\\cmd.exe",
args: [
"/d",
"/s",
"/c",
'""C:\\Program Files\\pnpm\\pnpm.cmd" exec vitest run -t "path with spaces""',
],
shell: false,
windowsVerbatimArguments: true,
});
});
it("falls back to bare pnpm on non-Windows when npm_execpath is missing", () => {
expect(
resolvePnpmRunner({
npmExecPath: "",
env: { PATH: "" },
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "pnpm",
args: ["exec", "vitest", "run"],
shell: false,
});
});
posixIt("does not resolve parent npm_execpath or PATH for an explicit empty env", () => {
const tempDir = path.join(fixturesRoot, "parent");
const npmExecPath = path.join(tempDir, "pnpm");
vi.stubEnv("npm_execpath", npmExecPath);
vi.stubEnv("PATH", tempDir);
expect(
resolvePnpmRunner({
env: {},
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "pnpm",
args: ["exec", "vitest", "run"],
shell: false,
});
});
posixIt.each([
{ name: "leading empty", segments: ["", "other"], marker: "cwd", exitCode: 7 },
{
name: "interior empty",
segments: ["corepack-only", "", "other"],
marker: "cwd",
exitCode: 7,
},
{ name: "trailing empty", segments: ["corepack-only", ""], marker: "cwd", exitCode: 7 },
{ name: "explicit current directory", segments: [".", "other"], marker: "cwd", exitCode: 7 },
{ name: "earlier pnpm", segments: ["other", ""], marker: "other", exitCode: 0 },
])("preserves native PATH selection for $name", ({ segments, marker, exitCode }) => {
const root = path.join(fixturesRoot, "empty-path");
const cwd = path.join(root, "cwd");
const env = {
PATH: segments
.map((entry) => (entry === "" || entry === "." ? entry : path.join(root, entry)))
.join(":"),
};
const args = ["run", "build", "literal & argument", ""];
const expectedOutput = [marker, ...args, ""].join("\n");
const spec = createPnpmRunnerSpawnSpec({
cwd,
env,
npmExecPath: "",
pnpmArgs: args,
stdio: "pipe",
});
const wrapped = spawnSync(spec.command, spec.args, {
...spec.options,
encoding: "utf8",
timeout: 5_000,
});
expect(wrapped.error).toBeUndefined();
expect(wrapped.status, wrapped.stderr).toBe(exitCode);
expect(wrapped.stdout).toBe(expectedOutput);
expect(wrapped.stderr).toBe("");
});
posixIt("uses Corepack when pnpm is not directly available on PATH", () => {
const tempDir = path.join(fixturesRoot, "corepack");
const corepackPath = path.join(tempDir, "corepack");
expect(
resolvePnpmRunner({
npmExecPath: "",
env: { PATH: tempDir },
pnpmArgs: ["exec", "tsdown"],
platform: "darwin",
}),
).toEqual({
command: corepackPath,
args: ["pnpm", "exec", "tsdown"],
shell: false,
});
});
posixIt("prefers a direct pnpm executable over Corepack", () => {
const tempDir = path.join(fixturesRoot, "path");
const pnpmPath = path.join(tempDir, "pnpm");
expect(
resolvePnpmRunner({
npmExecPath: "",
env: { PATH: tempDir },
pnpmArgs: ["exec", "tsdown"],
platform: "darwin",
}),
).toEqual({
command: pnpmPath,
args: ["exec", "tsdown"],
shell: false,
});
});
it("wraps pnpm.cmd via cmd.exe on Windows when npm_execpath is unavailable", () => {
expect(
resolvePnpmRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
env: { PATH: "" },
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("ignores ambient ComSpec when defaulting the Windows cmd shim launcher", () => {
expect(
resolvePnpmRunner({
env: {
ComSpec: "C:\\Users\\test\\bin\\cmd.exe",
PATH: "",
SystemRoot: "D:\\Windows",
},
npmExecPath: "",
pnpmArgs: ["exec", "vitest", "run"],
platform: "win32",
}),
).toEqual({
command: "D:\\Windows\\System32\\cmd.exe",
args: ["/d", "/s", "/c", "pnpm.cmd exec vitest run"],
shell: false,
windowsVerbatimArguments: true,
});
});
it("uses Corepack on Windows when no pnpm shim is available", () => {
const tempDir = path.join(fixturesRoot, "windows");
const corepackPath = path.join(tempDir, "corepack.cmd");
expect(
resolvePnpmRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
npmExecPath: "",
env: { Path: tempDir, PATHEXT: ".CMD;.EXE" },
pnpmArgs: ["exec", "vitest", "run"],
platform: "win32",
}),
).toEqual({
command: "C:\\Windows\\System32\\cmd.exe",
args: [
"/d",
"/s",
"/c",
buildCmdExeCommandLine(corepackPath, ["pnpm", "exec", "vitest", "run"]),
],
shell: false,
windowsVerbatimArguments: true,
});
});
it("escapes caret arguments for Windows cmd.exe", () => {
expect(
resolvePnpmRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
env: { PATH: "" },
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,
});
});
it("builds a shared spawn spec with inherited stdio and env overrides", () => {
const env = { PATH: "/custom/bin", FOO: "bar" };
expect(
createPnpmRunnerSpawnSpec({
cwd: "/repo",
detached: true,
npmExecPath: "",
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
env,
}),
).toEqual({
command: "pnpm",
args: ["exec", "vitest", "run"],
options: {
cwd: "/repo",
detached: true,
stdio: "inherit",
env,
shell: false,
windowsVerbatimArguments: undefined,
},
});
});
});