From 0710886dc585d32dc475ecedb6e700151b1bc375 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:25:09 -0700 Subject: [PATCH] fix(preflight): preserve network path detection --- src/main/ipc/command-path-directory.ts | 4 ++ .../command-path-resolver-batching.test.ts | 26 +++++++ src/main/ipc/command-path-resolver.ts | 4 +- ...ent-command-detection-windows-path.test.ts | 72 +++++++++++++++++++ .../preflight/agent-command-detection.test.ts | 19 ++--- 5 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 src/main/preflight/agent-command-detection-windows-path.test.ts diff --git a/src/main/ipc/command-path-directory.ts b/src/main/ipc/command-path-directory.ts index e2fa2fee16a..74179e8e2ed 100644 --- a/src/main/ipc/command-path-directory.ts +++ b/src/main/ipc/command-path-directory.ts @@ -16,6 +16,10 @@ export async function readDetectionDirectory(dir: string): Promise { expect(statFile).toHaveBeenCalledTimes(inventory.length) }) + it('closes a Unicode listing early and lets candidate stats decide filename equivalence', async () => { + let scanned = 0 + let closed = false + openDirectory.mockResolvedValue({ + async *[Symbol.asyncIterator]() { + try { + for (const name of ['cruſh', ...inventory]) { + scanned++ + yield { name } + } + } finally { + closed = true + } + } + }) + executableFiles(new Set(['/front/crush'])) + await expect( + findCommandsOnLocalPath(['crush', ...inventory], { + ...options, + env: { PATH: '/front' } + }) + ).resolves.toEqual(new Set(['crush'])) + expect(scanned).toBe(1) + expect(closed).toBe(true) + }) + it.each([true, false])( 'delegates folded POSIX matches to volume case semantics (%s)', async (caseSensitive) => { diff --git a/src/main/ipc/command-path-resolver.ts b/src/main/ipc/command-path-resolver.ts index 14050a9eb87..9b79be50336 100644 --- a/src/main/ipc/command-path-resolver.ts +++ b/src/main/ipc/command-path-resolver.ts @@ -207,6 +207,8 @@ export async function findCommandsOnLocalPath( const env = options.env ?? process.env const isAbsolute = isWin ? path.win32.isAbsolute : path.posix.isAbsolute + // Native Windows joining preserves UNC roots; POSIX hosts retain synthetic lookups. + const joinPath = isWin ? path.join : path.posix.join const pathDirs = (readEnvCaseInsensitive(env, 'PATH') ?? '') .split(isWin ? ';' : ':') // The install-dir fallback also searches trimmed PATH entries. @@ -239,7 +241,7 @@ export async function findCommandsOnLocalPath( continue } // POSIX stat must decide volume case sensitivity, even for a folded-name match. - const candidate = path.posix.join(dir, isWin ? (listed ?? wanted) : wanted) + const candidate = joinPath(dir, isWin ? (listed ?? wanted) : wanted) const candidateKey = isWin ? candidate.toLowerCase() : candidate if (seenCandidates.has(candidateKey)) { continue diff --git a/src/main/preflight/agent-command-detection-windows-path.test.ts b/src/main/preflight/agent-command-detection-windows-path.test.ts new file mode 100644 index 00000000000..4b132db7633 --- /dev/null +++ b/src/main/preflight/agent-command-detection-windows-path.test.ts @@ -0,0 +1,72 @@ +import type * as FileSystemPromises from 'node:fs/promises' +import path from 'node:path' +import type * as NodePath from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { openDirectory, statFile, detectInstallDirs } = vi.hoisted(() => ({ + openDirectory: vi.fn(), + statFile: vi.fn(), + detectInstallDirs: vi.fn(() => new Set()) +})) + +// Model native Windows path semantics even when this suite runs on a POSIX host. +vi.mock('node:path', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, ...actual.win32, default: actual.win32 } +}) +vi.mock('node:fs/promises', async (importOriginal) => ({ + ...(await importOriginal()), + opendir: openDirectory, + stat: statFile +})) +vi.mock('../../shared/local-agent-install-dir-detection', () => ({ + detectCommandsInInstallDirs: detectInstallDirs +})) +vi.mock('../pty/windows-environment-path', () => ({ + mergePersistedWindowsPath: vi.fn() +})) +vi.mock('../ipc/wsl-guest-windows-drive-path', () => ({ + readGuestWindowsDriveMounts: () => [] +})) + +import { detectAgentCommandsOnHost } from './agent-command-detection' + +describe('Windows host detection with PATH fallback skipped', () => { + const originalPlatform = process.platform + const shareDirectory = String.raw`\\review-server\tools\bin` + const executable = String.raw`\\review-server\tools\bin\codex.cmd` + + beforeEach(() => { + Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) + detectInstallDirs.mockClear() + openDirectory.mockReset().mockImplementation(async (dir: string) => ({ + async *[Symbol.asyncIterator]() { + if (path.normalize(dir).toLowerCase() === shareDirectory) { + yield { name: 'codex.cmd' } + } + } + })) + statFile.mockReset().mockImplementation(async (candidate: string) => { + if (path.normalize(candidate).toLowerCase() !== executable) { + throw Object.assign(new Error('not found'), { code: 'ENOENT' }) + } + return { isFile: () => true, isDirectory: () => false } + }) + vi.stubEnv('PATHEXT', '.COM') + }) + + afterEach(() => { + vi.unstubAllEnvs() + Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform }) + }) + + it.each(['//review-server/tools/bin', shareDirectory])( + 'detects a shim installed only in the UNC PATH entry %s', + async (directory) => { + vi.stubEnv('PATH', directory) + + await expect(detectAgentCommandsOnHost(['codex'])).resolves.toEqual(new Set(['codex'])) + expect(detectInstallDirs).toHaveBeenCalledWith([], undefined, { skipPathSearch: true }) + } + ) +}) diff --git a/src/main/preflight/agent-command-detection.test.ts b/src/main/preflight/agent-command-detection.test.ts index b79a39679e8..00faba34227 100644 --- a/src/main/preflight/agent-command-detection.test.ts +++ b/src/main/preflight/agent-command-detection.test.ts @@ -63,18 +63,21 @@ describe('detectAgentCommandsOnHost install-dir fallback', () => { } ) - it.skipIf(process.platform === 'win32')( - 'preserves actual volume case behavior when PATH fallback is skipped', - async () => { + it.skipIf(process.platform === 'win32').each([ + ['AgEnT-PrObE', 'agent-probe'], + ['cruſh', 'crush'] + ])( + 'preserves filesystem equivalence for %s when looking up %s with PATH fallback skipped', + async (filename, command) => { const dir = await mkdtemp(path.join(tmpdir(), 'agent-detection-case-')) try { - await writeFile(path.join(dir, 'AgEnT-PrObE'), '#!/bin/sh\n') - await chmod(path.join(dir, 'AgEnT-PrObE'), 0o755) + await writeFile(path.join(dir, filename), '#!/bin/sh\n') + await chmod(path.join(dir, filename), 0o755) vi.stubEnv('PATH', dir) - const commands = ['agent-probe', 'missing-a', 'missing-b', 'missing-c', 'missing-d'] - const expected = await isCommandOnLocalPath('agent-probe') + const commands = [command, 'missing-a', 'missing-b', 'missing-c', 'missing-d'] + const expected = await isCommandOnLocalPath(command) await expect(detectAgentCommandsOnHost(commands)).resolves.toEqual( - new Set(expected ? ['agent-probe'] : []) + new Set(expected ? [command] : []) ) expect(detectInstallDirs).toHaveBeenCalledWith( expected ? commands.slice(1) : commands,