fix(process): preserve caller overrides during Windows environment normalization

This commit is contained in:
morluto committed 2026-10-15 04:06:41 +08:00
1 parent 07a501e82c
commit 1bbf4bd16c
3 files changed
+52 -28

No files matched your search

+20 -15
View File
@@ -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);
});
+8 -8
View File
@@ -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<NodeJS.ProcessEnv>,
platform: NodeJS.Platform = process.platform,
): Readonly<NodeJS.ProcessEnv> => {
if (platform !== "win32") return Object.freeze({ ...environment });
const selected = new Map<string, string | undefined>();
// 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);
};
+24 -5
View File
@@ -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?.();
}
});