mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* chore: shorten process-host comments * chore: audit CLI parsing and host-routing comments * chore: remove stale host-selector restriction comment --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
503 lines
18 KiB
TypeScript
503 lines
18 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
const { listEnvironmentsMock, resolveEnvironmentMock, getDefaultUserDataPathMock } = vi.hoisted(
|
|
() => ({
|
|
listEnvironmentsMock: vi.fn(),
|
|
resolveEnvironmentMock: vi.fn(),
|
|
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data')
|
|
})
|
|
)
|
|
|
|
vi.mock('./runtime/environments', () => ({
|
|
addEnvironmentFromPairingCode: vi.fn(),
|
|
listEnvironments: listEnvironmentsMock,
|
|
removeEnvironment: vi.fn(),
|
|
resolveEnvironment: resolveEnvironmentMock
|
|
}))
|
|
|
|
vi.mock('./runtime-client', () => ({
|
|
getDefaultUserDataPath: getDefaultUserDataPathMock
|
|
}))
|
|
|
|
import {
|
|
resolveEnvironmentSelectorForStore,
|
|
hostFilterMatchesHostId,
|
|
parseHostFlag,
|
|
resolveHostFlagEnvironmentId,
|
|
resolveHostFlagTarget
|
|
} from './execution-host-flag'
|
|
import { parseExecutionHostId } from '../shared/execution-host'
|
|
import type { RuntimeClient } from './runtime-client'
|
|
|
|
const listSshTargetsMock = vi.fn(async () => [] as { id: string; label: string }[])
|
|
const NO_SELECTION = {
|
|
listSshTargets: listSshTargetsMock,
|
|
verifySshTargets: listSshTargetsMock,
|
|
pairingCode: null,
|
|
environmentSelector: null
|
|
}
|
|
|
|
function flags(entries: Record<string, string | boolean>): Map<string, string | boolean> {
|
|
return new Map(Object.entries(entries))
|
|
}
|
|
|
|
function environment(id: string, name = id) {
|
|
return { id, name }
|
|
}
|
|
|
|
describe('parseHostFlag', () => {
|
|
it('returns undefined when --host is absent', () => {
|
|
expect(parseHostFlag(flags({}))).toBeUndefined()
|
|
})
|
|
|
|
it('rejects a --host flag with no value', () => {
|
|
expect(() => parseHostFlag(flags({ host: true }))).toThrow('Missing value for --host')
|
|
})
|
|
|
|
it.each(['runtime:', 'ssh:', 'ssh:a|b', ' '])('rejects the unparseable host id %s', (value) => {
|
|
expect(() => parseHostFlag(flags({ host: value }))).toThrow(`Invalid --host value: ${value}`)
|
|
})
|
|
|
|
it('parses the three supported host kinds', () => {
|
|
expect(parseHostFlag(flags({ host: 'local' }))?.kind).toBe('local')
|
|
expect(parseHostFlag(flags({ host: 'ssh:box-1' }))?.kind).toBe('ssh')
|
|
expect(parseHostFlag(flags({ host: 'runtime:env-1' }))).toEqual({
|
|
kind: 'runtime',
|
|
id: 'runtime:env-1',
|
|
environmentId: 'env-1'
|
|
})
|
|
})
|
|
|
|
it('carries an unprefixed host name through as an alias', () => {
|
|
expect(parseHostFlag(flags({ host: 'trader-local' }))).toEqual({
|
|
kind: 'alias',
|
|
alias: 'trader-local'
|
|
})
|
|
expect(parseHostFlag(flags({ host: ' trader-local ' }))).toEqual({
|
|
kind: 'alias',
|
|
alias: 'trader-local'
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('resolveHostFlagEnvironmentId', () => {
|
|
it('keeps the current connection for local and ssh hosts', async () => {
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'local' }), NO_SELECTION)
|
|
).resolves.toBe(null)
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'ssh:box-1' }), NO_SELECTION)
|
|
).resolves.toBe(null)
|
|
expect(listEnvironmentsMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('routes a paired runtime host to that environment', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'gpu')])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:env-1' }), NO_SELECTION)
|
|
).resolves.toBe('env-1')
|
|
})
|
|
|
|
it('rejects a runtime host id that no paired environment owns', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'gpu')])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:env-missing' }), NO_SELECTION)
|
|
).rejects.toThrow('no paired Orca server is named or has id env-missing')
|
|
})
|
|
|
|
it('accepts the environment name as well as its id', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'gpu')])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:gpu' }), NO_SELECTION)
|
|
).resolves.toBe('env-1')
|
|
})
|
|
|
|
it('decodes percent-encoded environment ids', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env one')])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:env%20one' }), NO_SELECTION)
|
|
).resolves.toBe('env one')
|
|
})
|
|
|
|
it('refuses a runtime host alongside --pairing-code', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1')])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:env-1' }), {
|
|
listSshTargets: listSshTargetsMock,
|
|
verifySshTargets: listSshTargetsMock,
|
|
pairingCode: 'orca://pair?x',
|
|
environmentSelector: null
|
|
})
|
|
).rejects.toThrow('not both')
|
|
})
|
|
|
|
it('refuses a runtime host that disagrees with --environment', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1')])
|
|
resolveEnvironmentMock.mockReturnValue(environment('env-2'))
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:env-1' }), {
|
|
listSshTargets: listSshTargetsMock,
|
|
verifySshTargets: listSshTargetsMock,
|
|
pairingCode: null,
|
|
environmentSelector: { value: 'other', label: '--environment' }
|
|
})
|
|
).rejects.toThrow('name different Orca servers')
|
|
})
|
|
|
|
it('names the ambient variable when ORCA_ENVIRONMENT is the conflicting selector', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1')])
|
|
resolveEnvironmentMock.mockReturnValue(environment('env-2'))
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:env-1' }), {
|
|
listSshTargets: listSshTargetsMock,
|
|
verifySshTargets: listSshTargetsMock,
|
|
pairingCode: null,
|
|
environmentSelector: { value: 'staging', label: 'ORCA_ENVIRONMENT' }
|
|
})
|
|
).rejects.toThrow('ORCA_ENVIRONMENT staging name different Orca servers')
|
|
})
|
|
|
|
it('hands an agent the known environment ids to retry with', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'gpu')])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:missing' }), NO_SELECTION)
|
|
).rejects.toMatchObject({
|
|
code: 'invalid_argument',
|
|
data: { knownEnvironments: [{ id: 'env-1', name: 'gpu' }] }
|
|
})
|
|
})
|
|
|
|
it('accepts ORCA_ENVIRONMENT naming the runtime host in another case', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'Build')])
|
|
resolveEnvironmentMock.mockImplementation(() => {
|
|
throw new Error('Unknown environment: build')
|
|
})
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:env-1' }), {
|
|
listSshTargets: listSshTargetsMock,
|
|
verifySshTargets: listSshTargetsMock,
|
|
pairingCode: null,
|
|
environmentSelector: { value: 'build', label: 'ORCA_ENVIRONMENT' }
|
|
})
|
|
).resolves.toBe('env-1')
|
|
resolveEnvironmentMock.mockReset()
|
|
})
|
|
|
|
it('accepts --environment naming the same server as the runtime host', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'gpu')])
|
|
resolveEnvironmentMock.mockReturnValue(environment('env-1', 'gpu'))
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:env-1' }), {
|
|
listSshTargets: listSshTargetsMock,
|
|
verifySshTargets: listSshTargetsMock,
|
|
pairingCode: null,
|
|
environmentSelector: { value: 'gpu', label: '--environment' }
|
|
})
|
|
).resolves.toBe('env-1')
|
|
})
|
|
})
|
|
|
|
describe('unprefixed --host aliases', () => {
|
|
function clientWithTargets(targets: { id: string; label: string }[]): RuntimeClient {
|
|
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SSH target resolution reads only client.call.
|
|
return {
|
|
call: vi.fn(async () => ({ result: { targets } }))
|
|
} as unknown as RuntimeClient
|
|
}
|
|
|
|
it('routes an alias that names a paired server to that environment', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'gpu')])
|
|
listSshTargetsMock.mockResolvedValue([])
|
|
|
|
await expect(resolveHostFlagEnvironmentId(flags({ host: 'gpu' }), NO_SELECTION)).resolves.toBe(
|
|
'env-1'
|
|
)
|
|
})
|
|
|
|
it('leaves an alias that names no paired server to the SSH axis', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'gpu')])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'trader-local' }), NO_SELECTION)
|
|
).resolves.toBe(null)
|
|
})
|
|
|
|
it('resolves an alias to the SSH target that carries that label', async () => {
|
|
listEnvironmentsMock.mockReturnValue([])
|
|
const client = clientWithTargets([{ id: 'ssh-1783599077486-fyasux', label: 'trader-local' }])
|
|
|
|
await expect(resolveHostFlagTarget(flags({ host: 'trader-local' }), client)).resolves.toEqual({
|
|
kind: 'ssh',
|
|
id: 'ssh:ssh-1783599077486-fyasux',
|
|
targetId: 'ssh-1783599077486-fyasux'
|
|
})
|
|
})
|
|
|
|
it('names the alias the caller typed rather than an ssh: spelling they did not use', async () => {
|
|
listEnvironmentsMock.mockReturnValue([])
|
|
|
|
await expect(
|
|
resolveHostFlagTarget(flags({ host: 'trader-local' }), clientWithTargets([]))
|
|
).rejects.toThrow('Unknown host in --host trader-local')
|
|
})
|
|
|
|
it('refuses an alias that names a machine on both axes', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'awin')])
|
|
listSshTargetsMock.mockResolvedValue([{ id: 'ssh-1-a', label: 'awin' }])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'awin' }), NO_SELECTION)
|
|
).rejects.toMatchObject({
|
|
code: 'invalid_argument',
|
|
data: {
|
|
nextSteps: expect.arrayContaining([
|
|
expect.stringContaining('--host runtime:env-1'),
|
|
expect.stringContaining('--host ssh:ssh-1-a')
|
|
])
|
|
}
|
|
})
|
|
})
|
|
|
|
it('refuses an alias naming a server and two SSH targets sharing a label', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'awin')])
|
|
listSshTargetsMock.mockResolvedValue([
|
|
{ id: 'ssh-1-a', label: 'awin' },
|
|
{ id: 'ssh-2-b', label: 'awin' }
|
|
])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'awin' }), NO_SELECTION)
|
|
).rejects.toMatchObject({
|
|
code: 'invalid_argument',
|
|
data: {
|
|
nextSteps: expect.arrayContaining([
|
|
expect.stringContaining('--host runtime:env-1'),
|
|
expect.stringContaining('--host ssh:ssh-1-a'),
|
|
expect.stringContaining('--host ssh:ssh-2-b')
|
|
])
|
|
}
|
|
})
|
|
})
|
|
|
|
it('refuses an alias two paired servers answer to', async () => {
|
|
listEnvironmentsMock.mockReturnValue([
|
|
environment('env-1', 'awin'),
|
|
environment('env-2', 'awin')
|
|
])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'awin' }), NO_SELECTION)
|
|
).rejects.toThrow('Ambiguous Orca server in --host awin')
|
|
})
|
|
|
|
it('refuses to route an alias to a paired server when SSH targets cannot be read', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'awin')])
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'awin' }), {
|
|
...NO_SELECTION,
|
|
verifySshTargets: async () => {
|
|
throw new Error('runtime unreachable')
|
|
}
|
|
})
|
|
).rejects.toMatchObject({
|
|
message: expect.stringContaining('Cannot verify --host awin'),
|
|
data: { nextSteps: [expect.stringContaining('--host runtime:env-1')] }
|
|
})
|
|
})
|
|
|
|
it('resolves the local row name to --host local', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'gpu')])
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'This Machine' }), NO_SELECTION)
|
|
).resolves.toBe(null)
|
|
await expect(
|
|
resolveHostFlagTarget(flags({ host: 'this machine' }), clientWithTargets([]))
|
|
).resolves.toEqual({ kind: 'local', id: 'local' })
|
|
})
|
|
|
|
it('refuses the local row name when an SSH target carries it too', async () => {
|
|
listEnvironmentsMock.mockReturnValue([])
|
|
await expect(
|
|
resolveHostFlagTarget(
|
|
flags({ host: 'this machine' }),
|
|
clientWithTargets([{ id: 'ssh-1-a', label: 'this machine' }])
|
|
)
|
|
).rejects.toMatchObject({
|
|
message: expect.stringContaining('Ambiguous --host this machine'),
|
|
data: {
|
|
nextSteps: expect.arrayContaining([
|
|
'Use --host local for this machine.',
|
|
expect.stringContaining('--host ssh:ssh-1-a')
|
|
])
|
|
}
|
|
})
|
|
})
|
|
|
|
it('refuses the local row name when a paired server carries it too', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'this machine')])
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'this machine' }), NO_SELECTION)
|
|
).rejects.toThrow('Ambiguous --host this machine')
|
|
})
|
|
|
|
it('resolves an SSH target label that contains spaces', async () => {
|
|
listEnvironmentsMock.mockReturnValue([])
|
|
const client = clientWithTargets([{ id: 'ssh-9', label: 'Build Box 2' }])
|
|
await expect(resolveHostFlagTarget(flags({ host: 'build box 2' }), client)).resolves.toEqual({
|
|
kind: 'ssh',
|
|
id: 'ssh:ssh-9',
|
|
targetId: 'ssh-9'
|
|
})
|
|
})
|
|
|
|
// cmd.exe passes wrapping single quotes through literally.
|
|
it('ignores one pair of wrapping quotes around a host name', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'gpu')])
|
|
listSshTargetsMock.mockResolvedValue([])
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: "'gpu'" }), NO_SELECTION)
|
|
).resolves.toBe('env-1')
|
|
expect(parseHostFlag(flags({ host: '"local"' }))).toEqual({ kind: 'local', id: 'local' })
|
|
})
|
|
|
|
it('leaves an alias to the routed server SSH targets when --environment picks the server', async () => {
|
|
listEnvironmentsMock.mockReturnValue([
|
|
environment('env-1', 'openclaw'),
|
|
environment('env-2', 'omarchy')
|
|
])
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'omarchy' }), {
|
|
...NO_SELECTION,
|
|
environmentSelector: { value: 'openclaw', label: '--environment' }
|
|
})
|
|
).resolves.toBe(null)
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'openclaw' }), {
|
|
...NO_SELECTION,
|
|
environmentSelector: { value: 'openclaw', label: '--environment' }
|
|
})
|
|
).resolves.toBe(null)
|
|
})
|
|
})
|
|
|
|
describe('hostFilterMatchesHostId', () => {
|
|
const runtimeHost = parseExecutionHostId('runtime:env-1')!
|
|
const localHost = parseExecutionHostId('local')!
|
|
const sshHost = parseExecutionHostId('ssh:box-1')!
|
|
|
|
it('matches the identical host id', () => {
|
|
expect(hostFilterMatchesHostId(runtimeHost, 'runtime:env-1')).toBe(true)
|
|
expect(hostFilterMatchesHostId(localHost, 'local')).toBe(true)
|
|
expect(hostFilterMatchesHostId(sshHost, 'ssh:box-1')).toBe(true)
|
|
})
|
|
|
|
it('treats the routed runtime own local rows as that runtime', () => {
|
|
expect(hostFilterMatchesHostId(runtimeHost, 'local')).toBe(true)
|
|
})
|
|
|
|
it('does not widen local or ssh filters', () => {
|
|
expect(hostFilterMatchesHostId(localHost, 'runtime:env-1')).toBe(false)
|
|
expect(hostFilterMatchesHostId(sshHost, 'local')).toBe(false)
|
|
})
|
|
|
|
it('does not match a different runtime host', () => {
|
|
expect(hostFilterMatchesHostId(runtimeHost, 'runtime:env-2')).toBe(false)
|
|
expect(hostFilterMatchesHostId(runtimeHost, null)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('ambiguous environment names', () => {
|
|
it('refuses --host runtime:<name> when two servers share that name', async () => {
|
|
listEnvironmentsMock.mockReturnValue([
|
|
environment('env-1', 'awin'),
|
|
environment('env-2', 'awin')
|
|
])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:awin' }), NO_SELECTION)
|
|
).rejects.toThrow('2 paired servers are named awin')
|
|
})
|
|
|
|
it('refuses --environment <name> when two servers share that name', async () => {
|
|
listEnvironmentsMock.mockReturnValue([
|
|
environment('env-1', 'awin'),
|
|
environment('env-2', 'awin')
|
|
])
|
|
|
|
await expect(resolveEnvironmentSelectorForStore('awin', listSshTargetsMock)).rejects.toThrow(
|
|
'2 paired servers are named awin'
|
|
)
|
|
})
|
|
|
|
it('still routes an exact id past a colliding name', async () => {
|
|
listEnvironmentsMock.mockReturnValue([
|
|
environment('env-1', 'awin'),
|
|
environment('env-2', 'awin')
|
|
])
|
|
|
|
await expect(
|
|
resolveHostFlagEnvironmentId(flags({ host: 'runtime:env-2' }), NO_SELECTION)
|
|
).resolves.toBe('env-2')
|
|
})
|
|
})
|
|
|
|
describe('resolveEnvironmentSelectorForStore', () => {
|
|
it('accepts a paired server by name or id', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'awin')])
|
|
listSshTargetsMock.mockClear()
|
|
|
|
await expect(resolveEnvironmentSelectorForStore('awin', listSshTargetsMock)).resolves.toBe(
|
|
'awin'
|
|
)
|
|
await expect(resolveEnvironmentSelectorForStore('env-1', listSshTargetsMock)).resolves.toBe(
|
|
'env-1'
|
|
)
|
|
expect(listSshTargetsMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
// The store matches names exactly; canonicalize loose matches to IDs.
|
|
it('hands back the id for a name typed in another case or quoted', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'Build')])
|
|
|
|
await expect(resolveEnvironmentSelectorForStore('build', listSshTargetsMock)).resolves.toBe(
|
|
'env-1'
|
|
)
|
|
await expect(resolveEnvironmentSelectorForStore('"BUILD"', listSshTargetsMock)).resolves.toBe(
|
|
'env-1'
|
|
)
|
|
})
|
|
|
|
it('names the SSH target when the selector is one', async () => {
|
|
listEnvironmentsMock.mockReturnValue([environment('env-1', 'awin')])
|
|
listSshTargetsMock.mockResolvedValue([{ id: 'ssh-1-a', label: 'openclaw' }])
|
|
|
|
await expect(
|
|
resolveEnvironmentSelectorForStore('openclaw', listSshTargetsMock)
|
|
).rejects.toMatchObject({
|
|
code: 'invalid_argument',
|
|
data: {
|
|
nextSteps: expect.arrayContaining([expect.stringContaining('--host ssh:ssh-1-a')])
|
|
}
|
|
})
|
|
})
|
|
|
|
it('still points at host list when the name is on neither axis', async () => {
|
|
listEnvironmentsMock.mockReturnValue([])
|
|
listSshTargetsMock.mockResolvedValue([])
|
|
|
|
await expect(resolveEnvironmentSelectorForStore('nowhere', listSshTargetsMock)).rejects.toThrow(
|
|
'no paired Orca server is named or has id nowhere'
|
|
)
|
|
})
|
|
})
|