Files
orca/src/main/ipc/command-path-resolver.test.ts
T
Aashish Mahato e5ba5975df Recover working local forge CLIs behind broken PATH launchers
Recover a working local forge CLI when an earlier PATH launcher is broken. Bound executable probes and reuse the verified selection for native operations without replaying authentication or user requests.

Fixes #22975

Co-authored-by: Aashish <145881415+aashish254@users.noreply.github.com>
2026-10-03 20:45:52 -07:00

241 lines
9.4 KiB
TypeScript

import { access, chmod, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { isCommandOnLocalPath, listLocalCommandPaths } from './command-path-resolver'
describe('isCommandOnLocalPath', () => {
it('returns false for an empty command', async () => {
await expect(isCommandOnLocalPath('')).resolves.toBe(false)
})
// POSIX semantics: executable bit + absolute-only + symlink follow.
describe.skipIf(process.platform === 'win32')('posix', () => {
let dir = ''
beforeAll(async () => {
dir = await mkdtemp(path.join(tmpdir(), 'cmd-resolver-posix-'))
await writeFile(path.join(dir, 'runme'), '#!/bin/sh\n')
await chmod(path.join(dir, 'runme'), 0o755)
await writeFile(path.join(dir, 'plain'), 'not executable\n')
await chmod(path.join(dir, 'plain'), 0o644)
await mkdir(path.join(dir, 'adir'))
await symlink(path.join(dir, 'runme'), path.join(dir, 'linkme'))
})
afterAll(async () => {
await rm(dir, { recursive: true, force: true })
})
it('finds an executable file on PATH', async () => {
await expect(
isCommandOnLocalPath('runme', { platform: 'linux', env: { PATH: dir } })
).resolves.toBe(true)
})
it('rejects a non-executable file on PATH', async () => {
await expect(
isCommandOnLocalPath('plain', { platform: 'linux', env: { PATH: dir } })
).resolves.toBe(false)
})
it('rejects a directory whose name matches the command', async () => {
await expect(
isCommandOnLocalPath('adir', { platform: 'linux', env: { PATH: dir } })
).resolves.toBe(false)
})
it('follows a symlink to an executable', async () => {
await expect(
isCommandOnLocalPath('linkme', { platform: 'linux', env: { PATH: dir } })
).resolves.toBe(true)
})
it('returns false when PATH is empty', async () => {
await expect(
isCommandOnLocalPath('runme', { platform: 'linux', env: { PATH: '' } })
).resolves.toBe(false)
})
it('resolves an absolute command path directly', async () => {
await expect(
isCommandOnLocalPath(path.join(dir, 'runme'), { platform: 'linux', env: { PATH: '' } })
).resolves.toBe(true)
})
it('rejects a match reached via a relative PATH entry (absolute-only gate)', async () => {
await expect(
isCommandOnLocalPath('runme', { platform: 'linux', env: { PATH: '.' }, cwd: dir })
).resolves.toBe(false)
})
})
// Win32 semantics tested cross-platform via the explicit `platform` option:
// PATHEXT resolution + case-insensitive `Path` key.
describe('win32 (synthetic)', () => {
let dir = ''
beforeAll(async () => {
dir = await mkdtemp(path.join(tmpdir(), 'cmd-resolver-win32-'))
// Why: the fixture extension matches a PATHEXT entry's case exactly so these
// tests are filesystem-portable. Real Windows resolves `.CMD` vs `.cmd`
// case-insensitively via the FS itself (same as where.exe); we deliberately
// do NOT emulate that in the resolver, so we must not depend on a
// case-insensitive FS here — this suite also runs on case-sensitive Linux CI.
await writeFile(path.join(dir, 'tool.CMD'), '@echo off\n')
})
afterAll(async () => {
await rm(dir, { recursive: true, force: true })
})
it('resolves a bare command via PATHEXT using the case-insensitive Path key', async () => {
await expect(
isCommandOnLocalPath('tool', {
platform: 'win32',
env: { Path: dir, PATHEXT: '.COM;.EXE;.CMD;.BAT' }
})
).resolves.toBe(true)
})
it('returns false when PATHEXT excludes the only available extension', async () => {
await expect(
isCommandOnLocalPath('tool', {
platform: 'win32',
env: { Path: dir, PATHEXT: '.EXE;.COM' }
})
).resolves.toBe(false)
})
it('matches an exact name with extension already present', async () => {
await expect(
isCommandOnLocalPath('tool.CMD', {
platform: 'win32',
env: { Path: dir, PATHEXT: '.EXE' }
})
).resolves.toBe(true)
})
})
// Why the full list exists (#22975): a dead version-manager shim passes the
// same executable check as the binary it shadows, so a winner-only lookup can
// only ever hand the caller the shim. The copies behind it are the answer.
describe.skipIf(process.platform === 'win32')('listLocalCommandPaths', () => {
let front = ''
let middle = ''
let back = ''
async function executable(dir: string, name: string, body: string): Promise<void> {
await writeFile(path.join(dir, name), body)
await chmod(path.join(dir, name), 0o755)
}
beforeAll(async () => {
front = await mkdtemp(path.join(tmpdir(), 'cmd-list-front-'))
middle = await mkdtemp(path.join(tmpdir(), 'cmd-list-middle-'))
back = await mkdtemp(path.join(tmpdir(), 'cmd-list-back-'))
await executable(front, 'gh', '#!/usr/bin/env bash\nexec /nope/asdf exec "gh" "$@"\n')
await executable(back, 'gh', "#!/bin/sh\nprintf 'gh version 2.98.0\\n'\n")
// Both a non-executable file and a directory in the middle: neither is a
// match, and neither may end the scan.
await writeFile(path.join(middle, 'gh'), 'not executable\n')
await chmod(path.join(middle, 'gh'), 0o644)
await mkdir(path.join(middle, 'glab'))
await executable(back, 'glab', '#!/bin/sh\n')
})
afterAll(async () => {
await rm(front, { recursive: true, force: true })
await rm(middle, { recursive: true, force: true })
await rm(back, { recursive: true, force: true })
})
it('returns every match in PATH order, shim first', async () => {
await expect(
listLocalCommandPaths('gh', {
platform: 'linux',
env: { PATH: `${front}:${middle}:${back}` }
})
).resolves.toEqual([path.posix.join(front, 'gh'), path.posix.join(back, 'gh')])
})
it('follows PATH order rather than which copy runs', async () => {
await expect(
listLocalCommandPaths('gh', { platform: 'linux', env: { PATH: `${back}:${front}` } })
).resolves.toEqual([path.posix.join(back, 'gh'), path.posix.join(front, 'gh')])
})
it('skips a directory that matches the command name and keeps scanning', async () => {
await expect(
listLocalCommandPaths('glab', {
platform: 'linux',
env: { PATH: `${middle}:${front}:${back}` }
})
).resolves.toEqual([path.posix.join(back, 'glab')])
})
it('returns an empty list when PATH holds no absolute match', async () => {
await expect(
listLocalCommandPaths('gh', { platform: 'linux', env: { PATH: '' } })
).resolves.toEqual([])
})
it('applies the absolute-only gate to the whole list, not just the winner', async () => {
await expect(
listLocalCommandPaths('gh', {
platform: 'linux',
env: { PATH: `.${path.delimiter}${front}` }
})
).resolves.toEqual([path.posix.join(front, 'gh')])
})
it('drops a relative match the filesystem would otherwise find', async () => {
// Why a second relative case: `path.posix.join('.', 'gh')` collapses to
// `gh`, so the case above is unaffected by deleting the gate. `fs.access`
// resolves a relative candidate against the process cwd, so only a PATH
// entry that really reaches a fixture proves the filter — and the `access`
// proves that reach first, making this fail loudly instead of going
// vacuous if the suite is ever run from a directory that hides it.
const relative = path.relative(process.cwd(), back)
await expect(access(path.join(relative, 'gh'))).resolves.toBeUndefined()
await expect(
listLocalCommandPaths('gh', { platform: 'linux', env: { PATH: relative } })
).resolves.toEqual([])
})
it('lists a PATH entry that repeats only once', async () => {
// Why it matters: callers spawn each entry, and `/usr/local/bin` appearing
// twice in a real PATH is normal — a doomed shim must not be probed twice.
await expect(
listLocalCommandPaths('gh', {
platform: 'linux',
env: { PATH: `${back}:${front}:${back}` }
})
).resolves.toEqual([path.posix.join(back, 'gh'), path.posix.join(front, 'gh')])
})
it('returns an empty list for an empty command', async () => {
await expect(listLocalCommandPaths('')).resolves.toEqual([])
})
it('resolves an absolute command path directly', async () => {
await expect(
listLocalCommandPaths(path.join(front, 'gh'), { platform: 'linux', env: { PATH: '' } })
).resolves.toEqual([`${front}/gh`])
})
it('lists every PATHEXT permutation on win32, in resolver order', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'cmd-list-win32-'))
try {
await writeFile(path.join(dir, 'tool.CMD'), '@echo off\n')
await writeFile(path.join(dir, 'tool.EXE'), '')
await expect(
listLocalCommandPaths('tool', {
platform: 'win32',
env: { Path: dir, PATHEXT: '.CMD;.EXE' },
cwd: dir
})
).resolves.toEqual([`${dir}/tool.CMD`, `${dir}/tool.EXE`])
} finally {
await rm(dir, { recursive: true, force: true })
}
})
})
})