From 1bbf4bd16ca134cdb2187eaf29291cae90d14a57 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:06:41 +0800 Subject: [PATCH] fix(process): preserve caller overrides during Windows environment normalization --- src/process/snapshotEnvironment.test.ts | 35 ++++++++++++++----------- src/process/snapshotEnvironment.ts | 16 +++++------ src/setupEnvironment.test.ts | 29 ++++++++++++++++---- 3 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/process/snapshotEnvironment.test.ts b/src/process/snapshotEnvironment.test.ts index 9dfbf1e0a6..4012262993 100644 --- a/src/process/snapshotEnvironment.test.ts +++ b/src/process/snapshotEnvironment.test.ts @@ -25,19 +25,24 @@ it("canonicalizes selected Windows variables without inheriting ambient values", expect(Object.isFrozen(snapshot)).toBe(true); }); -it("uses Node's first lexicographic Windows key even when it is undefined", () => { - expect( - snapshotEnvironment( - { - path: "lower", - Path: "mixed", - PATH: "upper", - ComSpec: "mixed-cmd", - COMSPEC: "upper-cmd", - Temp: "mixed-temp", - TEMP: undefined, - }, - "win32", - ), - ).toEqual({ PATH: "upper", COMSPEC: "upper-cmd", TEMP: undefined }); +it("keeps the last caller value for Windows aliases, including undefined", () => { + const snapshot = snapshotEnvironment( + { + PATH: "upper", + path: "lower", + COMSPEC: "upper-cmd", + ComSpec: "mixed-cmd", + TEMP: "ambient-temp", + temp: undefined, + }, + "win32", + ); + + expect(snapshot).toEqual({ + COMSPEC: "mixed-cmd", + PATH: "lower", + TEMP: undefined, + }); + expect(Object.keys(snapshot)).toEqual(["COMSPEC", "PATH", "TEMP"]); + expect(Object.isFrozen(snapshot)).toBe(true); }); diff --git a/src/process/snapshotEnvironment.ts b/src/process/snapshotEnvironment.ts index 2bb54302f3..bfb57cd7d6 100644 --- a/src/process/snapshotEnvironment.ts +++ b/src/process/snapshotEnvironment.ts @@ -1,16 +1,16 @@ -/** Snapshot caller-selected variables using Node child-process key semantics. */ +/** Snapshot selected variables while preserving platform and caller precedence. */ export const snapshotEnvironment = ( environment: Readonly, platform: NodeJS.Platform = process.platform, ): Readonly => { if (platform !== "win32") return Object.freeze({ ...environment }); + const selected = new Map(); + // Fold aliases in caller order so an explicit later override wins before + // this canonical environment crosses a Windows process boundary. + for (const [key, value] of Object.entries(environment)) + selected.set(key.toUpperCase(), value); const snapshot: NodeJS.ProcessEnv = {}; - // Node sorts keys lexicographically and forwards the first case-insensitive - // match on Windows. Canonical keys also make subsequent lookups deterministic. - for (const key of Object.keys(environment).sort()) { - const canonicalKey = key.toUpperCase(); - if (!Object.hasOwn(snapshot, canonicalKey)) - snapshot[canonicalKey] = environment[key]; - } + for (const key of [...selected.keys()].sort()) + snapshot[key] = selected.get(key); return Object.freeze(snapshot); }; diff --git a/src/setupEnvironment.test.ts b/src/setupEnvironment.test.ts index 5921d4d6fb..bd78ad31cd 100644 --- a/src/setupEnvironment.test.ts +++ b/src/setupEnvironment.test.ts @@ -213,11 +213,30 @@ it.each([ }, ); -it("prioritizes overridden package runner environment variables over ambient keys with differing case on Windows", () => { - const environment = { - NPM_COMMAND: "run", - npm_command: "exec", +it("registers the selected Windows package runner after canonicalizing environment aliases", async () => { + const environment = snapshotEnvironment( + { + NPM_COMMAND: "run", + npm_command: "exec", + NPM_CONFIG_USER_AGENT: "npm/10.9.0 node/v24.11.0 win32 x64", + }, + "win32", + ); + expect(environment).toEqual({ + NPM_COMMAND: "exec", NPM_CONFIG_USER_AGENT: "npm/10.9.0 node/v24.11.0 win32 x64", - }; + }); expect(invokingPackageRunner(environment, "win32")).toBe("npx"); + + const host = systemSetupHost( + createDoctorHostFixture({ platform: "win32" }), + environment, + ); + try { + expect(host.registrationCommand).toEqual( + packageRunnerRegistrationCommand("npx", "win32"), + ); + } finally { + await host.close?.(); + } });