mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 00:02:54 +00:00
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>
310 lines
11 KiB
TypeScript
310 lines
11 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import path from 'node:path'
|
|
import type * as LocalCommandResolver from './command-path-resolver'
|
|
import { buildPosixCommandPathLookupScript } from '../../shared/posix-command-path-lookup'
|
|
|
|
const {
|
|
runPreflightCommandInWslMock,
|
|
execFileAsyncMock,
|
|
listLocalCommandPathsMock,
|
|
runProcessMock
|
|
} = vi.hoisted(() => ({
|
|
runPreflightCommandInWslMock: vi.fn(),
|
|
execFileAsyncMock: vi.fn(),
|
|
listLocalCommandPathsMock: vi.fn(),
|
|
runProcessMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../../shared/child-process/run-process', () => ({ runProcess: runProcessMock }))
|
|
|
|
vi.mock('./preflight-local-env', () => ({ buildLocalPreflightEnv: () => undefined }))
|
|
|
|
vi.mock('./preflight-wsl-command', () => ({
|
|
runPreflightCommandInWsl: runPreflightCommandInWslMock
|
|
}))
|
|
|
|
vi.mock('./command-path-resolver', async (importOriginal) => ({
|
|
...(await importOriginal<typeof LocalCommandResolver>()),
|
|
isCommandOnLocalPath: vi.fn(async () => false),
|
|
listLocalCommandPaths: listLocalCommandPathsMock
|
|
}))
|
|
|
|
// Why: `findRunnableLocalCommand` decides what to spawn next from the error
|
|
// `execFile` puts on a failed probe, so the shapes below have to be its own.
|
|
vi.mock('child_process', () => {
|
|
const execFileWithPromisify = Object.assign(vi.fn(), {
|
|
[Symbol.for('nodejs.util.promisify.custom')]: execFileAsyncMock
|
|
})
|
|
return { execFile: execFileWithPromisify, spawn: vi.fn() }
|
|
})
|
|
|
|
import { findRunnableLocalCommand, isCommandOnPath } from './preflight-command-exec'
|
|
|
|
describe('isCommandOnPath', () => {
|
|
const sentinel = '__ORCA_PREFLIGHT_COMMAND_PATH__'
|
|
|
|
beforeEach(() => {
|
|
runPreflightCommandInWslMock.mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('uses the shared literal lookup and accepts a sentinel-prefixed POSIX path', async () => {
|
|
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
|
runPreflightCommandInWslMock.mockResolvedValue({
|
|
stdout: `shell startup chatter\n${sentinel}/home/user/.local/bin/codex\n`,
|
|
stderr: ''
|
|
})
|
|
|
|
const found = await isCommandOnPath('codex', { distro: 'Ubuntu' })
|
|
|
|
expect(found).toBe(true)
|
|
expect(runPreflightCommandInWslMock).toHaveBeenCalledOnce()
|
|
const [, command] = runPreflightCommandInWslMock.mock.calls[0] as [{ distro: string }, string]
|
|
expect(command).toContain(
|
|
buildPosixCommandPathLookupScript(
|
|
{ kind: 'literal', value: 'codex' },
|
|
// The WSL branch skips Windows mounts, so detection and this check
|
|
// cannot disagree about the same distro.
|
|
{ skipWindowsMountDirs: true }
|
|
)
|
|
)
|
|
expect(command).toContain(
|
|
['if [ -n "$resolved" ]; then', `printf '${sentinel}%s\\n' "$resolved"`, 'fi'].join('\n')
|
|
)
|
|
})
|
|
|
|
it.each([
|
|
['/absolute/startup/chatter', false],
|
|
[`${sentinel}relative/path`, false],
|
|
['codex', false],
|
|
["alias codex='codex --wrapped'", false]
|
|
])('parses WSL lookup output %s as available: %s', async (stdout, expected) => {
|
|
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
|
runPreflightCommandInWslMock.mockResolvedValue({ stdout: `${stdout}\n`, stderr: '' })
|
|
|
|
await expect(isCommandOnPath('codex', { distro: 'Ubuntu' })).resolves.toBe(expected)
|
|
})
|
|
})
|
|
|
|
describe('findRunnableLocalCommand', () => {
|
|
const shim = '/Users/tester/.asdf/shims/gh'
|
|
const second = '/Users/tester/.volta/bin/gh'
|
|
const third = '/usr/local/bin/gh'
|
|
|
|
const spawnedCommands = () => execFileAsyncMock.mock.calls.map(([command]) => command)
|
|
|
|
let pathBefore = ''
|
|
|
|
beforeEach(() => {
|
|
pathBefore = process.env.PATH ?? ''
|
|
execFileAsyncMock.mockReset()
|
|
runProcessMock.mockReset()
|
|
listLocalCommandPathsMock.mockReset()
|
|
listLocalCommandPathsMock.mockResolvedValue([shim, second, third])
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
process.env.PATH = pathBefore
|
|
})
|
|
|
|
it('returns the first copy that runs and leaves the rest alone', async () => {
|
|
execFileAsyncMock.mockImplementation(async (command: string) => {
|
|
if (command === shim) {
|
|
throw Object.assign(new Error('cannot execute'), { code: 126 })
|
|
}
|
|
return { stdout: 'gh version 2.98.0\n', stderr: '' }
|
|
})
|
|
|
|
await expect(findRunnableLocalCommand('gh')).resolves.toEqual({
|
|
status: 'available',
|
|
binary: second
|
|
})
|
|
expect(spawnedCommands()).toEqual([shim, second])
|
|
})
|
|
|
|
it('keeps looking when a copy exits non-zero', async () => {
|
|
execFileAsyncMock.mockImplementation(async (command: string) => {
|
|
if (command === third) {
|
|
return { stdout: 'gh version 2.98.0\n', stderr: '' }
|
|
}
|
|
throw Object.assign(new Error('Command failed'), { code: 1 })
|
|
})
|
|
|
|
await expect(findRunnableLocalCommand('gh')).resolves.toEqual({
|
|
status: 'available',
|
|
binary: third
|
|
})
|
|
})
|
|
|
|
it('stops at a timed-out copy instead of paying the timeout once per copy', async () => {
|
|
execFileAsyncMock.mockImplementation(async (command: string) => {
|
|
if (command === second) {
|
|
throw Object.assign(new Error('Timed out'), { killed: true, code: null })
|
|
}
|
|
throw Object.assign(new Error('cannot execute'), { code: 126 })
|
|
})
|
|
|
|
await expect(findRunnableLocalCommand('gh')).resolves.toEqual({
|
|
status: 'timeout',
|
|
binary: second
|
|
})
|
|
expect(spawnedCommands()).toEqual([shim, second])
|
|
})
|
|
|
|
it('stops on the ETIMEDOUT shape as well as the killed shape', async () => {
|
|
execFileAsyncMock.mockImplementation((command: string) =>
|
|
command === second
|
|
? Promise.reject(Object.assign(new Error('Timed out'), { code: 'ETIMEDOUT' }))
|
|
: Promise.reject(Object.assign(new Error('cannot execute'), { code: 126 }))
|
|
)
|
|
|
|
await expect(findRunnableLocalCommand('gh')).resolves.toEqual({
|
|
status: 'timeout',
|
|
binary: second
|
|
})
|
|
expect(spawnedCommands()).toEqual([shim, second])
|
|
})
|
|
|
|
it('reads a rejection that is not an object as an ordinary failure', async () => {
|
|
execFileAsyncMock.mockImplementation((command: string) =>
|
|
command === third
|
|
? Promise.resolve({ stdout: 'gh version 2.98.0\n', stderr: '' })
|
|
: Promise.reject('not an error object')
|
|
)
|
|
|
|
await expect(findRunnableLocalCommand('gh')).resolves.toEqual({
|
|
status: 'available',
|
|
binary: third
|
|
})
|
|
expect(spawnedCommands()).toEqual([shim, second, third])
|
|
})
|
|
|
|
it('probes the bare command name when fs finds no candidate', async () => {
|
|
listLocalCommandPathsMock.mockResolvedValue([])
|
|
execFileAsyncMock.mockResolvedValue({ stdout: 'gh version 2.98.0\n', stderr: '' })
|
|
|
|
await expect(findRunnableLocalCommand('gh')).resolves.toEqual({
|
|
status: 'available',
|
|
binary: 'gh'
|
|
})
|
|
expect(spawnedCommands()).toEqual(['gh'])
|
|
})
|
|
|
|
it('probes a relative PATH entry as the absolute directory cwd gives it', async () => {
|
|
const relativeDir = path.join('.', 'tools')
|
|
process.env.PATH = [shim.replace('/gh', ''), relativeDir].join(path.delimiter)
|
|
const absoluteDir = path.resolve(relativeDir)
|
|
const hidden = `${absoluteDir}/gh`
|
|
const absolutePath = [shim.replace('/gh', ''), absoluteDir].join(path.delimiter)
|
|
listLocalCommandPathsMock.mockImplementation(
|
|
async (_command: string, options?: { env?: NodeJS.ProcessEnv }) =>
|
|
options?.env?.PATH === absolutePath ? [shim, hidden] : []
|
|
)
|
|
execFileAsyncMock.mockImplementation(async (command: string) => {
|
|
if (command === hidden) {
|
|
return { stdout: 'gh version 2.98.0\n', stderr: '' }
|
|
}
|
|
throw Object.assign(new Error('cannot execute'), { code: 126 })
|
|
})
|
|
|
|
await expect(findRunnableLocalCommand('gh')).resolves.toEqual({
|
|
status: 'available',
|
|
binary: hidden
|
|
})
|
|
expect(spawnedCommands()).toEqual([shim, hidden])
|
|
})
|
|
|
|
it('does not pay for the bare name when every PATH entry is absolute', async () => {
|
|
process.env.PATH = ['/Users/tester/.asdf/shims', '/usr/local/bin'].join(path.delimiter)
|
|
execFileAsyncMock.mockRejectedValue(Object.assign(new Error('cannot execute'), { code: 126 }))
|
|
|
|
await expect(findRunnableLocalCommand('gh')).resolves.toEqual({
|
|
status: 'exec_failed',
|
|
binary: third
|
|
})
|
|
expect(spawnedCommands()).not.toContain('gh')
|
|
})
|
|
|
|
it('runs an explicitly selected Windows cmd shim through the shared runner', async () => {
|
|
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
|
const command = path.win32.join('C:\\tools', 'gh.cmd')
|
|
runProcessMock.mockResolvedValue({
|
|
code: 0,
|
|
stdout: 'gh version fixture',
|
|
stderr: '',
|
|
timedOut: false
|
|
})
|
|
|
|
await expect(findRunnableLocalCommand(command)).resolves.toEqual({
|
|
status: 'available',
|
|
binary: command
|
|
})
|
|
expect(runProcessMock).toHaveBeenCalledWith({
|
|
program: command,
|
|
args: ['--version'],
|
|
env: undefined,
|
|
timeoutMs: expect.any(Number)
|
|
})
|
|
expect(execFileAsyncMock).not.toHaveBeenCalled()
|
|
expect(listLocalCommandPathsMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('keeps searching Windows PATH after a broken cmd shim', async () => {
|
|
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
|
const command = path.win32.join('C:\\tools', 'gh.cmd')
|
|
const working = path.win32.join('C:\\working', 'gh.exe')
|
|
listLocalCommandPathsMock.mockResolvedValue([command, working])
|
|
runProcessMock.mockResolvedValue({
|
|
code: 126,
|
|
stdout: '',
|
|
stderr: 'broken shim',
|
|
timedOut: false
|
|
})
|
|
execFileAsyncMock.mockResolvedValue({ stdout: 'gh version fixture', stderr: '' })
|
|
|
|
await expect(findRunnableLocalCommand('gh')).resolves.toEqual({
|
|
status: 'available',
|
|
binary: working
|
|
})
|
|
expect(runProcessMock).toHaveBeenCalledOnce()
|
|
expect(spawnedCommands()).toEqual([working])
|
|
})
|
|
|
|
it('stops on a shared runner timeout', async () => {
|
|
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
|
const command = path.win32.join('C:\\tools', 'gh.cmd')
|
|
listLocalCommandPathsMock.mockResolvedValue([command, third])
|
|
runProcessMock.mockResolvedValue({ code: null, stdout: '', stderr: '', timedOut: true })
|
|
|
|
await expect(findRunnableLocalCommand('gh')).resolves.toEqual({
|
|
status: 'timeout',
|
|
binary: command
|
|
})
|
|
expect(execFileAsyncMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('shares the five second timeout across failed candidates', async () => {
|
|
let now = 1000
|
|
vi.spyOn(Date, 'now').mockImplementation(() => now)
|
|
execFileAsyncMock.mockImplementation(async (command: string) => {
|
|
if (command === shim) {
|
|
now += 4000
|
|
throw Object.assign(new Error('cannot execute'), { code: 126 })
|
|
}
|
|
throw Object.assign(new Error('timeout'), { code: 'ETIMEDOUT' })
|
|
})
|
|
|
|
await expect(findRunnableLocalCommand('gh')).resolves.toEqual({
|
|
status: 'timeout',
|
|
binary: second
|
|
})
|
|
expect(execFileAsyncMock.mock.calls.map(([, , options]) => options.timeout)).toEqual([
|
|
5000, 1000
|
|
])
|
|
})
|
|
})
|