import * as NodeServices from "@effect/platform-node/NodeServices";import { it as effectIt } from "@effect/vitest";import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Path from "effect/Path";import * as TestClock from "effect/testing/TestClock";import { describe, expect, it, vi } from "vite-plus/test"; import { extractPathFromShellOutput, CommandAvailability, CommandResolutionCache, type CommandAvailabilityChecker, isCommandAvailable, listLoginShellCandidates, mergePathEntries, mergePathValues, readEnvironmentFromLoginShell, readEnvironmentFromWindowsShell, readPathFromLaunchctl, readPathFromLoginShell, resolveCommandPath, resolveKnownWindowsCliDirs, resolveSpawnCommand, resolveWindowsEnvironment, SpawnExecutableResolution, WindowsShellEnvironment, type WindowsShellEnvironmentReader,} from "./shell.ts"; const withWindowsEnvironmentMocks = <A, E, R>( effect: Effect.Effect<A, E, R>, readEnvironment: WindowsShellEnvironmentReader, commandAvailable: CommandAvailabilityChecker,) => effect.pipe( Effect.provideService(WindowsShellEnvironment, readEnvironment), Effect.provideService(CommandAvailability, commandAvailable), ); describe("extractPathFromShellOutput", () => { it("extracts the path between capture markers", () => { expect( extractPathFromShellOutput( "__T3CODE_PATH_START__\n/opt/homebrew/bin:/usr/bin\n__T3CODE_PATH_END__\n", ), ).toBe("/opt/homebrew/bin:/usr/bin"); }); it("ignores shell startup noise around the capture markers", () => { expect( extractPathFromShellOutput( "Welcome to fish\n__T3CODE_PATH_START__\n/opt/homebrew/bin:/usr/bin\n__T3CODE_PATH_END__\nBye\n", ), ).toBe("/opt/homebrew/bin:/usr/bin"); }); it("returns null when the markers are missing", () => { expect(extractPathFromShellOutput("/opt/homebrew/bin /usr/bin")).toBeNull(); });}); describe("readPathFromLoginShell", () => { it("uses a shell-agnostic printenv PATH probe", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => "__T3CODE_ENV_PATH_START__\n/a:/b\n__T3CODE_ENV_PATH_END__\n"); expect(readPathFromLoginShell("/opt/homebrew/bin/fish", execFile)).toBe("/a:/b"); expect(execFile).toHaveBeenCalledTimes(1); const firstCall = execFile.mock.calls[0] as | [string, ReadonlyArray<string>, { encoding: "utf8"; timeout: number }] | undefined; expect(firstCall).toBeDefined(); if (!firstCall) { throw new Error("Expected execFile to be called"); } const [shell, args, options] = firstCall; expect(shell).toBe("/opt/homebrew/bin/fish"); expect(args).toHaveLength(2); expect(args?.[0]).toBe("-ilc"); expect(args?.[1]).toContain("printenv PATH || true"); expect(args?.[1]).toContain("__T3CODE_ENV_PATH_START__"); expect(args?.[1]).toContain("__T3CODE_ENV_PATH_END__"); expect(options).toEqual({ encoding: "utf8", timeout: 5000 }); });}); describe("readPathFromLaunchctl", () => { it("returns a trimmed PATH value from launchctl", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => " /opt/homebrew/bin:/usr/bin \n"); expect(readPathFromLaunchctl(execFile)).toBe("/opt/homebrew/bin:/usr/bin"); expect(execFile).toHaveBeenCalledWith("/bin/launchctl", ["getenv", "PATH"], { encoding: "utf8", timeout: 2000, }); }); it("returns undefined when launchctl is unavailable", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => { throw new Error("spawn /bin/launchctl ENOENT"); }); expect(readPathFromLaunchctl(execFile)).toBeUndefined(); });}); describe("readEnvironmentFromLoginShell", () => { it("extracts multiple environment variables from a login shell command", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => [ "__T3CODE_ENV_PATH_START__", "/a:/b", "__T3CODE_ENV_PATH_END__", "__T3CODE_ENV_SSH_AUTH_SOCK_START__", "/tmp/secretive.sock", "__T3CODE_ENV_SSH_AUTH_SOCK_END__", ].join("\n"), ); expect(readEnvironmentFromLoginShell("/bin/zsh", ["PATH", "SSH_AUTH_SOCK"], execFile)).toEqual({ PATH: "/a:/b", SSH_AUTH_SOCK: "/tmp/secretive.sock", }); expect(execFile).toHaveBeenCalledTimes(1); }); it("omits environment variables that are missing or empty", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => [ "__T3CODE_ENV_PATH_START__", "/a:/b", "__T3CODE_ENV_PATH_END__", "__T3CODE_ENV_SSH_AUTH_SOCK_START__", "__T3CODE_ENV_SSH_AUTH_SOCK_END__", ].join("\n"), ); expect(readEnvironmentFromLoginShell("/bin/zsh", ["PATH", "SSH_AUTH_SOCK"], execFile)).toEqual({ PATH: "/a:/b", }); }); it("preserves surrounding whitespace in captured values", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => ["__T3CODE_ENV_CUSTOM_VAR_START__", " padded value ", "__T3CODE_ENV_CUSTOM_VAR_END__"].join( "\n", ), ); expect(readEnvironmentFromLoginShell("/bin/zsh", ["CUSTOM_VAR"], execFile)).toEqual({ CUSTOM_VAR: " padded value ", }); });}); describe("listLoginShellCandidates", () => { it("returns env shell, user shell, then the platform fallback without duplicates", () => { expect(listLoginShellCandidates("darwin", " /opt/homebrew/bin/nu ", "/bin/zsh")).toEqual([ "/opt/homebrew/bin/nu", "/bin/zsh", ]); }); it("falls back to the platform default when no shells are available", () => { expect(listLoginShellCandidates("linux", undefined, "")).toEqual(["/bin/bash"]); });}); describe("mergePathEntries", () => { it("prefers login-shell PATH entries and keeps inherited extras", () => { expect( mergePathEntries("/opt/homebrew/bin:/usr/bin", "/Users/test/.local/bin:/usr/bin", "darwin"), ).toBe("/opt/homebrew/bin:/usr/bin:/Users/test/.local/bin"); }); it("uses the platform-specific delimiter", () => { expect(mergePathEntries("C:\\Tools;C:\\Windows", "C:\\Windows;C:\\Git", "win32")).toBe( "C:\\Tools;C:\\Windows;C:\\Git", ); });}); describe("readEnvironmentFromWindowsShell", () => { it("extracts environment variables from a PowerShell command", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >( () => "__T3CODE_ENV_PATH_START__\nC:\\Users\\testuser\\AppData\\Roaming\\npm\n__T3CODE_ENV_PATH_END__\n", ); expect(readEnvironmentFromWindowsShell(["PATH"], execFile)).toEqual({ PATH: "C:\\Users\\testuser\\AppData\\Roaming\\npm", }); expect(execFile).toHaveBeenCalledWith( "pwsh.exe", expect.arrayContaining(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"]), { encoding: "utf8", timeout: 5000 }, ); }); it("strips CRLF delimiters from captured PowerShell values", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >( () => "__T3CODE_ENV_FNM_DIR_START__\r\nC:\\Users\\testuser\\AppData\\Roaming\\fnm\r\n__T3CODE_ENV_FNM_DIR_END__\r\n", ); expect(readEnvironmentFromWindowsShell(["FNM_DIR"], execFile)).toEqual({ FNM_DIR: "C:\\Users\\testuser\\AppData\\Roaming\\fnm", }); }); it("omits -NoProfile when loadProfile is enabled", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => "__T3CODE_ENV_PATH_START__\nC:\\Tools\n__T3CODE_ENV_PATH_END__\n"); expect(readEnvironmentFromWindowsShell(["PATH"], { loadProfile: true }, execFile)).toEqual({ PATH: "C:\\Tools", }); expect(execFile).toHaveBeenCalledWith( "pwsh.exe", expect.arrayContaining(["-NoLogo", "-NonInteractive", "-Command"]), { encoding: "utf8", timeout: 5000 }, ); expect(execFile.mock.calls[0]?.[1]).not.toContain("-NoProfile"); }); it("falls back to Windows PowerShell when pwsh.exe is unavailable", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >((file) => { if (file === "pwsh.exe") { throw new Error("spawn pwsh.exe ENOENT"); } return "__T3CODE_ENV_PATH_START__\nC:\\Tools\n__T3CODE_ENV_PATH_END__\n"; }); expect(readEnvironmentFromWindowsShell(["PATH"], execFile)).toEqual({ PATH: "C:\\Tools", }); expect(execFile).toHaveBeenNthCalledWith(1, "pwsh.exe", expect.any(Array), { encoding: "utf8", timeout: 5000, }); expect(execFile).toHaveBeenNthCalledWith(2, "powershell.exe", expect.any(Array), { encoding: "utf8", timeout: 5000, }); });}); describe("mergePathValues", () => { it("sanitizes and dedupes Windows entries while preserving preferred order", () => { expect( mergePathValues( 'C:\\Users\\testuser\\AppData\\Roaming\\npm;"C:\\Program Files\\nodejs"', "c:\\users\\testuser\\appdata\\roaming\\npm;C:\\Windows\\System32", "win32", ), ).toBe( "C:\\Users\\testuser\\AppData\\Roaming\\npm;C:\\Program Files\\nodejs;C:\\Windows\\System32", ); }); it("removes stray quotes from Windows entries", () => { expect( mergePathValues( 'C:\\Windows\\System32;C:\\cloudflared.exe;C:";C:\\Program Files\\nodejs', undefined, "win32", ), ).toBe("C:\\Windows\\System32;C:\\cloudflared.exe;C:;C:\\Program Files\\nodejs"); }); it("dedupes case-sensitively on POSIX", () => { expect(mergePathValues("/usr/local/bin:/usr/bin", "/usr/bin:/USR/BIN", "linux")).toBe( "/usr/local/bin:/usr/bin:/USR/BIN", ); });}); describe("resolveKnownWindowsCliDirs", () => { it("returns known Windows CLI install directories in priority order", () => { expect( resolveKnownWindowsCliDirs({ APPDATA: "C:\\Users\\testuser\\AppData\\Roaming", LOCALAPPDATA: "C:\\Users\\testuser\\AppData\\Local", USERPROFILE: "C:\\Users\\testuser", }), ).toEqual([ "C:\\Users\\testuser\\AppData\\Roaming\\npm", "C:\\Users\\testuser\\AppData\\Local\\Programs\\nodejs", "C:\\Users\\testuser\\AppData\\Local\\Volta\\bin", "C:\\Users\\testuser\\AppData\\Local\\pnpm", "C:\\Users\\testuser\\.local\\bin", "C:\\Users\\testuser\\.bun\\bin", "C:\\Users\\testuser\\scoop\\shims", ]); });}); effectIt.layer(NodeServices.layer)("isCommandAvailable", (it) => { it.effect("returns false when PATH is empty", () => Effect.gen(function* () { expect( yield* isCommandAvailable("definitely-not-installed", { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, }).pipe(Effect.provideService(HostProcessPlatform, "win32")), ).toBe(false); }), );}); effectIt.layer(NodeServices.layer)("resolveCommandPath", (it) => { it.effect("fails when PATH is empty", () => Effect.gen(function* () { const result = yield* resolveCommandPath("definitely-not-installed", { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, }).pipe(Effect.provideService(HostProcessPlatform, "win32"), Effect.result); expect(result._tag).toBe("Failure"); }), ); // Records every path the scan stats, without ever reporting a match, so the // walk runs to exhaustion and the probe set can be inspected. Assertions // below count probes rather than naming paths: `Path` is the host's, so the // separator differs between a Windows and a Linux CI runner. const recordProbes = (env: NodeJS.ProcessEnv) => Effect.gen(function* () { const probed: Array<string> = []; const result = yield* resolveCommandPath("definitely-not-installed", { env }).pipe( Effect.provideService(HostProcessPlatform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), Effect.provide( FileSystem.layerNoop({ stat: (filePath) => Effect.sync(() => { probed.push(filePath); return { type: "Directory" } as FileSystem.File.Info; }), }), ), Effect.result, ); expect(result._tag).toBe("Failure"); return probed; }); it.effect("visits a repeated PATH directory only once", () => Effect.gen(function* () { const probed = yield* recordProbes({ PATH: "C:\\bin;C:\\other;C:\\bin;C:\\other", PATHEXT: ".COM;.EXE", }); // Two directories, two extensions, upper and lowercase spellings. expect(probed).toHaveLength(8); expect(new Set(probed).size).toBe(probed.length); }), ); it.effect("still visits a PATH entry that differs only in case", () => Effect.gen(function* () { const probed = yield* recordProbes({ PATH: "C:\\bin;C:\\BIN", PATHEXT: ".COM;.EXE", }); // Deliberately not folded together. Windows 10+ can mark a directory // case-sensitive, so the two spellings are not provably one directory and // skipping the second could hide a command that is really there. expect(probed).toHaveLength(8); }), ); it.effect.each(["audit-command", "audit-command.CMD"])( "resolves lowercase executable files for %s in a case-sensitive Windows PATH directory", (command) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-case-sensitive-path-" }); const executable = path.join(cwd, "audit-command.cmd"); yield* fs.writeFileString(executable, "@echo off\n"); const resolved = yield* resolveCommandPath(command, { env: { PATH: cwd, PATHEXT: ".CMD" }, }).pipe( Effect.provideService(HostProcessPlatform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), Effect.provideService(FileSystem.FileSystem, { ...fs, // Keep this case-sensitive fixture portable to case-insensitive hosts. stat: (filePath) => fs.stat(filePath === executable ? filePath : path.join(cwd, "missing")), }), ); expect(resolved).toBe(executable); }), ); it.effect("keeps cached misses until expiry while allowing explicit paths", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-path-cache-" }); const executable = path.join(cwd, "appeared.CMD"); const options = { env: { PATH: `${cwd};${cwd}`, PATHEXT: ".CMD" } }; expect((yield* resolveCommandPath("appeared", options).pipe(Effect.result))._tag).toBe( "Failure", ); yield* fs.writeFileString(executable, "@echo off\n"); expect((yield* resolveCommandPath("appeared", options).pipe(Effect.result))._tag).toBe( "Failure", ); expect(yield* resolveCommandPath(executable, options)).toBe(executable); yield* TestClock.adjust("30 seconds"); expect(yield* resolveCommandPath("appeared", options)).toBe(executable); }).pipe( Effect.provideService(HostProcessPlatform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), ), ); it.effect("keeps upper and lowercase PATHEXT candidates", () => Effect.gen(function* () { const probed = yield* recordProbes({ PATH: "C:\\bin", PATHEXT: ".COM;.EXE;.BAT;.CMD", }); expect(probed).toHaveLength(8); expect(probed.filter((filePath) => /\.(COM|EXE|BAT|CMD)$/.test(filePath))).toHaveLength(4); expect(probed.filter((filePath) => /\.(com|exe|bat|cmd)$/.test(filePath))).toHaveLength(4); }), );}); Lines 1–500 of 711. Load more
import * as NodeServices from "@effect/platform-node/NodeServices"; import { it as effectIt } from "@effect/vitest"; import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as TestClock from "effect/testing/TestClock"; import { describe, expect, it, vi } from "vite-plus/test"; import { extractPathFromShellOutput, CommandAvailability, CommandResolutionCache, type CommandAvailabilityChecker, isCommandAvailable, listLoginShellCandidates, mergePathEntries, mergePathValues, readEnvironmentFromLoginShell, readEnvironmentFromWindowsShell, readPathFromLaunchctl, readPathFromLoginShell, resolveCommandPath, resolveKnownWindowsCliDirs, resolveSpawnCommand, resolveWindowsEnvironment, SpawnExecutableResolution, WindowsShellEnvironment, type WindowsShellEnvironmentReader, } from "./shell.ts"; const withWindowsEnvironmentMocks = <A, E, R>( effect: Effect.Effect<A, E, R>, readEnvironment: WindowsShellEnvironmentReader, commandAvailable: CommandAvailabilityChecker, ) => effect.pipe( Effect.provideService(WindowsShellEnvironment, readEnvironment), Effect.provideService(CommandAvailability, commandAvailable), ); describe("extractPathFromShellOutput", () => { it("extracts the path between capture markers", () => { expect( extractPathFromShellOutput( "__T3CODE_PATH_START__\n/opt/homebrew/bin:/usr/bin\n__T3CODE_PATH_END__\n", ), ).toBe("/opt/homebrew/bin:/usr/bin"); }); it("ignores shell startup noise around the capture markers", () => { expect( extractPathFromShellOutput( "Welcome to fish\n__T3CODE_PATH_START__\n/opt/homebrew/bin:/usr/bin\n__T3CODE_PATH_END__\nBye\n", ), ).toBe("/opt/homebrew/bin:/usr/bin"); }); it("returns null when the markers are missing", () => { expect(extractPathFromShellOutput("/opt/homebrew/bin /usr/bin")).toBeNull(); }); }); describe("readPathFromLoginShell", () => { it("uses a shell-agnostic printenv PATH probe", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => "__T3CODE_ENV_PATH_START__\n/a:/b\n__T3CODE_ENV_PATH_END__\n"); expect(readPathFromLoginShell("/opt/homebrew/bin/fish", execFile)).toBe("/a:/b"); expect(execFile).toHaveBeenCalledTimes(1); const firstCall = execFile.mock.calls[0] as | [string, ReadonlyArray<string>, { encoding: "utf8"; timeout: number }] | undefined; expect(firstCall).toBeDefined(); if (!firstCall) { throw new Error("Expected execFile to be called"); } const [shell, args, options] = firstCall; expect(shell).toBe("/opt/homebrew/bin/fish"); expect(args).toHaveLength(2); expect(args?.[0]).toBe("-ilc"); expect(args?.[1]).toContain("printenv PATH || true"); expect(args?.[1]).toContain("__T3CODE_ENV_PATH_START__"); expect(args?.[1]).toContain("__T3CODE_ENV_PATH_END__"); expect(options).toEqual({ encoding: "utf8", timeout: 5000 }); }); }); describe("readPathFromLaunchctl", () => { it("returns a trimmed PATH value from launchctl", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => " /opt/homebrew/bin:/usr/bin \n"); expect(readPathFromLaunchctl(execFile)).toBe("/opt/homebrew/bin:/usr/bin"); expect(execFile).toHaveBeenCalledWith("/bin/launchctl", ["getenv", "PATH"], { encoding: "utf8", timeout: 2000, }); }); it("returns undefined when launchctl is unavailable", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => { throw new Error("spawn /bin/launchctl ENOENT"); }); expect(readPathFromLaunchctl(execFile)).toBeUndefined(); }); }); describe("readEnvironmentFromLoginShell", () => { it("extracts multiple environment variables from a login shell command", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => [ "__T3CODE_ENV_PATH_START__", "/a:/b", "__T3CODE_ENV_PATH_END__", "__T3CODE_ENV_SSH_AUTH_SOCK_START__", "/tmp/secretive.sock", "__T3CODE_ENV_SSH_AUTH_SOCK_END__", ].join("\n"), ); expect(readEnvironmentFromLoginShell("/bin/zsh", ["PATH", "SSH_AUTH_SOCK"], execFile)).toEqual({ PATH: "/a:/b", SSH_AUTH_SOCK: "/tmp/secretive.sock", }); expect(execFile).toHaveBeenCalledTimes(1); }); it("omits environment variables that are missing or empty", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => [ "__T3CODE_ENV_PATH_START__", "/a:/b", "__T3CODE_ENV_PATH_END__", "__T3CODE_ENV_SSH_AUTH_SOCK_START__", "__T3CODE_ENV_SSH_AUTH_SOCK_END__", ].join("\n"), ); expect(readEnvironmentFromLoginShell("/bin/zsh", ["PATH", "SSH_AUTH_SOCK"], execFile)).toEqual({ PATH: "/a:/b", }); }); it("preserves surrounding whitespace in captured values", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => ["__T3CODE_ENV_CUSTOM_VAR_START__", " padded value ", "__T3CODE_ENV_CUSTOM_VAR_END__"].join( "\n", ), ); expect(readEnvironmentFromLoginShell("/bin/zsh", ["CUSTOM_VAR"], execFile)).toEqual({ CUSTOM_VAR: " padded value ", }); }); }); describe("listLoginShellCandidates", () => { it("returns env shell, user shell, then the platform fallback without duplicates", () => { expect(listLoginShellCandidates("darwin", " /opt/homebrew/bin/nu ", "/bin/zsh")).toEqual([ "/opt/homebrew/bin/nu", "/bin/zsh", ]); }); it("falls back to the platform default when no shells are available", () => { expect(listLoginShellCandidates("linux", undefined, "")).toEqual(["/bin/bash"]); }); }); describe("mergePathEntries", () => { it("prefers login-shell PATH entries and keeps inherited extras", () => { expect( mergePathEntries("/opt/homebrew/bin:/usr/bin", "/Users/test/.local/bin:/usr/bin", "darwin"), ).toBe("/opt/homebrew/bin:/usr/bin:/Users/test/.local/bin"); }); it("uses the platform-specific delimiter", () => { expect(mergePathEntries("C:\\Tools;C:\\Windows", "C:\\Windows;C:\\Git", "win32")).toBe( "C:\\Tools;C:\\Windows;C:\\Git", ); }); }); describe("readEnvironmentFromWindowsShell", () => { it("extracts environment variables from a PowerShell command", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >( () => "__T3CODE_ENV_PATH_START__\nC:\\Users\\testuser\\AppData\\Roaming\\npm\n__T3CODE_ENV_PATH_END__\n", ); expect(readEnvironmentFromWindowsShell(["PATH"], execFile)).toEqual({ PATH: "C:\\Users\\testuser\\AppData\\Roaming\\npm", }); expect(execFile).toHaveBeenCalledWith( "pwsh.exe", expect.arrayContaining(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"]), { encoding: "utf8", timeout: 5000 }, ); }); it("strips CRLF delimiters from captured PowerShell values", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >( () => "__T3CODE_ENV_FNM_DIR_START__\r\nC:\\Users\\testuser\\AppData\\Roaming\\fnm\r\n__T3CODE_ENV_FNM_DIR_END__\r\n", ); expect(readEnvironmentFromWindowsShell(["FNM_DIR"], execFile)).toEqual({ FNM_DIR: "C:\\Users\\testuser\\AppData\\Roaming\\fnm", }); }); it("omits -NoProfile when loadProfile is enabled", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >(() => "__T3CODE_ENV_PATH_START__\nC:\\Tools\n__T3CODE_ENV_PATH_END__\n"); expect(readEnvironmentFromWindowsShell(["PATH"], { loadProfile: true }, execFile)).toEqual({ PATH: "C:\\Tools", }); expect(execFile).toHaveBeenCalledWith( "pwsh.exe", expect.arrayContaining(["-NoLogo", "-NonInteractive", "-Command"]), { encoding: "utf8", timeout: 5000 }, ); expect(execFile.mock.calls[0]?.[1]).not.toContain("-NoProfile"); }); it("falls back to Windows PowerShell when pwsh.exe is unavailable", () => { const execFile = vi.fn< ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string >((file) => { if (file === "pwsh.exe") { throw new Error("spawn pwsh.exe ENOENT"); } return "__T3CODE_ENV_PATH_START__\nC:\\Tools\n__T3CODE_ENV_PATH_END__\n"; }); expect(readEnvironmentFromWindowsShell(["PATH"], execFile)).toEqual({ PATH: "C:\\Tools", }); expect(execFile).toHaveBeenNthCalledWith(1, "pwsh.exe", expect.any(Array), { encoding: "utf8", timeout: 5000, }); expect(execFile).toHaveBeenNthCalledWith(2, "powershell.exe", expect.any(Array), { encoding: "utf8", timeout: 5000, }); }); }); describe("mergePathValues", () => { it("sanitizes and dedupes Windows entries while preserving preferred order", () => { expect( mergePathValues( 'C:\\Users\\testuser\\AppData\\Roaming\\npm;"C:\\Program Files\\nodejs"', "c:\\users\\testuser\\appdata\\roaming\\npm;C:\\Windows\\System32", "win32", ), ).toBe( "C:\\Users\\testuser\\AppData\\Roaming\\npm;C:\\Program Files\\nodejs;C:\\Windows\\System32", ); }); it("removes stray quotes from Windows entries", () => { expect( mergePathValues( 'C:\\Windows\\System32;C:\\cloudflared.exe;C:";C:\\Program Files\\nodejs', undefined, "win32", ), ).toBe("C:\\Windows\\System32;C:\\cloudflared.exe;C:;C:\\Program Files\\nodejs"); }); it("dedupes case-sensitively on POSIX", () => { expect(mergePathValues("/usr/local/bin:/usr/bin", "/usr/bin:/USR/BIN", "linux")).toBe( "/usr/local/bin:/usr/bin:/USR/BIN", ); }); }); describe("resolveKnownWindowsCliDirs", () => { it("returns known Windows CLI install directories in priority order", () => { expect( resolveKnownWindowsCliDirs({ APPDATA: "C:\\Users\\testuser\\AppData\\Roaming", LOCALAPPDATA: "C:\\Users\\testuser\\AppData\\Local", USERPROFILE: "C:\\Users\\testuser", }), ).toEqual([ "C:\\Users\\testuser\\AppData\\Roaming\\npm", "C:\\Users\\testuser\\AppData\\Local\\Programs\\nodejs", "C:\\Users\\testuser\\AppData\\Local\\Volta\\bin", "C:\\Users\\testuser\\AppData\\Local\\pnpm", "C:\\Users\\testuser\\.local\\bin", "C:\\Users\\testuser\\.bun\\bin", "C:\\Users\\testuser\\scoop\\shims", ]); }); }); effectIt.layer(NodeServices.layer)("isCommandAvailable", (it) => { it.effect("returns false when PATH is empty", () => Effect.gen(function* () { expect( yield* isCommandAvailable("definitely-not-installed", { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, }).pipe(Effect.provideService(HostProcessPlatform, "win32")), ).toBe(false); }), ); }); effectIt.layer(NodeServices.layer)("resolveCommandPath", (it) => { it.effect("fails when PATH is empty", () => Effect.gen(function* () { const result = yield* resolveCommandPath("definitely-not-installed", { env: { PATH: "", PATHEXT: ".COM;.EXE;.BAT;.CMD" }, }).pipe(Effect.provideService(HostProcessPlatform, "win32"), Effect.result); expect(result._tag).toBe("Failure"); }), ); // Records every path the scan stats, without ever reporting a match, so the // walk runs to exhaustion and the probe set can be inspected. Assertions // below count probes rather than naming paths: `Path` is the host's, so the // separator differs between a Windows and a Linux CI runner. const recordProbes = (env: NodeJS.ProcessEnv) => Effect.gen(function* () { const probed: Array<string> = []; const result = yield* resolveCommandPath("definitely-not-installed", { env }).pipe( Effect.provideService(HostProcessPlatform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), Effect.provide( FileSystem.layerNoop({ stat: (filePath) => Effect.sync(() => { probed.push(filePath); return { type: "Directory" } as FileSystem.File.Info; }), }), ), Effect.result, ); expect(result._tag).toBe("Failure"); return probed; }); it.effect("visits a repeated PATH directory only once", () => Effect.gen(function* () { const probed = yield* recordProbes({ PATH: "C:\\bin;C:\\other;C:\\bin;C:\\other", PATHEXT: ".COM;.EXE", }); // Two directories, two extensions, upper and lowercase spellings. expect(probed).toHaveLength(8); expect(new Set(probed).size).toBe(probed.length); }), ); it.effect("still visits a PATH entry that differs only in case", () => Effect.gen(function* () { const probed = yield* recordProbes({ PATH: "C:\\bin;C:\\BIN", PATHEXT: ".COM;.EXE", }); // Deliberately not folded together. Windows 10+ can mark a directory // case-sensitive, so the two spellings are not provably one directory and // skipping the second could hide a command that is really there. expect(probed).toHaveLength(8); }), ); it.effect.each(["audit-command", "audit-command.CMD"])( "resolves lowercase executable files for %s in a case-sensitive Windows PATH directory", (command) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-case-sensitive-path-" }); const executable = path.join(cwd, "audit-command.cmd"); yield* fs.writeFileString(executable, "@echo off\n"); const resolved = yield* resolveCommandPath(command, { env: { PATH: cwd, PATHEXT: ".CMD" }, }).pipe( Effect.provideService(HostProcessPlatform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), Effect.provideService(FileSystem.FileSystem, { ...fs, // Keep this case-sensitive fixture portable to case-insensitive hosts. stat: (filePath) => fs.stat(filePath === executable ? filePath : path.join(cwd, "missing")), }), ); expect(resolved).toBe(executable); }), ); it.effect("keeps cached misses until expiry while allowing explicit paths", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-path-cache-" }); const executable = path.join(cwd, "appeared.CMD"); const options = { env: { PATH: `${cwd};${cwd}`, PATHEXT: ".CMD" } }; expect((yield* resolveCommandPath("appeared", options).pipe(Effect.result))._tag).toBe( "Failure", ); yield* fs.writeFileString(executable, "@echo off\n"); expect((yield* resolveCommandPath("appeared", options).pipe(Effect.result))._tag).toBe( "Failure", ); expect(yield* resolveCommandPath(executable, options)).toBe(executable); yield* TestClock.adjust("30 seconds"); expect(yield* resolveCommandPath("appeared", options)).toBe(executable); }).pipe( Effect.provideService(HostProcessPlatform, "win32"), Effect.provideService(CommandResolutionCache, new Map()), ), ); it.effect("keeps upper and lowercase PATHEXT candidates", () => Effect.gen(function* () { const probed = yield* recordProbes({ PATH: "C:\\bin", PATHEXT: ".COM;.EXE;.BAT;.CMD", }); expect(probed).toHaveLength(8); expect(probed.filter((filePath) => /\.(COM|EXE|BAT|CMD)$/.test(filePath))).toHaveLength(4); expect(probed.filter((filePath) => /\.(com|exe|bat|cmd)$/.test(filePath))).toHaveLength(4); }), ); });