mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
feat(mobile): offer a server's SSH repos on the New Workspace sheet
The desktop's composer offers a paired server's own SSH repos and connects them through that server. The phone left them out only because ssh.* was the Mac's own; it now reaches the server (#26519), so the drawer's existing SSH gate connects through the picked repo's server client. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -397,6 +397,99 @@ describe('NewWorktreeModal project targets', () => {
|
||||
expect(onCreated).toHaveBeenCalledWith('srv-wt', expect.any(String), undefined, 'runtime:env-1')
|
||||
})
|
||||
|
||||
it("connects and creates on a server's own SSH repo through that server", async () => {
|
||||
const serverSshRepo = {
|
||||
...repos[0]!,
|
||||
id: 'server-ssh',
|
||||
path: '/home/dev/orca',
|
||||
connectionId: 'devbox'
|
||||
}
|
||||
const sshState = (status: string) =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
result: { state: { targetId: 'devbox', status, error: null, reconnectAttempt: 0 } }
|
||||
})
|
||||
const reply = (method: string, listed: unknown[], hostPlatform: string) => {
|
||||
if (method === 'repo.list') {
|
||||
return Promise.resolve({ ok: true, result: { repos: listed } })
|
||||
}
|
||||
if (method === 'status.get') {
|
||||
return Promise.resolve({ ok: true, result: { hostPlatform } })
|
||||
}
|
||||
if (method === 'ssh.getState') {
|
||||
return sshState('disconnected')
|
||||
}
|
||||
if (method === 'ssh.connect') {
|
||||
return sshState('connected')
|
||||
}
|
||||
if (method === 'worktree.create') {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
result: { worktree: { id: 'ssh-wt', displayName: 'ssh-wt', path: '/home/dev/ssh-wt' } }
|
||||
})
|
||||
}
|
||||
if (method === 'settings.get') {
|
||||
return Promise.resolve({ ok: true, result: { settings: {} } })
|
||||
}
|
||||
return Promise.resolve({ ok: true, result: {} })
|
||||
}
|
||||
const desktop = vi.fn((method: string) => reply(method, repos, 'darwin'))
|
||||
const server = vi.fn((method: string) => reply(method, [serverSshRepo], 'linux'))
|
||||
const onCreated = vi.fn()
|
||||
recordHostDescriptor('host-1', { machineName: null, platform: 'darwin' })
|
||||
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
createElement(NewWorktreeModal, {
|
||||
visible: true,
|
||||
client: requestsOnly(desktop),
|
||||
serverClients: new Map([['runtime:env-1', requestsOnly(server)]]),
|
||||
hostId: 'host-1',
|
||||
hostLabelById: new Map([['runtime:env-1', 'scratch-server']]),
|
||||
onCreated,
|
||||
onClose: () => {}
|
||||
})
|
||||
)
|
||||
})
|
||||
await flushUpdates()
|
||||
|
||||
expect(pickerItems(renderer, 'Run on').map((item) => [item.label, item.detail])).toEqual([
|
||||
[LOCAL_HOST_LABEL, '/src/orca'],
|
||||
['scratch-server', '/home/dev/orca']
|
||||
])
|
||||
const picker = runTargetPicker(renderer)
|
||||
await act(async () => picker.props.onSelect(picker.props.items[1]))
|
||||
await flushUpdates()
|
||||
expect(server).toHaveBeenCalledWith('ssh.getState', { targetId: 'devbox' })
|
||||
|
||||
const connectable = renderer.root.find((node) => typeof node.props.onConnect === 'function')
|
||||
await act(async () => connectable.props.onConnect())
|
||||
await flushUpdates()
|
||||
expect(server).toHaveBeenCalledWith(
|
||||
'ssh.connect',
|
||||
{ targetId: 'devbox' },
|
||||
expect.objectContaining({ timeoutMs: 120_000 })
|
||||
)
|
||||
expect(server).toHaveBeenCalledWith('preflight.detectRemoteAgents', { connectionId: 'devbox' })
|
||||
|
||||
act(() => sourceInputs(renderer)[0]!.props.onChangeText('ssh-wt'))
|
||||
const form = renderer.root.find((node) => typeof node.props.onCreate === 'function')
|
||||
await act(async () => form.props.onCreate())
|
||||
await flushUpdates()
|
||||
|
||||
expect(server).toHaveBeenCalledWith(
|
||||
'worktree.create',
|
||||
expect.objectContaining({ repo: 'id:server-ssh' }),
|
||||
expect.anything()
|
||||
)
|
||||
expect(onCreated).toHaveBeenCalledWith('ssh-wt', expect.any(String), undefined, 'runtime:env-1')
|
||||
// The server holds this SSH target, so the desktop is never asked about it.
|
||||
const desktopMethods = desktop.mock.calls.map(([method]) => method)
|
||||
expect(desktopMethods.filter((method) => method.startsWith('ssh.'))).toEqual([])
|
||||
expect(desktopMethods).not.toContain('preflight.detectRemoteAgents')
|
||||
expect(desktopMethods).not.toContain('worktree.create')
|
||||
})
|
||||
|
||||
it("lists the desktop's repos at once while a server has not answered", async () => {
|
||||
const desktop = vi.fn((method: string) =>
|
||||
method === 'repo.list'
|
||||
|
||||
@@ -23,17 +23,14 @@ async function listHostRepos(client: RpcClient): Promise<MobileWorkspaceRepo[] |
|
||||
|
||||
/**
|
||||
* A server's repos tagged with that server, as the desktop's composer offers them. Its SSH repos
|
||||
* are left out: their connection is the server's, which the phone's SSH gate cannot read.
|
||||
* keep their connection id, which the drawer connects through that server's client.
|
||||
*/
|
||||
async function listServerRepos(
|
||||
executionHostId: ExecutionHostId,
|
||||
client: RpcClient
|
||||
): Promise<MobileWorkspaceRepo[] | null> {
|
||||
const listed = await listHostRepos(client).catch(() => null)
|
||||
return (
|
||||
listed?.filter((repo) => !repo.connectionId).map((repo) => ({ ...repo, executionHostId })) ??
|
||||
null
|
||||
)
|
||||
return listed?.map((repo) => ({ ...repo, executionHostId })) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user