fix(coding-agent): use cross-spawn for Windows shims

closes #4665
This commit is contained in:
Mario Zechner
2026-05-18 11:30:41 +02:00
parent b8f51957a0
commit 64150668b8
8 changed files with 124 additions and 153 deletions

View File

@@ -6,7 +6,6 @@ import { PassThrough } from "node:stream";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DefaultPackageManager, type ProgressEvent, type ResolvedResource } from "../src/core/package-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
import { resolveSpawnCommand } from "../src/utils/child-process.js";
function normalizeForMatch(value: string): string {
return value.replace(/\\/g, "/");
@@ -617,59 +616,19 @@ Content`,
});
});
describe("windows command spawning", () => {
it("should keep unresolved executables as argv commands", () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
describe("command spawning", () => {
it("should preserve argv entries containing spaces", () => {
const managerWithInternals = packageManager as unknown as {
runCommandSync(command: string, args: string[]): string;
};
const valueWithSpace = "C:\\Users\\A B\\.pi\\npm";
const output = managerWithInternals.runCommandSync(process.execPath, [
"-e",
"console.log(process.argv[1])",
valueWithSpace,
]);
const resolved = resolveSpawnCommand("git", ["clone", "repo", "C:\\Users\\A B\\repo"], {
env: { PATH: tempDir },
});
expect(resolved).toEqual({ command: "git", args: ["clone", "repo", "C:\\Users\\A B\\repo"] });
});
it("should prefer Windows executables over command scripts", () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const binDir = join(tempDir, "bin");
mkdirSync(binDir, { recursive: true });
writeFileSync(join(binDir, "npm.exe"), "");
writeFileSync(join(binDir, "npm.cmd"), "@echo off\r\n");
const args = ["install", "pkg", "--prefix", "C:\\Users\\A B\\.pi\\npm"];
const resolved = resolveSpawnCommand("npm", args, { env: { PATH: binDir } });
expect(resolved).toEqual({ command: join(binDir, "npm.exe"), args });
});
it("should resolve npm command shims to their Node entrypoint without a shell", () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const binDir = join(tempDir, "node-bin");
const npmCli = join(binDir, "node_modules", "npm", "bin", "npm-cli.js");
mkdirSync(join(binDir, "node_modules", "npm", "bin"), { recursive: true });
writeFileSync(join(binDir, "node.exe"), "");
writeFileSync(npmCli, "");
writeFileSync(
join(binDir, "npm.cmd"),
'@ECHO off\r\nSET "dp0=%~dp0"\r\nSET "_prog=%dp0%\\node.exe"\r\nendLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\\node_modules\\npm\\bin\\npm-cli.js" %*\r\n',
);
const args = ["install", "pkg", "--prefix", "C:\\Users\\A B\\.pi\\npm"];
const resolved = resolveSpawnCommand("npm", args, { env: { PATH: binDir } });
expect(resolved).toEqual({ command: join(binDir, "node.exe"), args: [npmCli, ...args] });
});
it("should reject unrecognized Windows command scripts instead of using cmd.exe", () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const binDir = join(tempDir, "bin");
mkdirSync(binDir, { recursive: true });
writeFileSync(join(binDir, "npm.cmd"), "@echo off\r\necho %*\r\n");
expect(() =>
resolveSpawnCommand("npm", ["install", "pkg"], {
env: { PATH: binDir },
}),
).toThrow("Refusing to run Windows command shim without a shell");
expect(output).toBe(valueWithSpace);
});
});