mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 00:03:20 +00:00
359 lines
11 KiB
TypeScript
359 lines
11 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { execFileAsyncMock } = vi.hoisted(() => ({
|
|
execFileAsyncMock: vi.fn()
|
|
}))
|
|
|
|
const { isPwshAvailableMock, isWslAvailableMock, listWslDistrosMock, isGitBashAvailableMock } =
|
|
vi.hoisted(() => ({
|
|
isPwshAvailableMock: vi.fn(),
|
|
isWslAvailableMock: vi.fn(),
|
|
listWslDistrosMock: vi.fn(),
|
|
isGitBashAvailableMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('child_process', () => {
|
|
const execFileWithPromisify = Object.assign(vi.fn(), {
|
|
[Symbol.for('nodejs.util.promisify.custom')]: execFileAsyncMock
|
|
})
|
|
return { execFile: execFileWithPromisify }
|
|
})
|
|
|
|
vi.mock('../main/pwsh', () => ({ isPwshAvailable: isPwshAvailableMock }))
|
|
vi.mock('../main/wsl', () => ({
|
|
isWslAvailable: isWslAvailableMock,
|
|
listWslDistros: listWslDistrosMock
|
|
}))
|
|
vi.mock('../main/git-bash', () => ({ isGitBashAvailable: isGitBashAvailableMock }))
|
|
|
|
import {
|
|
buildCommandLookupSpec,
|
|
buildCommandLookupSpecs,
|
|
hasAbsoluteCommandPath,
|
|
isCommandOnPathForRelay,
|
|
PreflightHandler
|
|
} from './preflight-handler'
|
|
|
|
function lookupArgs(command: string, mode: '-lc' | '-ilc' = '-lc'): string[] {
|
|
return [
|
|
mode,
|
|
[
|
|
`if resolved=$(command -v ${command} 2>/dev/null); then`,
|
|
'printf \'__ORCA_AGENT_PATH__%s\\n\' "$resolved"',
|
|
'fi'
|
|
].join('\n')
|
|
]
|
|
}
|
|
|
|
function fishLookupArgs(command: string): string[] {
|
|
return [
|
|
'-ilc',
|
|
[
|
|
`set -l resolved (command -v ${command} 2>/dev/null)`,
|
|
'if test -n "$resolved"',
|
|
'printf \'__ORCA_AGENT_PATH__%s\\n\' "$resolved"',
|
|
'end'
|
|
].join('\n')
|
|
]
|
|
}
|
|
|
|
beforeEach(() => {
|
|
execFileAsyncMock.mockReset()
|
|
isPwshAvailableMock.mockReset()
|
|
isWslAvailableMock.mockReset()
|
|
listWslDistrosMock.mockReset()
|
|
isGitBashAvailableMock.mockReset()
|
|
})
|
|
|
|
describe('buildCommandLookupSpec', () => {
|
|
it('uses where.exe on native Windows SSH hosts', () => {
|
|
expect(buildCommandLookupSpec('codex', 'win32')).toEqual({
|
|
file: 'where.exe',
|
|
args: ['codex'],
|
|
windowsHide: true
|
|
})
|
|
})
|
|
|
|
it('falls back to sh for POSIX probes without a configured shell', () => {
|
|
expect(buildCommandLookupSpec('codex', 'linux', {}, null)).toEqual({
|
|
file: '/bin/sh',
|
|
args: lookupArgs("'codex'")
|
|
})
|
|
})
|
|
|
|
it('uses the configured remote shell for POSIX probes', () => {
|
|
expect(buildCommandLookupSpec('codex', 'linux', { SHELL: '/bin/zsh' }, '/bin/zsh')).toEqual({
|
|
file: '/bin/zsh',
|
|
args: lookupArgs("'codex'", '-ilc')
|
|
})
|
|
})
|
|
|
|
it('quotes command names in shell probes', () => {
|
|
expect(
|
|
buildCommandLookupSpec("agent'cli", 'linux', { SHELL: '/bin/bash' }, '/bin/bash')
|
|
).toEqual({
|
|
file: '/bin/bash',
|
|
args: lookupArgs("'agent'\\''cli'", '-ilc')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('buildCommandLookupSpecs', () => {
|
|
it('falls back to inherited PATH after a trusted configured POSIX shell', () => {
|
|
expect(buildCommandLookupSpecs('codex', 'linux', { SHELL: '/bin/zsh' }, '/bin/zsh')).toEqual([
|
|
{ file: '/bin/zsh', args: lookupArgs("'codex'", '-ilc') },
|
|
{ file: '/bin/sh', args: lookupArgs("'codex'") }
|
|
])
|
|
})
|
|
|
|
it('allows a custom shell path only when the account login shell matches', () => {
|
|
expect(
|
|
buildCommandLookupSpecs(
|
|
'codex',
|
|
'darwin',
|
|
{ SHELL: '/opt/homebrew/bin/zsh' },
|
|
'/opt/homebrew/bin/zsh'
|
|
)
|
|
).toEqual([
|
|
{ file: '/opt/homebrew/bin/zsh', args: lookupArgs("'codex'", '-ilc') },
|
|
{ file: '/bin/sh', args: lookupArgs("'codex'") }
|
|
])
|
|
})
|
|
|
|
it('allows conservative system shell paths when account lookup is unavailable', () => {
|
|
expect(buildCommandLookupSpecs('codex', 'linux', { SHELL: '/usr/bin/bash' }, null)[0]).toEqual({
|
|
file: '/usr/bin/bash',
|
|
args: lookupArgs("'codex'", '-ilc')
|
|
})
|
|
})
|
|
|
|
it('uses fish syntax for trusted fish shells', () => {
|
|
expect(buildCommandLookupSpecs('codex', 'linux', { SHELL: '/usr/bin/fish' }, null)[0]).toEqual({
|
|
file: '/usr/bin/fish',
|
|
args: fishLookupArgs("'codex'")
|
|
})
|
|
})
|
|
|
|
it('ignores untrusted temp shell paths even when the basename is supported', () => {
|
|
expect(buildCommandLookupSpecs('codex', 'linux', { SHELL: '/tmp/zsh' }, '/bin/bash')).toEqual([
|
|
{ file: '/bin/sh', args: lookupArgs("'codex'") }
|
|
])
|
|
})
|
|
|
|
it('ignores untrusted home-bin shell paths even when the basename is supported', () => {
|
|
expect(
|
|
buildCommandLookupSpecs('codex', 'linux', { SHELL: '/home/test/bin/bash' }, '/bin/bash')
|
|
).toEqual([{ file: '/bin/sh', args: lookupArgs("'codex'") }])
|
|
})
|
|
})
|
|
|
|
describe('isCommandOnPathForRelay', () => {
|
|
it('falls back to inherited PATH when shell startup returns no absolute command path', async () => {
|
|
execFileAsyncMock
|
|
.mockResolvedValueOnce({ stdout: 'welcome\ncodex is a function\n' })
|
|
.mockResolvedValueOnce({ stdout: '__ORCA_AGENT_PATH__/relay/path/codex\n' })
|
|
|
|
await expect(
|
|
isCommandOnPathForRelay('codex', {
|
|
platform: 'linux',
|
|
env: { SHELL: '/bin/zsh', PATH: '/usr/bin' },
|
|
accountLoginShell: '/bin/zsh'
|
|
})
|
|
).resolves.toBe(true)
|
|
expect(execFileAsyncMock).toHaveBeenNthCalledWith(
|
|
1,
|
|
'/bin/zsh',
|
|
lookupArgs("'codex'", '-ilc'),
|
|
{
|
|
encoding: 'utf-8',
|
|
env: expect.objectContaining({ SHELL: '/bin/zsh' }),
|
|
timeout: 5000
|
|
}
|
|
)
|
|
expect(execFileAsyncMock).toHaveBeenNthCalledWith(2, '/bin/sh', lookupArgs("'codex'"), {
|
|
encoding: 'utf-8',
|
|
env: expect.objectContaining({ SHELL: '/bin/zsh' }),
|
|
timeout: 5000
|
|
})
|
|
})
|
|
|
|
it('falls back to inherited PATH when shell startup fails', async () => {
|
|
execFileAsyncMock
|
|
.mockRejectedValueOnce(new Error('startup failed'))
|
|
.mockResolvedValueOnce({ stdout: '__ORCA_AGENT_PATH__/relay/path/codex\n' })
|
|
|
|
await expect(
|
|
isCommandOnPathForRelay('codex', {
|
|
platform: 'linux',
|
|
env: { SHELL: '/bin/bash', PATH: '/usr/bin' },
|
|
accountLoginShell: '/bin/bash'
|
|
})
|
|
).resolves.toBe(true)
|
|
expect(execFileAsyncMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('does not execute an untrusted configured shell before inherited PATH lookup', async () => {
|
|
execFileAsyncMock.mockResolvedValueOnce({ stdout: '__ORCA_AGENT_PATH__/relay/path/codex\n' })
|
|
|
|
await expect(
|
|
isCommandOnPathForRelay('codex', {
|
|
platform: 'linux',
|
|
env: { SHELL: '/tmp/zsh', PATH: '/usr/bin' },
|
|
accountLoginShell: '/bin/bash'
|
|
})
|
|
).resolves.toBe(true)
|
|
expect(execFileAsyncMock).toHaveBeenCalledTimes(1)
|
|
expect(execFileAsyncMock).toHaveBeenCalledWith('/bin/sh', lookupArgs("'codex'"), {
|
|
encoding: 'utf-8',
|
|
env: expect.objectContaining({ SHELL: '/tmp/zsh' }),
|
|
timeout: 5000
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('hasAbsoluteCommandPath', () => {
|
|
it('ignores banners and shell function output', () => {
|
|
expect(hasAbsoluteCommandPath('/tmp/not-the-agent\ncodex is a shell function\n', 'linux')).toBe(
|
|
false
|
|
)
|
|
})
|
|
|
|
it('ignores unmarked POSIX absolute paths from shell startup output', () => {
|
|
expect(hasAbsoluteCommandPath('/tmp/not-the-agent\n', 'linux')).toBe(false)
|
|
})
|
|
|
|
it('recognizes a sentinel-marked command path amid shell startup and exit output', () => {
|
|
expect(
|
|
hasAbsoluteCommandPath('welcome\n__ORCA_AGENT_PATH__/opt/bin/codex\nlogout-banner\n', 'linux')
|
|
).toBe(true)
|
|
})
|
|
|
|
it('recognizes Windows absolute command paths', () => {
|
|
expect(
|
|
hasAbsoluteCommandPath('C:\\Users\\alice\\AppData\\Roaming\\npm\\codex.cmd\r\n', 'win32')
|
|
).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('PreflightHandler', () => {
|
|
it('honors required commands when reporting detected agents', async () => {
|
|
execFileAsyncMock.mockImplementation(async (_file, args) => {
|
|
const script = String(args[1])
|
|
if (script.includes("'orca'")) {
|
|
return { stdout: '__ORCA_AGENT_PATH__/relay/path/orca\n' }
|
|
}
|
|
throw new Error('not found')
|
|
})
|
|
const requestHandlers = new Map<string, (params: Record<string, unknown>) => Promise<unknown>>()
|
|
const dispatcher = {
|
|
onRequest: vi.fn(
|
|
(method: string, handler: (params: Record<string, unknown>) => Promise<unknown>) => {
|
|
requestHandlers.set(method, handler)
|
|
}
|
|
)
|
|
}
|
|
|
|
new PreflightHandler(dispatcher as never)
|
|
|
|
const handler = requestHandlers.get('preflight.detectAgents')
|
|
expect(handler).toBeDefined()
|
|
await expect(
|
|
handler!({
|
|
commands: [
|
|
{ id: 'claude-agent-teams', cmd: 'orca', requiredCommands: ['claude'] },
|
|
{ id: 'claude', cmd: 'claude' }
|
|
]
|
|
})
|
|
).resolves.toEqual({ agents: [] })
|
|
})
|
|
|
|
it('does not report platform-unsupported agents on native Windows SSH hosts', async () => {
|
|
const originalPlatform = process.platform
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: 'win32'
|
|
})
|
|
execFileAsyncMock.mockImplementation(async (_file, args) => {
|
|
if (String(args[0]) === 'claude') {
|
|
return { stdout: 'C:\\Users\\test\\AppData\\Roaming\\npm\\claude.cmd\r\n' }
|
|
}
|
|
if (String(args[0]) === 'orca') {
|
|
return { stdout: 'C:\\Program Files\\Orca\\orca.cmd\r\n' }
|
|
}
|
|
throw new Error('not found')
|
|
})
|
|
const requestHandlers = new Map<string, (params: Record<string, unknown>) => Promise<unknown>>()
|
|
const dispatcher = {
|
|
onRequest: vi.fn(
|
|
(method: string, handler: (params: Record<string, unknown>) => Promise<unknown>) => {
|
|
requestHandlers.set(method, handler)
|
|
}
|
|
)
|
|
}
|
|
|
|
try {
|
|
new PreflightHandler(dispatcher as never)
|
|
const handler = requestHandlers.get('preflight.detectAgents')
|
|
expect(handler).toBeDefined()
|
|
await expect(
|
|
handler!({
|
|
commands: [
|
|
{
|
|
id: 'claude-agent-teams',
|
|
cmd: 'orca',
|
|
requiredCommands: ['claude'],
|
|
unsupportedRuntimes: ['win32']
|
|
},
|
|
{ id: 'claude', cmd: 'claude' }
|
|
]
|
|
})
|
|
).resolves.toEqual({ agents: ['claude'] })
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: originalPlatform
|
|
})
|
|
}
|
|
})
|
|
|
|
it('reports remote Windows shell capabilities through the SSH preflight path', async () => {
|
|
const originalPlatform = process.platform
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: 'win32'
|
|
})
|
|
isWslAvailableMock.mockReturnValue(true)
|
|
listWslDistrosMock.mockReturnValue(['Ubuntu'])
|
|
isPwshAvailableMock.mockReturnValue(true)
|
|
isGitBashAvailableMock.mockReturnValue(true)
|
|
|
|
const requestHandlers = new Map<string, (params: Record<string, unknown>) => Promise<unknown>>()
|
|
const dispatcher = {
|
|
onRequest: vi.fn(
|
|
(method: string, handler: (params: Record<string, unknown>) => Promise<unknown>) => {
|
|
requestHandlers.set(method, handler)
|
|
}
|
|
)
|
|
}
|
|
|
|
new PreflightHandler(dispatcher as never)
|
|
|
|
try {
|
|
const handler = requestHandlers.get('preflight.detectWindowsTerminalCapabilities')
|
|
expect(handler).toBeDefined()
|
|
await expect(handler!({})).resolves.toEqual({
|
|
wslAvailable: true,
|
|
wslDistros: ['Ubuntu'],
|
|
pwshAvailable: true,
|
|
gitBashAvailable: true,
|
|
hostPlatform: 'win32'
|
|
})
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: originalPlatform
|
|
})
|
|
}
|
|
})
|
|
})
|