fix(verifiers): launch npm through Node on Windows (#4153)

* fix(verifiers): launch npm through Node on Windows

* test(verifiers): cover Windows npm fallback
This commit is contained in:
Ysopwnable
2026-10-15 23:12:29 +08:00
committed by GitHub
parent 9d6651e5b5
commit 6ae59f3d19
11 changed files with 143 additions and 49 deletions
+1
View File
@@ -741,6 +741,7 @@ jobs:
tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts
tests/boundary/filesystem/webScriptManifestPaths.test.ts
tests/boundary/process/ProcessEvidence.host.test.ts
tests/boundary/process/packageNpmInvocation.test.ts
tests/boundary/process/windowsOutputCapture.test.ts
tests/acceptance/webModuleCli.test.ts
tests/acceptance/webSourceLocationCli.test.ts
+1
View File
@@ -138,6 +138,7 @@ export const SCOPES = {
"src/domain/javascript/javascriptRuntimeStaticCandidates.ts",
"tests/boundary/filesystem/webScriptManifestPaths.test.ts",
"tests/boundary/process/ProcessEvidence.host.test.ts",
"tests/boundary/process/packageNpmInvocation.test.ts",
"tests/boundary/process/windowsOutputCapture.test.ts",
"tests/acceptance/webModuleCli.test.ts",
"tests/acceptance/webSourceLocationCli.test.ts",
+20
View File
@@ -1,5 +1,6 @@
import { execFile } from "node:child_process";
import { lstat } from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { Ajv2020 } from "ajv/dist/2020.js";
@@ -17,6 +18,25 @@ const loadMcpVerifierResults = () => import("./mcp-verifier-results.mjs");
/** Promisified child_process execution helper. */
export const exec = promisify(execFile);
/** Run npm through its JavaScript entry point so Windows command scripts are not required. */
export const execNpm = (arguments_, options = {}) => {
const environment = options.env ?? process.env;
const selectedEntry = environment.npm_execpath;
if (
selectedEntry !== undefined &&
(typeof selectedEntry !== "string" || selectedEntry.length === 0)
)
throw new Error("npm_execpath must name the selected npm CLI entry point");
const entry =
selectedEntry ??
(process.platform === "win32"
? join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js")
: undefined);
return entry === undefined
? exec("npm", arguments_, options)
: exec(process.execPath, [entry, ...arguments_], options);
};
/** Run a command and return its stdout. */
export const run = async (command, args, env) =>
(await exec(command, args, { env })).stdout;
+4 -6
View File
@@ -2,7 +2,7 @@ import { rm } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { exec, json, run } from "./lib/verify-package-core.mjs";
import { exec, execNpm, json, run } from "./lib/verify-package-core.mjs";
/** Install the packed CLI globally with and without optional dependencies and verify PTY degradation. */
export async function verifyPackageInstall({
@@ -11,8 +11,7 @@ export async function verifyPackageInstall({
workspace,
environment,
}) {
await exec(
"npm",
await execNpm(
[
"install",
"--global",
@@ -53,8 +52,7 @@ export async function verifyPackageInstall({
`packaged PTY backend failed without lifecycle scripts: ${JSON.stringify(processCaptureCapability)}`,
);
const noOptionalPrefix = join(workspace, "prefix-no-optional");
await exec(
"npm",
await execNpm(
[
"install",
"--global",
@@ -108,7 +106,7 @@ export async function verifyPackageInstall({
const verifyInstalledPackageCanRepack = async (prefix, environment) => {
const installedPackageRoot = join(prefix, "lib/node_modules/rea-agents");
const repackedTarball = (
await exec("npm", ["pack", "--silent"], {
await execNpm(["pack", "--silent"], {
cwd: installedPackageRoot,
env: environment,
})
+4 -8
View File
@@ -2,7 +2,7 @@ import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
import { exec } from "./lib/verify-package-core.mjs";
import { exec, execNpm } from "./lib/verify-package-core.mjs";
import { verifyPackedBridge } from "./verify-packed-bridge.mjs";
/** Create a tarball and assert production packaging constraints. */
@@ -12,13 +12,9 @@ export async function verifyPackagePack({ root, workspace }) {
const tarball = join(
packDirectory,
(
await exec(
"npm",
["pack", "--silent", "--pack-destination", packDirectory],
{
cwd: root,
},
)
await execNpm(["pack", "--silent", "--pack-destination", packDirectory], {
cwd: root,
})
).stdout.trim(),
);
const packedFiles = (await exec("tar", ["-tf", tarball])).stdout.split("\n");
+3 -9
View File
@@ -28,6 +28,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { execNpm } from "./lib/verify-package-core.mjs";
if (process.platform !== "win32" || process.arch !== "x64")
throw new Error(
@@ -82,15 +83,10 @@ let packageRoot = resolve(packageRootArgument ?? ".");
// package roots support an already-installed artifact without a second install.
if (packageRootArgument === undefined) {
try {
const npmEntry =
process.env.npm_execpath ??
join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js");
const packed = JSON.parse(
(
await exec(
process.execPath,
await execNpm(
[
npmEntry,
"pack",
"--json",
"--ignore-scripts",
@@ -107,10 +103,8 @@ if (packageRootArgument === undefined) {
"The package lane must produce one artifact.",
);
const prefix = join(workspace, "installed");
await exec(
process.execPath,
await execNpm(
[
npmEntry,
"install",
"--prefix",
prefix,
+2 -3
View File
@@ -2,7 +2,7 @@
import { join } from "node:path";
import { mkdir } from "node:fs/promises";
import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
import { exec } from "./lib/verify-package-core.mjs";
import { exec, execNpm } from "./lib/verify-package-core.mjs";
import { WABT_WORKFLOW_TIMEOUT_MS } from "./lib/wabt-verifier-timeouts.mjs";
import { verifyPackagePack } from "./verify-package-pack.mjs";
@@ -24,8 +24,7 @@ try {
XDG_CONFIG_HOME: join(home, ".config"),
XDG_CACHE_HOME: join(home, ".cache"),
};
await exec(
"npm",
await execNpm(
[
"install",
"--global",
+2 -3
View File
@@ -1,7 +1,7 @@
import { cp, mkdir, readFile, readdir } from "node:fs/promises";
import { dirname, join, relative } from "node:path";
import { exec, pathExists } from "./lib/verify-package-core.mjs";
import { exec, execNpm, pathExists } from "./lib/verify-package-core.mjs";
/** Exercise Skills' default root discovery without ignored build outputs. */
export async function verifyRepositorySkill({ root, workspace }) {
@@ -34,8 +34,7 @@ export async function verifyRepositorySkill({ root, workspace }) {
await mkdir(consumer, { recursive: true });
await mkdir(home, { recursive: true });
const skillsVersion = "1.7.2";
await exec(
"npm",
await execNpm(
[
"exec",
"--yes",
+10 -20
View File
@@ -18,7 +18,10 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
import {
execNpm,
verifyCompleteToolCatalog,
} from "./lib/verify-package-core.mjs";
import { verifyPackageUpdate } from "./verify-package-update.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
@@ -32,9 +35,9 @@ let report;
try {
const packed = JSON.parse(
(
await npm(
await execNpm(
["pack", "--json", "--silent", "--pack-destination", workspace],
root,
{ cwd: root, windowsHide: true },
)
).stdout,
);
@@ -63,9 +66,9 @@ try {
const tarball = join(workspace, packageResult.filename);
await access(tarball);
await npm(
await execNpm(
["install", "--no-package-lock", "--no-save", "--prefix", prefix, tarball],
workspace,
{ cwd: workspace, windowsHide: true },
);
const entry = join(
prefix,
@@ -209,9 +212,9 @@ try {
throw new Error(`Packaged uninstall did not remove ${id}`);
if (process.platform !== "win32")
await npm(
await execNpm(
["install", "--global", "--ignore-scripts", "--prefix", prefix, tarball],
workspace,
{ cwd: workspace, windowsHide: true },
);
const update = await verifyPackageUpdate({
prefix,
@@ -241,16 +244,3 @@ try {
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), ...report })}\n`,
);
function npm(arguments_, cwd) {
const npmExecPath = process.env.npm_execpath;
return npmExecPath === undefined
? exec(process.platform === "win32" ? "npm.cmd" : "npm", arguments_, {
cwd,
windowsHide: true,
})
: exec(process.execPath, [npmExecPath, ...arguments_], {
cwd,
windowsHide: true,
});
}
@@ -0,0 +1,90 @@
import { execFile } from "node:child_process";
import { copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { expect, it } from "vitest";
const execute = promisify(execFile);
it("runs the caller-selected npm entry through Node without rewriting its inputs", async () => {
const root = await mkdtemp(join(tmpdir(), "rea-npm-invocation-"));
try {
const npmEntry = join(root, "selected npm entry.mjs");
await writeFile(
npmEntry,
`process.stdout.write(JSON.stringify({ arguments: process.argv.slice(2), cwd: process.cwd(), marker: process.env.REA_NPM_TEST_MARKER }));\n`,
);
const helper = pathToFileURL(
resolve("scripts/lib/verify-package-core.mjs"),
).href;
const program = `
const { execNpm } = await import(${JSON.stringify(helper)});
const result = await execNpm(["pack", "value with spaces"], {
cwd: ${JSON.stringify(root)},
env: { ...process.env, npm_execpath: ${JSON.stringify(npmEntry)}, REA_NPM_TEST_MARKER: "caller-selected" },
});
process.stdout.write(result.stdout);
`;
const result = await execute(
process.execPath,
["--input-type=module", "--eval", program],
{ windowsHide: true },
);
expect(JSON.parse(result.stdout)).toEqual({
arguments: ["pack", "value with spaces"],
cwd: root,
marker: "caller-selected",
});
} finally {
await rm(root, { recursive: true, force: true });
}
});
const windowsIt = process.platform === "win32" ? it : it.skip;
windowsIt(
"uses the npm entry adjacent to Node when none is selected",
async () => {
const root = await mkdtemp(join(tmpdir(), "rea-adjacent-npm-"));
try {
const node = join(root, "node.exe");
const npmEntry = join(root, "node_modules/npm/bin/npm-cli.js");
await mkdir(dirname(npmEntry), { recursive: true });
await Promise.all([
copyFile(process.execPath, node),
writeFile(
npmEntry,
`process.stdout.write(JSON.stringify({ arguments: process.argv.slice(2), cwd: process.cwd() }));\n`,
),
]);
const helper = pathToFileURL(
resolve("scripts/lib/verify-package-core.mjs"),
).href;
const program = `
const { execNpm } = await import(${JSON.stringify(helper)});
const environment = { ...process.env };
delete environment.npm_execpath;
const result = await execNpm(["pack", "value with spaces"], {
cwd: ${JSON.stringify(root)},
env: environment,
});
process.stdout.write(result.stdout);
`;
const environment = { ...process.env };
delete environment.npm_execpath;
const result = await execute(
node,
["--input-type=module", "--eval", program],
{ env: environment, windowsHide: true },
);
expect(JSON.parse(result.stdout)).toEqual({
arguments: ["pack", "value with spaces"],
cwd: root,
});
} finally {
await rm(root, { recursive: true, force: true });
}
},
);
@@ -69,6 +69,12 @@ describe("curated Windows test lane", () => {
"tests/boundary/process/windowsOutputCapture.test.ts",
);
});
it("keeps the adjacent npm fallback regression in the lane", async () => {
expect(await explicitPaths("windows")).toContain(
"tests/boundary/process/packageNpmInvocation.test.ts",
);
});
});
it("keeps every explicit focused CI test path executable after file moves", async () => {