Files
orca/src/cli/host-selector-alternatives.test.ts
T
Neil 51eed5a1bc feat(cli): report SSH host platforms (#18896)
* feat(cli): report SSH host platforms

* feat(cli): include SSH connection status

* fix(cli): preserve unknown SSH connection state
2026-09-05 14:35:45 -07:00

198 lines
7.7 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import {
ambiguousEnvironments,
ambiguousSshTargets,
crossKindNextSteps,
findEnvironmentByName,
findSshTargetByName,
listSshTargets,
resolveSshHostTargetId
} from './host-selector-alternatives'
import type { RuntimeClient } from './runtime-client'
const SSH_TARGETS = [{ id: 'ssh-1755000000000-a1b2c3', label: 'openclaw' }]
const ENVIRONMENTS = [{ id: '03ef704c-b180-4b10-998d-e28fbd5de9a3', name: 'awin' }]
function clientReturning(targets: { id: string; label: string }[]): RuntimeClient {
return {
call: vi.fn(async () => ({ result: { targets } }))
} as unknown as RuntimeClient
}
describe('findSshTargetByName', () => {
// Why: ids are machine-generated `ssh-<timestamp>-<random>`, so the label is the only name a
// person or agent ever has. Matching ids alone reports "not found" for a target that exists.
it('matches the human label as well as the generated id', () => {
expect(findSshTargetByName(SSH_TARGETS, 'openclaw')?.id).toBe('ssh-1755000000000-a1b2c3')
expect(findSshTargetByName(SSH_TARGETS, 'ssh-1755000000000-a1b2c3')?.label).toBe('openclaw')
expect(findSshTargetByName(SSH_TARGETS, 'OpenClaw')?.label).toBe('openclaw')
})
it('does not invent a match', () => {
expect(findSshTargetByName(SSH_TARGETS, 'awin')).toBeUndefined()
})
})
describe('findEnvironmentByName', () => {
it('matches a paired server by name or id', () => {
expect(findEnvironmentByName(ENVIRONMENTS, 'awin')?.id).toBe(ENVIRONMENTS[0]!.id)
expect(findEnvironmentByName(ENVIRONMENTS, ENVIRONMENTS[0]!.id)?.name).toBe('awin')
})
})
describe('crossKindNextSteps', () => {
const alternatives = { sshTargets: SSH_TARGETS, environments: ENVIRONMENTS }
it('points an SSH name at the SSH flag when a server was asked for', () => {
const steps = crossKindNextSteps('openclaw', alternatives, 'environment')
expect(steps.join('\n')).toContain('--host ssh:ssh-1755000000000-a1b2c3')
expect(steps.join('\n')).toContain('is an SSH target')
})
it('points a server name at --environment when an SSH target was asked for', () => {
const steps = crossKindNextSteps('awin', alternatives, 'ssh')
expect(steps.join('\n')).toContain('--environment awin')
expect(steps.join('\n')).toContain('is a paired Orca server')
})
it('says nothing when the name exists on neither axis', () => {
expect(crossKindNextSteps('nowhere', alternatives, 'ssh')).toEqual([])
})
// Why: suggesting the axis the caller already used would be noise, not help.
it('does not suggest the axis that was already requested', () => {
expect(crossKindNextSteps('openclaw', alternatives, 'ssh')).toEqual([])
expect(crossKindNextSteps('awin', alternatives, 'environment')).toEqual([])
})
})
describe('resolveSshHostTargetId', () => {
it('resolves a label to the target id', async () => {
await expect(
resolveSshHostTargetId(clientReturning(SSH_TARGETS), 'openclaw', ENVIRONMENTS)
).resolves.toBe('ssh-1755000000000-a1b2c3')
})
// Why: this used to answer ok:true with an empty list — the same silent wrong-machine result
// that unknown runtime ids gave before they were rejected.
it('rejects an unknown target instead of letting it filter to nothing', async () => {
await expect(
resolveSshHostTargetId(clientReturning(SSH_TARGETS), 'nowhere', ENVIRONMENTS)
).rejects.toThrow('no SSH target named or with id nowhere')
})
it('names the paired server when an SSH target was asked for by a server name', async () => {
await expect(
resolveSshHostTargetId(clientReturning(SSH_TARGETS), 'awin', ENVIRONMENTS)
).rejects.toMatchObject({
data: {
nextSteps: expect.arrayContaining([expect.stringContaining('--environment awin')])
}
})
})
it('says so plainly when the host has no SSH targets at all', async () => {
await expect(
resolveSshHostTargetId(clientReturning([]), 'openclaw', ENVIRONMENTS)
).rejects.toMatchObject({
data: { nextSteps: expect.arrayContaining(['This Orca host has no SSH targets registered.']) }
})
})
})
describe('listSshTargets', () => {
// Why: an older host answers listTargets but not listTargetSummaries. Treating that as "no
// targets" would reject an ssh id that is valid on that host.
it('falls back to the older listing when the newer method is absent', async () => {
const { RuntimeClientError } = await import('./runtime/types.js')
const call = vi.fn(async (method: string) => {
if (method === 'ssh.listTargetSummaries') {
throw new RuntimeClientError('method_not_found', 'Unknown method')
}
return { result: { targets: SSH_TARGETS } }
})
await expect(listSshTargets({ call } as unknown as RuntimeClient)).resolves.toEqual(SSH_TARGETS)
expect(call).toHaveBeenCalledWith('ssh.listTargets')
})
it('enriches legacy target rows from host-owned connection state', async () => {
const { RuntimeClientError } = await import('./runtime/types.js')
const call = vi.fn(async (method: string) => {
if (method === 'ssh.listTargetSummaries') {
throw new RuntimeClientError('method_not_found', 'Unknown method')
}
if (method === 'ssh.getState') {
return { result: { state: { status: 'connected', remotePlatform: 'win32' } } }
}
return { result: { targets: SSH_TARGETS } }
})
await expect(listSshTargets({ call } as unknown as RuntimeClient)).resolves.toEqual([
{ ...SSH_TARGETS[0], connected: true, connectionStatus: 'connected', remotePlatform: 'win32' }
])
expect(call).toHaveBeenCalledWith('ssh.getState', { targetId: SSH_TARGETS[0].id })
})
// Why: this only ever runs to enrich an error we are already reporting; a failure here must
// not replace that error with a confusing one about SSH enumeration.
it('returns nothing rather than masking the error it was enriching', async () => {
const client = {
call: vi.fn(async () => {
throw new Error('boom')
})
}
await expect(listSshTargets(client as unknown as RuntimeClient)).resolves.toEqual([])
})
})
describe('ambiguous names never resolve silently', () => {
const twoOpenclaw = [
{ id: 'ssh-1-a', label: 'openclaw' },
{ id: 'ssh-2-b', label: 'openclaw' }
]
const twoAwin = [
{ id: 'env-1', name: 'awin' },
{ id: 'env-2', name: 'awin' }
]
// Why: picking the first would choose a machine on the caller's behalf — the exact failure the
// whole selector path exists to prevent.
it('refuses to guess between two ssh targets sharing a label', () => {
expect(findSshTargetByName(twoOpenclaw, 'openclaw')).toBeUndefined()
expect(ambiguousSshTargets(twoOpenclaw, 'openclaw')).toHaveLength(2)
})
it('refuses to guess between two servers sharing a name', () => {
expect(findEnvironmentByName(twoAwin, 'awin')).toBeUndefined()
expect(ambiguousEnvironments(twoAwin, 'awin')).toHaveLength(2)
})
// An exact id is never ambiguous, even when labels collide.
it('still resolves an exact id past a colliding label', () => {
expect(findSshTargetByName(twoOpenclaw, 'ssh-2-b')?.id).toBe('ssh-2-b')
expect(findEnvironmentByName(twoAwin, 'env-2')?.id).toBe('env-2')
})
it('reports no ambiguity for a unique name', () => {
expect(ambiguousSshTargets(SSH_TARGETS, 'openclaw')).toEqual([])
expect(ambiguousEnvironments(ENVIRONMENTS, 'awin')).toEqual([])
})
it('names both candidates when an ssh label is ambiguous', async () => {
await expect(
resolveSshHostTargetId(clientReturning(twoOpenclaw), 'openclaw', ENVIRONMENTS)
).rejects.toMatchObject({
data: {
nextSteps: expect.arrayContaining([
expect.stringContaining('ssh-1-a'),
expect.stringContaining('ssh-2-b')
])
}
})
})
})