fix(preflight): preserve network path detection

This commit is contained in:
Jinjing
2026-10-28 17:51:53 -07:00
parent 7c364fa757
commit 0710886dc5
5 changed files with 116 additions and 9 deletions
+4
View File
@@ -16,6 +16,10 @@ export async function readDetectionDirectory(dir: string): Promise<Map<string, s
// Large directories cost less to probe by candidate than to enumerate in full.
return null
}
if (/[^\p{ASCII}]/u.test(entry.name)) {
// Filesystem Unicode equivalence can exceed JavaScript normalization and case folding.
return null
}
names.set(directoryNameKey(entry.name), entry.name)
}
return names
@@ -120,6 +120,32 @@ describe('bounded PATH detection', () => {
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) => {
+3 -1
View File
@@ -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
@@ -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<string>())
}))
// Model native Windows path semantics even when this suite runs on a POSIX host.
vi.mock('node:path', async (importOriginal) => {
const actual = await importOriginal<typeof NodePath>()
return { ...actual, ...actual.win32, default: actual.win32 }
})
vi.mock('node:fs/promises', async (importOriginal) => ({
...(await importOriginal<typeof FileSystemPromises>()),
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 })
}
)
})
@@ -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,