mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 00:27:39 +00:00
* 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>
89 lines
3.6 KiB
TypeScript
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)
|
|
})
|
|
})
|