From 968fb69372bc996154f5057208a359d09ebe10e1 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Thu, 9 Jul 2026 02:47:14 +0200 Subject: [PATCH 1/3] fix(cli): canonicalize Windows PATH env key so local builds find bun On Windows the PATH variable is typically keyed as `Path` (its registry casing). The compute SDK spreads process.env into a plain, case-sensitive object when it prepends node_modules/.bin directories for a local build, so `baseEnv.PATH` reads undefined: the rebuilt PATH drops every inherited entry and is written as a second, truncated `PATH` key next to `Path`. In the spawned shell's case-insensitive environment block the truncated key wins, and `bun run build` fails with "'bun' is not recognized as an internal or external command" even though Bun is installed and on Path. Collapse case-variants of PATH into the canonical `PATH` key at CLI startup, before any command delegates to the SDK. Windows resolves environment variables case-insensitively, so the rename is invisible to this process and its children. Co-Authored-By: Claude Fable 5 --- packages/cli/src/cli.ts | 6 +++ packages/cli/src/shell/path-env.ts | 44 ++++++++++++++++++ packages/cli/tests/path-env.test.ts | 71 +++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 packages/cli/src/shell/path-env.ts create mode 100644 packages/cli/tests/path-env.test.ts diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 04dec58..2f9c872 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -23,6 +23,7 @@ import { writeJsonError, writeJsonSuccess, } from "./shell/output"; +import { canonicalizeWindowsPathKey } from "./shell/path-env"; import { disposePromptState } from "./shell/prompt"; import { type CliRuntime, @@ -37,6 +38,11 @@ export interface RunCliOptions extends Partial { } export async function runCli(options: RunCliOptions = {}): Promise { + // The compute SDK spreads process.env into plain objects when building the + // environment for local build subprocesses; a Windows `Path` key breaks + // that (see canonicalizeWindowsPathKey), so normalize before any command. + canonicalizeWindowsPathKey(process.env); + const runtime = resolveRuntime(options); const program = createProgram(runtime); process.exitCode = 0; diff --git a/packages/cli/src/shell/path-env.ts b/packages/cli/src/shell/path-env.ts new file mode 100644 index 0000000..31ece48 --- /dev/null +++ b/packages/cli/src/shell/path-env.ts @@ -0,0 +1,44 @@ +/** + * Collapses case-variant spellings of the `PATH` environment variable into + * the canonical `PATH` key, in place. No-op outside Windows. + * + * Windows typically keys the variable as `Path` (its registry casing), and + * `process.env` papers over that with case-insensitive lookups. Anything that + * spreads `process.env` into a plain object — the compute SDK does this when + * it prepends `node_modules/.bin` directories for a local build — loses that + * case-insensitivity: reading `env.PATH` returns undefined, so the rewritten + * PATH drops every inherited entry, and writing `env.PATH` forks a second key + * alongside `Path`. The child's environment block is case-insensitive again, + * so the truncated `PATH` clobbers the real `Path` and the spawned build + * cannot resolve commands like `bun` ("'bun' is not recognized ..."). + * + * Windows itself resolves environment variables case-insensitively, so + * renaming the key to `PATH` is invisible to this process and its children. + */ +export function canonicalizeWindowsPathKey( + env: NodeJS.ProcessEnv, + platform: NodeJS.Platform = process.platform, +): void { + if (platform !== "win32") { + return; + } + + const variants = Object.keys(env).filter( + (key) => key.toUpperCase() === "PATH" && key !== "PATH", + ); + if (variants.length === 0) { + return; + } + + // On the real Windows process.env, `env.PATH` already resolves the `Path` + // entry case-insensitively; a plain-object copy needs the variant lookup. + const value = env.PATH ?? env[variants[0]]; + for (const variant of variants) { + delete env[variant]; + } + // On process.env the deletes above also remove the case-insensitive `PATH` + // entry itself, so always write the canonical key back. + if (value !== undefined) { + env.PATH = value; + } +} diff --git a/packages/cli/tests/path-env.test.ts b/packages/cli/tests/path-env.test.ts new file mode 100644 index 0000000..d2e5ef9 --- /dev/null +++ b/packages/cli/tests/path-env.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; + +import { canonicalizeWindowsPathKey } from "../src/shell/path-env"; + +describe("canonicalizeWindowsPathKey", () => { + it("renames the Windows registry-cased Path key to PATH", () => { + const env: NodeJS.ProcessEnv = { + Path: "C:\\Windows\\System32;C:\\Users\\flo\\.bun\\bin", + ComSpec: "C:\\Windows\\System32\\cmd.exe", + }; + + canonicalizeWindowsPathKey(env, "win32"); + + expect(Object.keys(env).sort()).toEqual(["ComSpec", "PATH"]); + expect(env.PATH).toBe("C:\\Windows\\System32;C:\\Users\\flo\\.bun\\bin"); + }); + + it("keeps a spread of the env readable and rewritable via env.PATH", () => { + // The failure mode from the field: the compute SDK spreads process.env + // into a plain object and rebuilds PATH from `baseEnv.PATH`. With the + // Windows `Path` casing that read is undefined, so the rebuilt PATH loses + // every inherited entry and the spawned `bun run build` cannot find bun. + const env: NodeJS.ProcessEnv = { Path: "C:\\Users\\flo\\.bun\\bin" }; + + canonicalizeWindowsPathKey(env, "win32"); + const spread = { ...env }; + + expect(spread.PATH).toBe("C:\\Users\\flo\\.bun\\bin"); + expect( + Object.keys(spread).filter((key) => key.toUpperCase() === "PATH"), + ).toEqual(["PATH"]); + }); + + it("collapses multiple case variants without dropping the value", () => { + const env: NodeJS.ProcessEnv = { + path: "ignored", + PATH: "C:\\kept", + }; + + canonicalizeWindowsPathKey(env, "win32"); + + expect(env).toEqual({ PATH: "C:\\kept" }); + }); + + it("leaves a canonical PATH untouched", () => { + const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows\\System32" }; + + canonicalizeWindowsPathKey(env, "win32"); + + expect(env).toEqual({ PATH: "C:\\Windows\\System32" }); + }); + + it("does nothing when no path variable exists", () => { + const env: NodeJS.ProcessEnv = { HOME: "C:\\Users\\flo" }; + + canonicalizeWindowsPathKey(env, "win32"); + + expect(env).toEqual({ HOME: "C:\\Users\\flo" }); + }); + + it("is a no-op outside Windows, where casing is significant", () => { + const env: NodeJS.ProcessEnv = { + Path: "/opt/custom", + PATH: "/usr/bin:/bin", + }; + + canonicalizeWindowsPathKey(env, "linux"); + + expect(env).toEqual({ Path: "/opt/custom", PATH: "/usr/bin:/bin" }); + }); +}); From 81be1fa9550c4ab6c15669175397aa6cc6c4df53 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:04:40 +0200 Subject: [PATCH 2/3] refactor(cli): deterministic PATH precedence + integration coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback on the Windows PATH canonicalization fix: - Give the multi-variant case a fixed precedence (existing `PATH`, then the Windows-native `Path`, then any remaining spelling) so the result no longer depends on Object.keys insertion order. - Trim the helper's top-level comment to the "why"; the detailed spread failure mode now lives next to the test that reproduces it. - Add an integration test that drives the real end-to-end path — process.env spread into a plain object, PATH rebuilt with node_modules/.bin prepended (the compute SDK's shape), then a spawned shell child that must still resolve a binary on the inherited path. Co-Authored-By: Claude Fable 5 --- packages/cli/src/shell/path-env.ts | 38 +++--- .../cli/tests/path-env.integration.test.ts | 112 ++++++++++++++++++ packages/cli/tests/path-env.test.ts | 19 +++ 3 files changed, 153 insertions(+), 16 deletions(-) create mode 100644 packages/cli/tests/path-env.integration.test.ts diff --git a/packages/cli/src/shell/path-env.ts b/packages/cli/src/shell/path-env.ts index 31ece48..fb3354c 100644 --- a/packages/cli/src/shell/path-env.ts +++ b/packages/cli/src/shell/path-env.ts @@ -2,18 +2,17 @@ * Collapses case-variant spellings of the `PATH` environment variable into * the canonical `PATH` key, in place. No-op outside Windows. * - * Windows typically keys the variable as `Path` (its registry casing), and - * `process.env` papers over that with case-insensitive lookups. Anything that - * spreads `process.env` into a plain object — the compute SDK does this when - * it prepends `node_modules/.bin` directories for a local build — loses that - * case-insensitivity: reading `env.PATH` returns undefined, so the rewritten - * PATH drops every inherited entry, and writing `env.PATH` forks a second key - * alongside `Path`. The child's environment block is case-insensitive again, - * so the truncated `PATH` clobbers the real `Path` and the spawned build - * cannot resolve commands like `bun` ("'bun' is not recognized ..."). + * Windows keys the variable as `Path` (its registry casing) and resolves + * environment lookups case-insensitively, so `process.env.PATH` works even + * though the underlying key is `Path`. Code that spreads `process.env` into a + * plain object loses that case-insensitivity — the compute SDK does exactly + * this when it prepends `node_modules/.bin` to PATH for a local build, and the + * result is a truncated PATH that leaves the spawned build unable to resolve + * `bun`/`next`. Normalizing to `PATH` up front keeps that spread correct. + * See tests/path-env.test.ts for the reproduced failure mode. * - * Windows itself resolves environment variables case-insensitively, so - * renaming the key to `PATH` is invisible to this process and its children. + * Renaming `Path` -> `PATH` is invisible to this process and its children + * because Windows resolves the lookup case-insensitively either way. */ export function canonicalizeWindowsPathKey( env: NodeJS.ProcessEnv, @@ -30,15 +29,22 @@ export function canonicalizeWindowsPathKey( return; } - // On the real Windows process.env, `env.PATH` already resolves the `Path` - // entry case-insensitively; a plain-object copy needs the variant lookup. - const value = env.PATH ?? env[variants[0]]; + // Precedence when several spellings coexist, so the result never depends on + // key insertion order: an existing canonical `PATH`, then the Windows-native + // `Path`, then any remaining variant. On the real process.env, `env.PATH` + // already reflects the `Path` value case-insensitively. + const value = env.PATH ?? env.Path ?? env[selectPreferredVariant(variants)]; for (const variant of variants) { delete env[variant]; } - // On process.env the deletes above also remove the case-insensitive `PATH` - // entry itself, so always write the canonical key back. + // Deleting the variants above also clears the case-insensitive `PATH` entry + // on the real process.env, so always write the canonical key back. if (value !== undefined) { env.PATH = value; } } + +/** The Windows-native `Path` if present, otherwise the first variant. */ +function selectPreferredVariant(variants: string[]): string { + return variants.includes("Path") ? "Path" : variants[0]; +} diff --git a/packages/cli/tests/path-env.integration.test.ts b/packages/cli/tests/path-env.integration.test.ts new file mode 100644 index 0000000..14d1b2e --- /dev/null +++ b/packages/cli/tests/path-env.integration.test.ts @@ -0,0 +1,112 @@ +import { spawn } from "node:child_process"; +import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { canonicalizeWindowsPathKey } from "../src/shell/path-env"; + +// Guards the end-to-end failure path the fix exists for: +// process.env -> spread/copy -> PATH rewrite -> spawned child +// The compute SDK builds a local build subprocess exactly this way — it +// spreads the inherited env and prepends `node_modules/.bin` to PATH — so we +// reproduce that env shape here and assert a real child can still resolve a +// binary on the inherited path (the `bun` the report could not find). + +const isWindows = process.platform === "win32"; + +/** + * Mirrors `@prisma/compute-sdk`'s `buildCommandEnv`: spread the inherited env + * into a plain object, then rebuild PATH with extra bin dirs prepended. This + * is the operation that silently truncates PATH when the inherited key is the + * Windows-native `Path` rather than `PATH`. + */ +function buildCommandEnvLikeSdk( + baseEnv: NodeJS.ProcessEnv, + binDirs: string[], +): NodeJS.ProcessEnv { + const spread = { ...baseEnv }; + return { + ...spread, + PATH: [...binDirs, spread.PATH].filter(Boolean).join(path.delimiter), + }; +} + +function runCommandInShell( + command: string, + env: NodeJS.ProcessEnv, + cwd: string, +): Promise<{ code: number | null; stdout: string; stderr: string }> { + return new Promise((resolve, reject) => { + const child = spawn(command, [], { cwd, env, shell: true }); + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk) => { + stdout += chunk; + }); + child.stderr.on("data", (chunk) => { + stderr += chunk; + }); + child.on("error", reject); + child.on("close", (code) => resolve({ code, stdout, stderr })); + }); +} + +describe("local build env resolves binaries (integration)", () => { + let workdir: string; + let binDir: string; + const binaryName = "faux-bun"; + + beforeEach(async () => { + workdir = await mkdtemp(path.join(os.tmpdir(), "prisma-path-env-")); + binDir = path.join(workdir, "tools"); + await mkdir(binDir, { recursive: true }); + + if (isWindows) { + // A .cmd shim is what a real installer drops on Windows. + await writeFile( + path.join(binDir, `${binaryName}.cmd`), + "@echo faux-bun-ran\r\n", + ); + } else { + const script = path.join(binDir, binaryName); + await writeFile(script, "#!/bin/sh\necho faux-bun-ran\n"); + await chmod(script, 0o755); + } + }); + + afterEach(async () => { + await rm(workdir, { recursive: true, force: true }); + }); + + it("spawns a child that finds a binary on the inherited path after the SDK-style rewrite", async () => { + // Inherit the real environment, but place our binary dir on the path + // under the OS-native key: `Path` on Windows (where the bug lives), the + // canonical `PATH` elsewhere. + const baseEnv: NodeJS.ProcessEnv = { ...process.env }; + const pathKey = isWindows ? "Path" : "PATH"; + const existingPath = process.env.PATH ?? ""; + delete baseEnv.PATH; + delete baseEnv.Path; + baseEnv[pathKey] = [binDir, existingPath] + .filter(Boolean) + .join(path.delimiter); + + // The fix: normalize before the env is spread for the subprocess. + canonicalizeWindowsPathKey(baseEnv); + + // The node_modules/.bin dir the SDK prepends; empty here, but it forces the + // PATH rewrite that dropped inherited entries in the reported failure. + const childEnv = buildCommandEnvLikeSdk(baseEnv, [ + path.join(workdir, "node_modules", ".bin"), + ]); + + const result = await runCommandInShell(binaryName, childEnv, workdir); + + expect(result.stdout.trim()).toBe("faux-bun-ran"); + expect(result.code).toBe(0); + }); +}); diff --git a/packages/cli/tests/path-env.test.ts b/packages/cli/tests/path-env.test.ts index d2e5ef9..2e75a5a 100644 --- a/packages/cli/tests/path-env.test.ts +++ b/packages/cli/tests/path-env.test.ts @@ -42,6 +42,25 @@ describe("canonicalizeWindowsPathKey", () => { expect(env).toEqual({ PATH: "C:\\kept" }); }); + it("prefers the Windows-native Path over other variants, independent of insertion order", () => { + // With several non-canonical spellings and no `PATH`, precedence is fixed + // (Path wins) rather than dependent on Object.keys insertion order. + const pathFirst: NodeJS.ProcessEnv = { + path: "C:\\other", + Path: "C:\\windows-native", + }; + const pathLast: NodeJS.ProcessEnv = { + Path: "C:\\windows-native", + path: "C:\\other", + }; + + canonicalizeWindowsPathKey(pathFirst, "win32"); + canonicalizeWindowsPathKey(pathLast, "win32"); + + expect(pathFirst).toEqual({ PATH: "C:\\windows-native" }); + expect(pathLast).toEqual({ PATH: "C:\\windows-native" }); + }); + it("leaves a canonical PATH untouched", () => { const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows\\System32" }; From 88e6c30799d4c6154a8a5d9aa10d8b1dc0120ed0 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:03:09 +0200 Subject: [PATCH 3/3] refactor(cli): drop dead PATH variant helper, trim comments Apply review feedback from Kristof and CodeRabbit: - Remove `selectPreferredVariant`: its `"Path"` branch was dead code, since `env.PATH` already resolves `Path` case-insensitively on the real process.env and short-circuits before the helper runs. Replace with a sort over the remaining variants so the fallback is genuinely independent of key insertion order (the earlier claim relied on the dead branch). - Rework the precedence test to exercise that sorted fallback with exotic spellings (`PaTH`), the path CodeRabbit noted the old test never hit. - Strip comments across the change down to the non-trivial "why". Co-Authored-By: Claude Fable 5 --- packages/cli/src/cli.ts | 5 +-- packages/cli/src/shell/path-env.ts | 40 +++++++------------ .../cli/tests/path-env.integration.test.ts | 32 ++++++--------- packages/cli/tests/path-env.test.ts | 33 +++++++-------- 4 files changed, 44 insertions(+), 66 deletions(-) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 2f9c872..b33a219 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -38,9 +38,8 @@ export interface RunCliOptions extends Partial { } export async function runCli(options: RunCliOptions = {}): Promise { - // The compute SDK spreads process.env into plain objects when building the - // environment for local build subprocesses; a Windows `Path` key breaks - // that (see canonicalizeWindowsPathKey), so normalize before any command. + // Normalize the Windows `Path` key before any command spawns a subprocess + // off process.env (see canonicalizeWindowsPathKey). canonicalizeWindowsPathKey(process.env); const runtime = resolveRuntime(options); diff --git a/packages/cli/src/shell/path-env.ts b/packages/cli/src/shell/path-env.ts index fb3354c..9a1e313 100644 --- a/packages/cli/src/shell/path-env.ts +++ b/packages/cli/src/shell/path-env.ts @@ -1,18 +1,14 @@ /** - * Collapses case-variant spellings of the `PATH` environment variable into - * the canonical `PATH` key, in place. No-op outside Windows. + * Collapses case-variant spellings of `PATH` into the canonical `PATH` key, in + * place. No-op outside Windows. * - * Windows keys the variable as `Path` (its registry casing) and resolves - * environment lookups case-insensitively, so `process.env.PATH` works even - * though the underlying key is `Path`. Code that spreads `process.env` into a - * plain object loses that case-insensitivity — the compute SDK does exactly - * this when it prepends `node_modules/.bin` to PATH for a local build, and the - * result is a truncated PATH that leaves the spawned build unable to resolve - * `bun`/`next`. Normalizing to `PATH` up front keeps that spread correct. - * See tests/path-env.test.ts for the reproduced failure mode. - * - * Renaming `Path` -> `PATH` is invisible to this process and its children - * because Windows resolves the lookup case-insensitively either way. + * Windows stores the variable as `Path` and resolves env lookups + * case-insensitively, so `process.env.PATH` reads it fine — but spreading + * `process.env` into a plain object drops that case-insensitivity. The compute + * SDK does exactly that to prepend `node_modules/.bin` for a local build, so + * its `PATH` read misses the inherited `Path`, truncating it until the spawned + * build can no longer resolve `bun`/`next`. Normalizing up front keeps the + * spread correct; the rename is invisible because Windows ignores the casing. */ export function canonicalizeWindowsPathKey( env: NodeJS.ProcessEnv, @@ -29,22 +25,16 @@ export function canonicalizeWindowsPathKey( return; } - // Precedence when several spellings coexist, so the result never depends on - // key insertion order: an existing canonical `PATH`, then the Windows-native - // `Path`, then any remaining variant. On the real process.env, `env.PATH` - // already reflects the `Path` value case-insensitively. - const value = env.PATH ?? env.Path ?? env[selectPreferredVariant(variants)]; + // `env.PATH` resolves the canonical key and, on the real process.env, `Path` + // too; sort the remaining variants so the fallback never depends on key + // insertion order. + const value = env.PATH ?? env[[...variants].sort()[0]]; for (const variant of variants) { delete env[variant]; } - // Deleting the variants above also clears the case-insensitive `PATH` entry - // on the real process.env, so always write the canonical key back. + // The deletes above also clear the case-insensitive `PATH` on the real + // process.env, so write the canonical key back. if (value !== undefined) { env.PATH = value; } } - -/** The Windows-native `Path` if present, otherwise the first variant. */ -function selectPreferredVariant(variants: string[]): string { - return variants.includes("Path") ? "Path" : variants[0]; -} diff --git a/packages/cli/tests/path-env.integration.test.ts b/packages/cli/tests/path-env.integration.test.ts index 14d1b2e..1ff2e1c 100644 --- a/packages/cli/tests/path-env.integration.test.ts +++ b/packages/cli/tests/path-env.integration.test.ts @@ -7,21 +7,17 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { canonicalizeWindowsPathKey } from "../src/shell/path-env"; -// Guards the end-to-end failure path the fix exists for: -// process.env -> spread/copy -> PATH rewrite -> spawned child -// The compute SDK builds a local build subprocess exactly this way — it -// spreads the inherited env and prepends `node_modules/.bin` to PATH — so we -// reproduce that env shape here and assert a real child can still resolve a -// binary on the inherited path (the `bun` the report could not find). +// Guards the whole chain the fix protects: process.env -> spread -> PATH +// rewrite -> spawned child. The compute SDK builds its local-build subprocess +// this way, so on Windows the truncated PATH left the spawned build unable to +// find `bun`; here a real child must still resolve a binary on the inherited +// path. const isWindows = process.platform === "win32"; -/** - * Mirrors `@prisma/compute-sdk`'s `buildCommandEnv`: spread the inherited env - * into a plain object, then rebuild PATH with extra bin dirs prepended. This - * is the operation that silently truncates PATH when the inherited key is the - * Windows-native `Path` rather than `PATH`. - */ +// Mirrors the compute SDK's `buildCommandEnv`: spread the inherited env, then +// rebuild PATH with bin dirs prepended — the step that silently truncated PATH +// when the inherited key was the Windows-native `Path`. function buildCommandEnvLikeSdk( baseEnv: NodeJS.ProcessEnv, binDirs: string[], @@ -66,7 +62,7 @@ describe("local build env resolves binaries (integration)", () => { await mkdir(binDir, { recursive: true }); if (isWindows) { - // A .cmd shim is what a real installer drops on Windows. + // A .cmd shim, as a real installer drops on Windows. await writeFile( path.join(binDir, `${binaryName}.cmd`), "@echo faux-bun-ran\r\n", @@ -83,9 +79,8 @@ describe("local build env resolves binaries (integration)", () => { }); it("spawns a child that finds a binary on the inherited path after the SDK-style rewrite", async () => { - // Inherit the real environment, but place our binary dir on the path - // under the OS-native key: `Path` on Windows (where the bug lives), the - // canonical `PATH` elsewhere. + // Put the binary dir on PATH under the OS-native key: `Path` on Windows + // (where the bug lives), canonical `PATH` elsewhere. const baseEnv: NodeJS.ProcessEnv = { ...process.env }; const pathKey = isWindows ? "Path" : "PATH"; const existingPath = process.env.PATH ?? ""; @@ -95,11 +90,10 @@ describe("local build env resolves binaries (integration)", () => { .filter(Boolean) .join(path.delimiter); - // The fix: normalize before the env is spread for the subprocess. canonicalizeWindowsPathKey(baseEnv); - // The node_modules/.bin dir the SDK prepends; empty here, but it forces the - // PATH rewrite that dropped inherited entries in the reported failure. + // Prepend an (empty) node_modules/.bin as the SDK does, forcing the PATH + // rewrite that dropped inherited entries in the reported failure. const childEnv = buildCommandEnvLikeSdk(baseEnv, [ path.join(workdir, "node_modules", ".bin"), ]); diff --git a/packages/cli/tests/path-env.test.ts b/packages/cli/tests/path-env.test.ts index 2e75a5a..fd3119f 100644 --- a/packages/cli/tests/path-env.test.ts +++ b/packages/cli/tests/path-env.test.ts @@ -16,10 +16,10 @@ describe("canonicalizeWindowsPathKey", () => { }); it("keeps a spread of the env readable and rewritable via env.PATH", () => { - // The failure mode from the field: the compute SDK spreads process.env - // into a plain object and rebuilds PATH from `baseEnv.PATH`. With the - // Windows `Path` casing that read is undefined, so the rebuilt PATH loses - // every inherited entry and the spawned `bun run build` cannot find bun. + // The reported failure mode: the compute SDK spreads process.env into a + // plain object and rebuilds PATH from `baseEnv.PATH`. With the Windows + // `Path` casing that read is undefined, so the rebuilt PATH loses every + // inherited entry and the spawned `bun run build` cannot find bun. const env: NodeJS.ProcessEnv = { Path: "C:\\Users\\flo\\.bun\\bin" }; canonicalizeWindowsPathKey(env, "win32"); @@ -42,23 +42,18 @@ describe("canonicalizeWindowsPathKey", () => { expect(env).toEqual({ PATH: "C:\\kept" }); }); - it("prefers the Windows-native Path over other variants, independent of insertion order", () => { - // With several non-canonical spellings and no `PATH`, precedence is fixed - // (Path wins) rather than dependent on Object.keys insertion order. - const pathFirst: NodeJS.ProcessEnv = { - path: "C:\\other", - Path: "C:\\windows-native", - }; - const pathLast: NodeJS.ProcessEnv = { - Path: "C:\\windows-native", - path: "C:\\other", - }; + it("picks the same variant regardless of insertion order", () => { + // Several non-canonical spellings and no `PATH`: the sorted fallback must + // pick deterministically rather than following Object.keys order. "PaTH" + // sorts before "path" (uppercase precedes lowercase), so it wins both ways. + const first: NodeJS.ProcessEnv = { path: "C:\\other", PaTH: "C:\\kept" }; + const second: NodeJS.ProcessEnv = { PaTH: "C:\\kept", path: "C:\\other" }; - canonicalizeWindowsPathKey(pathFirst, "win32"); - canonicalizeWindowsPathKey(pathLast, "win32"); + canonicalizeWindowsPathKey(first, "win32"); + canonicalizeWindowsPathKey(second, "win32"); - expect(pathFirst).toEqual({ PATH: "C:\\windows-native" }); - expect(pathLast).toEqual({ PATH: "C:\\windows-native" }); + expect(first).toEqual({ PATH: "C:\\kept" }); + expect(second).toEqual({ PATH: "C:\\kept" }); }); it("leaves a canonical PATH untouched", () => {