mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 08:03:33 +00:00
* feat(cli): report SSH host platforms * feat(cli): include SSH connection status * fix(cli): preserve unknown SSH connection state
198 lines
7.7 KiB
TypeScript
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')
|
|
])
|
|
}
|
|
})
|
|
})
|
|
})
|