Files
orca/src/main/ipc/ssh-plain-ssh-connect.test.ts
T
dfdcfcf61f feat(ssh): plain SSH terminals and SFTP browsing when no Orca runtime can run (#24147)
* feat(ssh): connect in plain SSH mode when no Orca runtime can run on the host

Runtime ladder rung D (design D6): instead of failing the connect, register an
ssh2 shell-channel PTY provider and an SFTP-only filesystem provider and publish
the classified reason on the SSH connection state.

* fix(ssh): harden plain SSH mode against stale reconnects, host sleep and tilde cwd

- Only the current connect or reconnect attempt may enter plain SSH mode; a superseded
  reconnect whose ladder ends at rung D no longer registers a second provider set.
- Host-sleep resume probes a plain session over SFTP instead of always reconnecting,
  which ended every open plain shell.
- A home-relative cwd keeps its tilde outside the quotes so the shell expands it.
- The SFTP provider implements folder download, which the connect state advertises.

* docs(ssh): rung D now means plain SSH mode, not a failed connect

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-01 03:24:26 -07:00

89 lines
3.6 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = await vi.hoisted(async () => {
const { createSshIpcMocks } = await import('./ssh-ipc-module-mocks')
return createSshIpcMocks()
})
vi.mock('../ssh/ssh-config-host-picker', () => mocks.sshConfigHostPicker)
vi.mock('electron', () => mocks.electron)
vi.mock('./ssh-pty-output-intake-registry', () => mocks.sshPtyOutputIntakeRegistry)
vi.mock('../ssh/ssh-connection-store', () => mocks.sshConnectionStore)
vi.mock('../ssh/ssh-connection-manager', () => mocks.sshConnectionManager)
vi.mock('../ssh/ssh-relay-deploy', () => mocks.sshRelayDeploy)
vi.mock('../ssh/ssh-relay-reset', () => mocks.sshRelayReset)
vi.mock('../ssh/ssh-channel-multiplexer', () => mocks.sshChannelMultiplexer)
vi.mock('../providers/ssh-pty-provider', () => mocks.sshPtyProvider)
vi.mock('../providers/ssh-filesystem-provider', () => mocks.sshFilesystemProvider)
vi.mock('./pty', () => mocks.pty)
vi.mock('../providers/ssh-filesystem-dispatch', () => mocks.sshFilesystemDispatch)
vi.mock('../providers/ssh-git-provider', () => mocks.sshGitProvider)
vi.mock('../providers/ssh-git-dispatch', () => mocks.sshGitDispatch)
vi.mock('../ssh/ssh-port-forward', () => mocks.sshPortForward)
vi.mock('../ssh/ssh-port-scanner', () => mocks.sshPortScanner)
import type { SshConnectionState, SshTarget } from '../../shared/ssh-types'
import {
RelayRuntimeLadderRun,
RemoteRuntimeUnavailableError
} from '../ssh/ssh-relay-runtime-resolution'
import { createSshIpcHarness } from './ssh-ipc-test-harness'
const { mockSshStore, mockConnectionManager, mockDeployAndLaunchRelay } = mocks
function noexecHome(): RemoteRuntimeUnavailableError {
const run = new RelayRuntimeLadderRun('ssh-1', null)
run.refused('A', 'noexec')
return new RemoteRuntimeUnavailableError('home_noexec', run)
}
function connectedStates(send: ReturnType<typeof vi.fn>): SshConnectionState[] {
return (
send.mock.calls
.filter(([channel]) => channel === 'ssh:state-changed')
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: ssh:state-changed always carries { targetId, state }.
.map(([, payload]) => (payload as { state: SshConnectionState }).state)
.filter((state) => state.status === 'connected')
)
}
describe('ssh:connect when no Orca runtime can run on the host', () => {
const harness = createSshIpcHarness(mocks)
const { handlers, mockWindow } = harness
beforeEach(harness.reset)
it('connects in plain SSH mode, publishes the reason, and keeps it across a refresh', async () => {
const target: SshTarget = {
id: 'ssh-1',
label: 'Server',
host: 'example.com',
port: 22,
username: 'deploy'
}
const conn = { usesSystemSshTransport: () => false, shell: vi.fn(), sftp: vi.fn() }
mockSshStore.getTarget.mockReturnValue(target)
mockConnectionManager.connect.mockResolvedValue(conn)
mockConnectionManager.getConnection.mockReturnValue(conn)
mockConnectionManager.getState.mockReturnValue({
targetId: 'ssh-1',
status: 'connected',
error: null,
reconnectAttempt: 0
})
mockDeployAndLaunchRelay.mockRejectedValue(noexecHome())
await handlers.get('ssh:connect')!(null, { targetId: 'ssh-1' })
const [connected] = connectedStates(mockWindow.webContents.send)
expect(connected?.plainSsh).toEqual({
reason: 'home_noexec',
message: expect.stringContaining('the home directory is mounted noexec')
})
// A window reactivation re-fires ssh:connect; it must not redeploy and kill the plain shells.
await handlers.get('ssh:connect')!(null, { targetId: 'ssh-1' })
expect(mockDeployAndLaunchRelay).toHaveBeenCalledTimes(1)
})
})