Files
orca/src/main/ssh/ssh-connection.test.ts
T
Neil a1331d2602 fix(ssh): verify host keys instead of accepting every one (STA-4319)
The actual fix. ssh-connection's verifier recorded a fingerprint and returned
true, so every ssh2 connection accepted every host key — no known_hosts consult,
no change detection. It now consults the user's known_hosts plus our own store
and refuses a changed, revoked or unverifiable key.

Phase 1 by design: no dialog. Unknown hosts are accepted and recorded
(accept-new semantics), because startup restore opens every previously-active
target at once, ephemeral VM targets present a new key each launch, and
paired-web connects run on the host desktop where a prompt would appear on
someone else's screen. The MITM defence lands now; the prompt is Phase 2.

Also sets algorithms.serverHostKey to lead with the types already known for the
host. Without it the type-scoped matching is a downgrade — an attacker who cannot
forge the key on file just presents another type and turns a hard failure into
first contact. Verified from ssh2's kex.js that the client list decides.

Denial replaces ssh2's generic handshake error with the specific reason, because
the reconnect ladder cannot distinguish a generic failure from a transient fault
and would retry forever against a decision that will never change.

An unreadable trust store degrades to known_hosts only rather than failing the
connect: a changed key is still refused, and a host trusted only by us falls back
to first contact and is re-recorded, reaching the same decision.

The ssh2 mock now uses the callback form and aborts the handshake on denial. As
written it called hostVerifier(key) with one argument and ignored the result, so
it would have passed against a verifier that never decides — flagged in the
design as a mock that had to change, not a test to quietly rewrite. Two new tests
pin the wiring rather than the module: an unidentifiable blob is refused, and a
well-formed key is accepted.

Note for review: commit 2d2a0880ba unintentionally swept in two modules built
concurrently (ssh-known-hosts-source, ssh-host-key-store) because I staged with
'git add -A'; its message describes only the verifier. Both are covered by their
own tests, but the attribution in that commit is wrong.

1461 SSH tests pass.
2026-08-14 03:03:45 -07:00

2813 lines
101 KiB
TypeScript

/* eslint-disable max-lines -- Why: SSH connection lifecycle tests share one ssh2 mock so auth, reconnect, and system-transport behavior stay consistent. */
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { Socket } from 'node:net'
import { EventEmitter } from 'node:events'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
let eventHandlers: Map<string, Set<(...args: unknown[]) => void>>
let connectBehavior: 'ready' | 'error' = 'ready'
let connectErrorMessage = ''
let connectErrorCode = ''
let destroyErrorMessage = ''
let connectSequence: ('ready' | Error)[] = []
let connectAttempts = 0
/** Lets a test present a real key blob instead of the placeholder. */
const VALID_ED25519_HOST_KEY = Buffer.from(
'AAAAC3NzaC1lZDI1NTE5AAAAIKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq',
'base64'
)
let mockPresentedHostKey: Buffer | undefined
/** What the verifier decided about the presented key on the most recent connect. */
let lastHostKeyAccepted: boolean | undefined
let execBehavior: 'callback' | 'pending' = 'callback'
let pendingExecCallback: ((err: Error | undefined, channel: unknown) => void) | null = null
let sftpBehavior: 'callback' | 'pending' = 'callback'
let pendingSftpCallback: ((err: Error | undefined, channel: unknown) => void) | null = null
let notifyClientCreated: (() => void) | undefined
type MockSshClient = {
setNoDelay: ReturnType<typeof vi.fn>
_sock: Socket | undefined
lastExecCommand?: string
lastConnectConfig?: unknown
exec: (cmd: string, cb: (err: Error | undefined, channel: unknown) => void) => void
sftp: (cb: (err: Error | undefined, channel: unknown) => void) => void
}
let clientInstances: MockSshClient[] = []
function emitSshEvent(event: string, ...args: unknown[]): void {
for (const handler of eventHandlers?.get(event) ?? []) {
handler(...args)
}
}
function nextSshClientCreation(): Promise<void> {
return new Promise((resolve) => {
notifyClientCreated = resolve
})
}
async function connectWithFakeTimers(conn: SshConnection): Promise<void> {
const clientCreated = nextSshClientCreation()
const connected = conn.connect()
await clientCreated
await vi.advanceTimersByTimeAsync(1)
await connected
}
async function advanceToNextSshClient(delayMs: number): Promise<void> {
const clientCreated = nextSshClientCreation()
await vi.advanceTimersByTimeAsync(delayMs)
await clientCreated
await vi.advanceTimersByTimeAsync(1)
}
vi.mock('ssh2', () => {
class MockBaseAgent {}
class MockSshClient {
setNoDelay = vi.fn()
// Why: production code reads `client._sock` and checks `instanceof net.Socket`
// to decide which log line to emit. A real Socket instance lets the test
// exercise the "enabled" branch instead of the "skipped (proxy socket)" branch.
_sock: Socket | undefined = new Socket()
lastExecCommand?: string
lastConnectConfig?: unknown
constructor() {
clientInstances.push(this)
notifyClientCreated?.()
notifyClientCreated = undefined
}
on(event: string, handler: (...args: unknown[]) => void) {
const handlers = eventHandlers?.get(event) ?? new Set<(...args: unknown[]) => void>()
handlers.add(handler)
eventHandlers?.set(event, handlers)
}
off(event: string, handler: (...args: unknown[]) => void) {
const handlers = eventHandlers?.get(event)
handlers?.delete(handler)
if (handlers?.size === 0) {
eventHandlers.delete(event)
}
}
connect(config?: unknown) {
connectAttempts += 1
this.lastConnectConfig = config
// Why the callback form: ssh2 calls hostVerifier(key, verify) and only accepts synchronously
// when the return is not undefined. A mock that passed one argument and ignored the result
// would pass against a verifier that never decides — which is the regression this whole
// module exists to prevent.
const hostVerifier = (
config as
| { hostVerifier?: (key: Buffer, verify: (ok: boolean) => void) => undefined }
| undefined
)?.hostVerifier
const presentedHostKey = mockPresentedHostKey ?? VALID_ED25519_HOST_KEY
lastHostKeyAccepted = undefined
hostVerifier?.(presentedHostKey, (ok) => {
lastHostKeyAccepted = ok
})
if (lastHostKeyAccepted === false) {
// ssh2 aborts the handshake when the verifier denies; a mock that carried on to 'ready'
// would let a rejected host key look like a successful connect.
setTimeout(
() => emitSshEvent('error', new Error('All configured authentication methods failed')),
0
)
return
}
setTimeout(() => {
const next = connectSequence.shift()
if (next instanceof Error) {
emitSshEvent('error', next)
return
}
if (next === 'ready') {
emitSshEvent('ready')
return
}
if (connectBehavior === 'error') {
const err = new Error(connectErrorMessage) as NodeJS.ErrnoException
if (connectErrorCode) {
err.code = connectErrorCode
}
emitSshEvent('error', err)
} else {
emitSshEvent('ready')
}
}, 0)
}
end() {}
destroy() {
if (!destroyErrorMessage) {
return
}
if (eventHandlers?.has('error')) {
emitSshEvent('error', new Error(destroyErrorMessage))
return
}
throw new Error(destroyErrorMessage)
}
exec(cmd: string, cb: (err: Error | undefined, channel: unknown) => void) {
this.lastExecCommand = cmd
if (execBehavior === 'pending') {
pendingExecCallback = cb
return
}
cb(undefined, { close: vi.fn() })
}
sftp(cb: (err: Error | undefined, channel: unknown) => void) {
if (sftpBehavior === 'pending') {
pendingSftpCallback = cb
return
}
cb(undefined, { end: vi.fn() })
}
}
return {
BaseAgent: MockBaseAgent,
Client: MockSshClient,
createAgent: vi.fn(),
utils: {
parseKey: vi.fn()
}
}
})
const {
findSystemSshMock,
getOrcaControlSocketPathMock,
removeControlSocketPathMock,
spawnSystemSshCommandMock,
spawnSystemSshMock
} = vi.hoisted(() => ({
findSystemSshMock: vi.fn<() => string | null>(),
getOrcaControlSocketPathMock: vi.fn(),
removeControlSocketPathMock: vi.fn(),
spawnSystemSshMock: vi.fn(),
spawnSystemSshCommandMock: vi.fn()
}))
// Why: security-key transport selection scans the real ~/.ssh defaults, so a developer's own
// FIDO2 key would otherwise decide which transport these tests take.
vi.mock('./system-ssh-binary', () => ({ findSystemSsh: findSystemSshMock }))
vi.mock('./ssh-system-fallback', () => ({
getOrcaControlSocketPath: getOrcaControlSocketPathMock,
spawnSystemSsh: spawnSystemSshMock,
spawnSystemSshCommand: spawnSystemSshCommandMock,
downloadFileViaSystemSsh: vi.fn(),
uploadDirectoryViaSystemSsh: vi.fn(),
uploadFileViaSystemSsh: vi.fn(),
writeBufferViaSystemSsh: vi.fn(),
writeFileViaSystemSsh: vi.fn()
}))
vi.mock('./ssh-control-socket', () => ({
removeControlSocketPath: removeControlSocketPathMock
}))
vi.mock('./ssh-config-parser', () => ({
resolveWithSshG: vi.fn().mockResolvedValue(null)
}))
import {
SshConnection,
shouldUseSystemSshTransport,
type SshConnectionCallbacks
} from './ssh-connection'
import { SshConnectionManager } from './ssh-connection-manager'
import { resolveWithSshG, type SshResolvedConfig } from './ssh-config-parser'
import {
downloadFileViaSystemSsh,
uploadDirectoryViaSystemSsh,
uploadFileViaSystemSsh,
writeBufferViaSystemSsh,
writeFileViaSystemSsh
} from './ssh-system-fallback'
import { getRemoteHostPlatform } from './ssh-remote-platform'
import { CONNECT_TIMEOUT_MS, RECONNECT_BACKOFF_MS } from './ssh-connection-utils'
import { MIN_SSH_RELAY_GRACE_PERIOD_SECONDS, type SshTarget } from '../../shared/ssh-types'
import {
createOpenSshPrivateKeyFixture,
createOpenSshPublicKeyFixture
} from './ssh-security-key-identity.test-fixture'
function createTarget(overrides?: Partial<SshTarget>): SshTarget {
return {
id: 'target-1',
label: 'Test Server',
host: 'example.com',
port: 22,
username: 'deploy',
...overrides
}
}
function createResolvedConfig(overrides?: Partial<SshResolvedConfig>): SshResolvedConfig {
return {
hostname: 'example.com',
port: 22,
identityFile: [],
forwardAgent: false,
identitiesOnly: false,
proxyUseFdpass: true,
controlMaster: 'no',
controlPersist: 'no',
userKnownHostsFiles: [],
globalKnownHostsFiles: [],
strictHostKeyChecking: 'ask',
hashKnownHosts: false,
updateHostKeys: 'no',
...overrides
}
}
function createCallbacks(overrides?: Partial<SshConnectionCallbacks>): SshConnectionCallbacks {
return {
onStateChange: vi.fn(),
...overrides
}
}
function createSystemCommandChannel(): EventEmitter & {
stdin: { end: ReturnType<typeof vi.fn>; write: ReturnType<typeof vi.fn> }
stderr: EventEmitter
close: ReturnType<typeof vi.fn>
} {
const channel = new EventEmitter() as EventEmitter & {
stdin: { end: ReturnType<typeof vi.fn>; write: ReturnType<typeof vi.fn> }
stderr: EventEmitter
close: ReturnType<typeof vi.fn>
}
channel.stdin = { end: vi.fn(), write: vi.fn() }
channel.stderr = new EventEmitter()
channel.close = vi.fn()
queueMicrotask(() => {
channel.emit('data', Buffer.from('ORCA-SYSTEM-SSH-OK'))
channel.emit('close', 0)
})
return channel
}
function createFailingSystemCommandChannel(
code: number,
stderrText = ''
): ReturnType<typeof createSystemCommandChannel> {
const channel = new EventEmitter() as ReturnType<typeof createSystemCommandChannel>
channel.stdin = { end: vi.fn(), write: vi.fn() }
channel.stderr = new EventEmitter()
channel.close = vi.fn()
queueMicrotask(() => {
if (stderrText) {
channel.stderr.emit('data', Buffer.from(stderrText))
}
channel.emit('close', code)
})
return channel
}
// A probe that never answers — the shape a FIDO2/system-transport host takes when the network drops.
function createHangingSystemCommandChannel(): ReturnType<typeof createSystemCommandChannel> {
const channel = new EventEmitter() as ReturnType<typeof createSystemCommandChannel>
channel.stdin = { end: vi.fn(), write: vi.fn() }
channel.stderr = new EventEmitter()
channel.close = vi.fn()
return channel
}
function createPendingSystemSshProcess() {
const stdout = new EventEmitter()
return {
stdin: {},
stdout,
stderr: new EventEmitter(),
kill: vi.fn(),
onExit: vi.fn(),
pid: 99999
}
}
function createSystemSshProcess() {
const proc = createPendingSystemSshProcess()
queueMicrotask(() => {
proc.stdout.emit('data', Buffer.from('ORCA-SYSTEM-SSH-READY'))
})
return proc
}
function createFailingSystemSshProcess(code: number) {
const proc = createPendingSystemSshProcess()
proc.onExit = vi.fn((handler: (exitCode: number | null) => void) => {
queueMicrotask(() => handler(code))
})
return proc
}
describe('SshConnection', () => {
beforeEach(() => {
eventHandlers = new Map()
connectBehavior = 'ready'
connectErrorMessage = ''
connectErrorCode = ''
destroyErrorMessage = ''
connectSequence = []
connectAttempts = 0
mockPresentedHostKey = undefined
lastHostKeyAccepted = undefined
execBehavior = 'callback'
pendingExecCallback = null
sftpBehavior = 'callback'
pendingSftpCallback = null
notifyClientCreated = undefined
clientInstances = []
getOrcaControlSocketPathMock.mockReset()
getOrcaControlSocketPathMock.mockReturnValue(null)
removeControlSocketPathMock.mockReset()
spawnSystemSshMock.mockReset()
spawnSystemSshMock.mockImplementation(() => createSystemSshProcess())
spawnSystemSshCommandMock.mockReset()
spawnSystemSshCommandMock.mockImplementation(() => createSystemCommandChannel())
vi.mocked(downloadFileViaSystemSsh).mockReset()
vi.mocked(downloadFileViaSystemSsh).mockResolvedValue(undefined)
vi.mocked(uploadDirectoryViaSystemSsh).mockReset()
vi.mocked(uploadDirectoryViaSystemSsh).mockResolvedValue(undefined)
vi.mocked(uploadFileViaSystemSsh).mockReset()
vi.mocked(uploadFileViaSystemSsh).mockResolvedValue(undefined)
vi.mocked(writeBufferViaSystemSsh).mockReset()
vi.mocked(writeBufferViaSystemSsh).mockResolvedValue(undefined)
vi.mocked(writeFileViaSystemSsh).mockReset()
vi.mocked(writeFileViaSystemSsh).mockResolvedValue(undefined)
vi.mocked(resolveWithSshG).mockReset()
vi.mocked(resolveWithSshG).mockResolvedValue(null)
findSystemSshMock.mockReset()
findSystemSshMock.mockReturnValue(null)
vi.unstubAllEnvs()
})
it('transitions to connected on successful connect', async () => {
const callbacks = createCallbacks()
const conn = new SshConnection(createTarget(), callbacks)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.getState().supportsFolderDownload).toBe(true)
expect(callbacks.onStateChange).toHaveBeenCalledWith(
'target-1',
expect.objectContaining({ status: 'connected' })
)
})
it('enables TCP_NODELAY on the ssh2 client after ready', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
expect(clientInstances).toHaveLength(1)
expect(clientInstances[0].setNoDelay).toHaveBeenCalledWith(true)
})
// Proves the WIRING, not just the module: a verifier that decides correctly is worthless if the
// handshake never consults it, and until this change ssh-connection accepted every key.
it('refuses a host key whose own blob cannot be identified', async () => {
mockPresentedHostKey = Buffer.from('not-a-real-host-key-blob')
const conn = new SshConnection(createTarget(), createCallbacks())
await expect(conn.connect()).rejects.toThrow()
expect(lastHostKeyAccepted, 'the handshake accepted an unidentifiable key').toBe(false)
})
it('accepts a well-formed host key on first contact', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
expect(lastHostKeyAccepted).toBe(true)
})
it('captures the negotiated SSH server key fingerprint', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
expect(conn.getHostKeyFingerprint()).toMatch(/^SHA256:[A-Za-z\d+/]{43}$/)
})
it('ignores a late host fingerprint from an obsolete connect generation', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
const firstVerifier = (
clientInstances[0].lastConnectConfig as {
hostVerifier?: (key: Buffer, verify: (ok: boolean) => void) => undefined
}
).hostVerifier
const privateConn = conn as unknown as { attemptConnect: () => Promise<void> }
await privateConn.attemptConnect()
const secondVerifier = (
clientInstances[1].lastConnectConfig as {
hostVerifier?: (key: Buffer, verify: (ok: boolean) => void) => undefined
}
).hostVerifier
expect(firstVerifier).toBeTypeOf('function')
expect(secondVerifier).toBeTypeOf('function')
const newerKey = Buffer.from(
'AAAAC3NzaC1lZDI1NTE5AAAAILu7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7',
'base64'
)
secondVerifier?.(newerKey, () => {})
const currentFingerprint = conn.getHostKeyFingerprint()
firstVerifier?.(VALID_ED25519_HOST_KEY, () => {})
expect(conn.getHostKeyFingerprint()).toBe(currentFingerprint)
})
it('allows concurrent exec commands for ssh2 transport', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
expect(conn.usesSystemSshTransport()).toBe(false)
expect(conn.canRunConcurrentExecCommands()).toBe(true)
})
it('removes startup listeners after ssh2 connect succeeds', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
expect(eventHandlers.has('ready')).toBe(false)
// The remaining error listener is the steady-state disconnect handler.
expect(eventHandlers.has('error')).toBe(true)
})
it('enables TCP_NODELAY on the new ssh2 client after a reconnect cycle', async () => {
// Why: guards the "Nagle is re-enabled because someone refactored only
// the initial connect path" regression class. attemptConnect bumps
// connectGeneration on every call, and both the initial connect and the
// explicit reconnect path go through doSsh2Connect → client.on('ready').
// The new client must also receive setNoDelay(true).
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
expect(clientInstances).toHaveLength(1)
expect(clientInstances[0].setNoDelay).toHaveBeenCalledWith(true)
// Simulate the reconnect path: a fresh attemptConnect run via the
// internal helper that scheduleReconnect uses. Easiest from the public
// API is to call connect() again — disposed/connected guard rejects, so
// we exercise the path via a private call. Use the bracket-access
// form to keep the test free of `any` casts.
const privateConn = conn as unknown as {
attemptConnect: () => Promise<void>
}
await privateConn.attemptConnect()
expect(clientInstances).toHaveLength(2)
expect(clientInstances[1].setNoDelay).toHaveBeenCalledWith(true)
})
it('forces a fresh SSH connection for an explicit reconnect', async () => {
const states: string[] = []
const conn = new SshConnection(
createTarget(),
createCallbacks({
onStateChange: vi.fn((_id, state) => states.push(state.status))
})
)
await conn.connect()
await conn.reconnect()
expect(clientInstances).toHaveLength(2)
expect(states).toEqual(['connecting', 'connected', 'reconnecting', 'connecting', 'connected'])
expect(conn.getState().status).toBe('connected')
})
it('escalates the backoff across repeated post-handshake drops', async () => {
vi.useFakeTimers()
try {
const conn = new SshConnection(createTarget(), createCallbacks())
await connectWithFakeTimers(conn)
expect(clientInstances).toHaveLength(1)
emitSshEvent('close')
await vi.advanceTimersByTimeAsync(999)
expect(clientInstances).toHaveLength(1)
await advanceToNextSshClient(1)
expect(clientInstances).toHaveLength(2)
// Why: a single published counter pinned every post-handshake drop at the 1000ms head step.
emitSshEvent('close')
await vi.advanceTimersByTimeAsync(1999)
expect(clientInstances).toHaveLength(2)
await advanceToNextSshClient(1)
expect(clientInstances).toHaveLength(3)
} finally {
vi.useRealTimers()
}
})
it('publishes reconnectAttempt=0 on the reconnected state and the escalating step while reconnecting', async () => {
vi.useFakeTimers()
try {
const published: { status: string; reconnectAttempt: number }[] = []
const conn = new SshConnection(
createTarget(),
createCallbacks({
onStateChange: vi.fn((_id, state) =>
published.push({ status: state.status, reconnectAttempt: state.reconnectAttempt })
)
})
)
await connectWithFakeTimers(conn)
for (let drop = 0; drop < 3; drop++) {
emitSshEvent('close')
await advanceToNextSshClient(30_000)
}
expect(
published.filter((e) => e.status === 'reconnecting').map((e) => e.reconnectAttempt)
).toEqual([0, 1, 2])
// src/main/ipc/ssh.ts gates the relay redeploy on reconnectAttempt === 0 at 'connected'.
expect(
published.filter((e) => e.status === 'connected').map((e) => e.reconnectAttempt)
).toEqual([0, 0, 0, 0])
} finally {
vi.useRealTimers()
}
})
it('restarts the ladder at the head when an explicit reconnect fails', async () => {
// Accepted delta: reset() puts the ladder at the head and the explicit attempt consumes no
// step, so the first failure publishes 0/1000ms where the single-counter version published 1/2000ms.
vi.useFakeTimers()
try {
const published: { status: string; reconnectAttempt: number }[] = []
const conn = new SshConnection(
createTarget(),
createCallbacks({
onStateChange: vi.fn((_id, state) =>
published.push({ status: state.status, reconnectAttempt: state.reconnectAttempt })
)
})
)
await connectWithFakeTimers(conn)
connectBehavior = 'error'
connectErrorMessage = 'connect ETIMEDOUT 10.0.0.5:22'
connectErrorCode = 'ETIMEDOUT'
published.length = 0
const clientCreated = nextSshClientCreation()
const reconnected = conn.reconnect()
await clientCreated
await vi.advanceTimersByTimeAsync(1)
await reconnected
// Shipped published [0, 1] here; the ladder's reset() keeps the retry at the head instead.
expect(
published.filter((e) => e.status === 'reconnecting').map((e) => e.reconnectAttempt)
).toEqual([0, 0])
} finally {
vi.useRealTimers()
}
})
it('reaches reconnection-failed after 9 consecutive handshake failures', async () => {
vi.useFakeTimers()
try {
const statuses: string[] = []
const conn = new SshConnection(
createTarget(),
createCallbacks({
onStateChange: vi.fn((_id, state) => statuses.push(state.status))
})
)
await connectWithFakeTimers(conn)
connectBehavior = 'error'
connectErrorMessage = 'connect ETIMEDOUT 10.0.0.5:22'
connectErrorCode = 'ETIMEDOUT'
emitSshEvent('close')
for (const delayMs of RECONNECT_BACKOFF_MS) {
await advanceToNextSshClient(delayMs)
}
expect(statuses).toContain('reconnection-failed')
// Pin the budget itself: the initial success plus exactly RECONNECT_BACKOFF_MS.length retries.
// Counting a failure twice, or giving up early, would strand a user on a flaky link.
expect(connectAttempts).toBe(1 + RECONNECT_BACKOFF_MS.length)
} finally {
vi.useRealTimers()
}
})
it('retries a saturated flap streak while the remote relay is still in grace', async () => {
vi.useFakeTimers()
try {
const conn = new SshConnection(createTarget(), createCallbacks())
await connectWithFakeTimers(conn)
// Saturate the delay ladder on flaps alone; 45s reconnects each drop while staying under
// STABLE_CONNECTION_MS, so the ladder never resets to the head.
for (let drop = 0; drop < 12; drop++) {
emitSshEvent('close')
await advanceToNextSshClient(45_000)
}
expect(conn.getState().status).toBe('connected')
const before = clientInstances.length
emitSshEvent('close')
// The retry plus a worst-case handshake must land inside the shortest configurable relay grace,
// or the remote daemon shuts down and takes every PTY on that host with it.
await advanceToNextSshClient(
MIN_SSH_RELAY_GRACE_PERIOD_SECONDS * 1000 - CONNECT_TIMEOUT_MS - 1
)
expect(clientInstances.length).toBeGreaterThan(before)
} finally {
vi.useRealTimers()
}
})
it('logs the delay ladder position separately from the failure streak', async () => {
vi.useFakeTimers()
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
const conn = new SshConnection(createTarget(), createCallbacks())
await connectWithFakeTimers(conn)
for (let drop = 0; drop < 12; drop++) {
emitSshEvent('close')
await advanceToNextSshClient(45_000)
}
const lastReconnectLog = warn.mock.calls
.map((call) => String(call[0]))
.findLast((line) => line.includes('Reconnecting to'))
// A saturated flap ladder must not read like the connection is one step from giving up.
expect(lastReconnectLog).toContain('delay step 9/9')
expect(lastReconnectLog).toContain('failed handshakes 0/9')
} finally {
warn.mockRestore()
vi.useRealTimers()
}
})
it('keeps retrying when a flap streak is followed by one handshake failure', async () => {
vi.useFakeTimers()
try {
const statuses: string[] = []
const conn = new SshConnection(
createTarget(),
createCallbacks({
onStateChange: vi.fn((_id, state) => statuses.push(state.status))
})
)
await connectWithFakeTimers(conn)
// 12 flaps saturate the delay ladder without ever touching the failure streak.
for (let drop = 0; drop < 12; drop++) {
emitSshEvent('close')
await advanceToNextSshClient(30_000)
}
connectSequence = [new Error('connect ETIMEDOUT 10.0.0.5:22')]
emitSshEvent('close')
await advanceToNextSshClient(30_000)
await advanceToNextSshClient(30_000)
expect(statuses).not.toContain('reconnection-failed')
expect(conn.getState().status).toBe('connected')
} finally {
vi.useRealTimers()
}
})
it('keeps a system-transport target on the ladder after a probe timeout', async () => {
vi.useFakeTimers()
try {
vi.mocked(resolveWithSshG).mockResolvedValue(createResolvedConfig())
const statuses: string[] = []
const conn = new SshConnection(
createTarget(),
createCallbacks({
onStateChange: vi.fn((_id, state) => statuses.push(state.status))
})
)
await conn.connect()
expect(conn.usesSystemSshTransport()).toBe(true)
// The probe times out with OpenSSH prose, not an errno the transient code table matches.
spawnSystemSshCommandMock.mockImplementation(() => createHangingSystemCommandChannel())
statuses.length = 0
const reconnected = conn.reconnect()
await vi.advanceTimersByTimeAsync(CONNECT_TIMEOUT_MS)
await reconnected
// Shipped published 'error' here, stranding FIDO2 hosts with no path back short of a restart.
expect(statuses).not.toContain('error')
expect(conn.getState().status).toBe('reconnecting')
const probesBefore = spawnSystemSshCommandMock.mock.calls.length
await vi.advanceTimersByTimeAsync(RECONNECT_BACKOFF_MS[0])
expect(spawnSystemSshCommandMock.mock.calls.length).toBeGreaterThan(probesBefore)
} finally {
vi.useRealTimers()
}
})
it('publishes no cancellation error when a connect supersedes a parked ladder attempt', async () => {
const published: { status: string; error?: string }[] = []
let releaseParked: (() => void) | null = null
vi.mocked(resolveWithSshG).mockImplementationOnce(
() =>
new Promise((resolve) => {
releaseParked = () => resolve(null)
})
)
const conn = new SshConnection(
createTarget(),
createCallbacks({
onStateChange: vi.fn((_id, state) => published.push({ status: state.status, ...state }))
})
)
// The ladder attempt parks inside ssh -G while the user presses Connect.
const parked = conn.reconnect()
await conn.connect()
releaseParked!()
await parked
expect(conn.getState().status).toBe('connected')
// Shipped published error:'SSH connection attempt was cancelled' over a live connection.
expect(published.filter((entry) => entry.status === 'error')).toEqual([])
expect(published.map((entry) => entry.error).filter(Boolean)).toEqual([])
})
it('rejects a superseded connect without publishing a permanent error', async () => {
const published: string[] = []
let releaseParked: (() => void) | null = null
vi.mocked(resolveWithSshG).mockImplementationOnce(
() =>
new Promise((resolve) => {
releaseParked = () => resolve(null)
})
)
const conn = new SshConnection(
createTarget(),
createCallbacks({
onStateChange: vi.fn((_id, state) => published.push(state.status))
})
)
const superseded = conn.connect()
await conn.connect()
releaseParked!()
await expect(superseded).rejects.toThrow('SSH connection attempt was cancelled')
expect(conn.getState().status).toBe('connected')
expect(published).not.toContain('error')
})
it('transitions through connecting → connected states', async () => {
const states: string[] = []
const callbacks = createCallbacks({
onStateChange: vi.fn((_id, state) => states.push(state.status))
})
const conn = new SshConnection(createTarget(), callbacks)
await conn.connect()
expect(states).toContain('connecting')
expect(states).toContain('connected')
})
it('reports error state on connection failure', async () => {
connectBehavior = 'error'
connectErrorMessage = 'Connection refused'
const callbacks = createCallbacks()
const conn = new SshConnection(createTarget(), callbacks)
await expect(conn.connect()).rejects.toThrow('Connection refused')
expect(conn.getState().status).toBe('error')
})
it('guards late ssh2 errors emitted while destroying a failed startup client', async () => {
connectBehavior = 'error'
connectErrorMessage = 'Connection lost before handshake'
destroyErrorMessage = 'Connection lost before handshake'
const callbacks = createCallbacks()
const conn = new SshConnection(createTarget(), callbacks)
await expect(conn.connect()).rejects.toThrow('Connection lost before handshake')
expect(conn.getState().status).toBe('error')
})
it('disconnect cleans up and sets state to disconnected', async () => {
const callbacks = createCallbacks()
const conn = new SshConnection(createTarget(), callbacks)
await conn.connect()
await conn.disconnect()
expect(conn.getState().status).toBe('disconnected')
})
it('rejects late ssh2 ready after disconnect without resurrecting the connection', async () => {
const callbacks = createCallbacks()
const conn = new SshConnection(createTarget(), callbacks)
const clientCreated = new Promise<void>((resolve) => {
notifyClientCreated = resolve
})
const connectResult = conn.connect().catch((error: Error) => error)
await clientCreated
expect(clientInstances).toHaveLength(1)
await conn.disconnect()
await expect(connectResult).resolves.toMatchObject({
message: 'SSH connection attempt was cancelled'
})
expect(conn.getState()).toMatchObject({ status: 'disconnected', error: null })
expect(callbacks.onStateChange).not.toHaveBeenCalledWith(
'target-1',
expect.objectContaining({ status: 'connected' })
)
})
it('keeps disconnected state when ssh2 reports a late startup error', async () => {
connectBehavior = 'error'
connectErrorMessage = 'Connection lost before handshake'
const callbacks = createCallbacks()
const conn = new SshConnection(createTarget(), callbacks)
const clientCreated = new Promise<void>((resolve) => {
notifyClientCreated = resolve
})
const connectResult = conn.connect().catch((error: Error) => error)
await clientCreated
expect(clientInstances).toHaveLength(1)
await conn.disconnect()
await expect(connectResult).resolves.toMatchObject({
message: 'Connection lost before handshake'
})
expect(conn.getState()).toMatchObject({ status: 'disconnected', error: null })
expect(callbacks.onStateChange).not.toHaveBeenCalledWith(
'target-1',
expect.objectContaining({ status: 'error' })
)
})
it('getTarget returns a copy of the target', () => {
const target = createTarget()
const conn = new SshConnection(target, createCallbacks())
const returned = conn.getTarget()
expect(returned).toEqual(target)
expect(returned).not.toBe(target)
})
it('getState returns a copy of the state', () => {
const conn = new SshConnection(createTarget(), createCallbacks())
const state1 = conn.getState()
const state2 = conn.getState()
expect(state1).toEqual(state2)
expect(state1).not.toBe(state2)
})
it('throws when connecting a disposed connection', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.disconnect()
await expect(conn.connect()).rejects.toThrow('Connection disposed')
})
it('resolves OpenSSH config using configHost when present', async () => {
const callbacks = createCallbacks()
const conn = new SshConnection(
createTarget({
label: 'Friendly Name',
configHost: 'ssh-alias'
}),
callbacks
)
await conn.connect()
expect(resolveWithSshG).toHaveBeenCalledWith('ssh-alias')
})
it('tries ssh-agent before reading an explicit private key', async () => {
vi.stubEnv('SSH_AUTH_SOCK', '/tmp/agent.sock')
const callbacks = createCallbacks({
onCredentialRequest: vi.fn()
})
const conn = new SshConnection(
createTarget({
identityFile: '/tmp/encrypted-key'
}),
callbacks
)
await conn.connect()
const initialConfig = clientInstances[0].lastConnectConfig as {
agent?: unknown
privateKey?: unknown
}
expect(initialConfig.agent).toBe('/tmp/agent.sock')
expect(initialConfig.privateKey).toBeUndefined()
expect(callbacks.onCredentialRequest).not.toHaveBeenCalled()
})
it('falls back to direct private key auth when agent auth fails', async () => {
vi.stubEnv('SSH_AUTH_SOCK', '/tmp/agent.sock')
const tempDir = mkdtempSync(join(tmpdir(), 'orca-ssh-key-'))
const keyPath = join(tempDir, 'id_ed25519')
writeFileSync(keyPath, 'test-key')
connectSequence = [new Error('All configured authentication methods failed'), 'ready']
try {
const conn = new SshConnection(createTarget({ identityFile: keyPath }), createCallbacks())
await conn.connect()
expect(clientInstances).toHaveLength(2)
const initialConfig = clientInstances[0].lastConnectConfig as {
agent?: unknown
privateKey?: unknown
}
const fallbackConfig = clientInstances[1].lastConnectConfig as {
agent?: unknown
privateKey?: Buffer
}
expect(initialConfig.agent).toBe('/tmp/agent.sock')
expect(initialConfig.privateKey).toBeUndefined()
expect(fallbackConfig.agent).toBeUndefined()
expect(fallbackConfig.privateKey).toEqual(Buffer.from('test-key'))
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
})
it('falls back to direct private key auth when the agent socket is unavailable', async () => {
vi.stubEnv('SSH_AUTH_SOCK', '/tmp/stale-agent.sock')
const tempDir = mkdtempSync(join(tmpdir(), 'orca-ssh-key-'))
const keyPath = join(tempDir, 'id_ed25519')
writeFileSync(keyPath, 'test-key')
const agentError = new Error('Failed to connect to agent') as Error & { level: string }
agentError.level = 'agent'
connectSequence = [agentError, 'ready']
try {
const conn = new SshConnection(createTarget({ identityFile: keyPath }), createCallbacks())
await conn.connect()
expect(clientInstances).toHaveLength(2)
const fallbackConfig = clientInstances[1].lastConnectConfig as {
agent?: unknown
privateKey?: Buffer
}
expect(fallbackConfig.agent).toBeUndefined()
expect(fallbackConfig.privateKey).toEqual(Buffer.from('test-key'))
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
})
it('falls back to direct private key auth after too many agent authentication failures', async () => {
vi.stubEnv('SSH_AUTH_SOCK', '/tmp/agent.sock')
const tempDir = mkdtempSync(join(tmpdir(), 'orca-ssh-key-'))
const keyPath = join(tempDir, 'id_ed25519')
writeFileSync(keyPath, 'test-key')
connectSequence = [new Error('Received disconnect: Too many authentication failures'), 'ready']
try {
const conn = new SshConnection(createTarget({ identityFile: keyPath }), createCallbacks())
await conn.connect()
expect(clientInstances).toHaveLength(2)
const fallbackConfig = clientInstances[1].lastConnectConfig as {
agent?: unknown
privateKey?: Buffer
}
expect(fallbackConfig.agent).toBeUndefined()
expect(fallbackConfig.privateKey).toEqual(Buffer.from('test-key'))
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
})
it('retries password auth without a stale agent when no private key fallback exists', async () => {
vi.stubEnv('SSH_AUTH_SOCK', '/tmp/stale-agent.sock')
const agentError = new Error('Failed to connect to agent') as Error & { level: string }
agentError.level = 'agent'
connectSequence = [agentError, 'ready']
const onCredentialRequest = vi.fn(async () => 'password-123')
const conn = new SshConnection(
createTarget({ identityFile: join(tmpdir(), 'missing-key') }),
createCallbacks({ onCredentialRequest })
)
await conn.connect()
expect(clientInstances).toHaveLength(2)
const retryConfig = clientInstances[1].lastConnectConfig as {
agent?: unknown
password?: string
privateKey?: unknown
}
expect(retryConfig.agent).toBeUndefined()
expect(retryConfig.password).toBe('password-123')
expect(retryConfig.privateKey).toBeUndefined()
expect(onCredentialRequest).toHaveBeenCalledWith('target-1', 'password', 'example.com')
})
it('retries password auth with the no-agent key config after direct key fallback fails', async () => {
vi.stubEnv('SSH_AUTH_SOCK', '/tmp/agent.sock')
const tempDir = mkdtempSync(join(tmpdir(), 'orca-ssh-key-'))
const keyPath = join(tempDir, 'id_ed25519')
writeFileSync(keyPath, 'test-key')
connectSequence = [
new Error('All configured authentication methods failed'),
new Error('All configured authentication methods failed'),
'ready'
]
const onCredentialRequest = vi.fn(async () => 'password-123')
try {
const conn = new SshConnection(
createTarget({ identityFile: keyPath }),
createCallbacks({ onCredentialRequest })
)
await conn.connect()
expect(clientInstances).toHaveLength(3)
const keyRetryConfig = clientInstances[1].lastConnectConfig as {
agent?: unknown
privateKey?: Buffer
}
const passwordRetryConfig = clientInstances[2].lastConnectConfig as {
agent?: unknown
password?: string
privateKey?: Buffer
}
expect(keyRetryConfig.agent).toBeUndefined()
expect(keyRetryConfig.privateKey).toEqual(Buffer.from('test-key'))
expect(passwordRetryConfig.agent).toBeUndefined()
expect(passwordRetryConfig.privateKey).toEqual(Buffer.from('test-key'))
expect(passwordRetryConfig.password).toBe('password-123')
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
})
it('does not prompt twice when post-agent private key passphrase is cancelled', async () => {
vi.stubEnv('SSH_AUTH_SOCK', '/tmp/agent.sock')
const tempDir = mkdtempSync(join(tmpdir(), 'orca-ssh-key-'))
const keyPath = join(tempDir, 'id_ed25519')
writeFileSync(keyPath, 'test-key')
connectSequence = [
new Error('All configured authentication methods failed'),
new Error('Encrypted private OpenSSH key detected, but no passphrase given')
]
const onCredentialRequest = vi.fn(async () => null)
try {
const conn = new SshConnection(
createTarget({ identityFile: keyPath }),
createCallbacks({ onCredentialRequest })
)
await expect(conn.connect()).rejects.toThrow('Encrypted private OpenSSH key detected')
expect(onCredentialRequest).toHaveBeenCalledTimes(1)
expect(onCredentialRequest).toHaveBeenCalledWith('target-1', 'passphrase', keyPath)
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
})
it('wraps exec commands as a single line that csh/tcsh login shells cannot break', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
const original = "cd '/tmp' && ('/usr/bin/node' -e 'console.log(1)' || echo MISSING)"
await conn.exec(original)
const wrapped = clientInstances[0].lastExecCommand!
// Why: sshd lets the login shell parse this first, so raw newlines let
// csh/tcsh split the command before /bin/sh receives it (issue #8701).
expect(wrapped).not.toContain('\n')
expect(wrapped).toMatch(/^exec \/bin\/sh -c '.*printf %b .*' orca-command /)
expect(wrapped).not.toContain('base64')
})
it('can execute native remote commands without the POSIX shell wrapper', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
await conn.exec('powershell.exe -NoProfile -EncodedCommand AAAA', { wrapCommand: false })
expect(clientInstances[0].lastExecCommand).toBe(
'powershell.exe -NoProfile -EncodedCommand AAAA'
)
})
it('times out when ssh2 never opens an exec channel', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
execBehavior = 'pending'
vi.useFakeTimers()
try {
const outcomePromise = conn.exec('printf ready').catch((error: Error) => error)
await vi.advanceTimersByTimeAsync(30_000)
const outcome = await Promise.race([outcomePromise, Promise.resolve('pending')])
expect(outcome).toMatchObject({
message: 'SSH exec channel timed out',
sshChannelCloseConfirmed: false
})
} finally {
vi.useRealTimers()
}
})
it('closes a late exec callback after the channel-open timeout settles', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
execBehavior = 'pending'
const lateChannel = { close: vi.fn() }
vi.useFakeTimers()
try {
const outcomePromise = conn
.exec('printf ready')
.then(() => 'opened')
.catch((error: Error) => error.message)
await vi.advanceTimersByTimeAsync(30_000)
pendingExecCallback?.(undefined, lateChannel)
await expect(outcomePromise).resolves.toBe('SSH exec channel timed out')
expect(lateChannel.close).toHaveBeenCalledTimes(1)
} finally {
vi.useRealTimers()
}
})
it('retries a session-limit-refused exec open and succeeds on a later attempt', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
const channel = { close: vi.fn() }
const execMock = vi
.fn<(cmd: string, cb: (err: Error | undefined, ch: unknown) => void) => void>()
.mockImplementationOnce((_cmd, cb) => {
cb(
Object.assign(new Error('(SSH) Channel open failure: open failed'), { reason: 2 }),
undefined
)
})
.mockImplementation((_cmd, cb) => cb(undefined, channel))
clientInstances[0].exec = execMock as never
await expect(conn.exec('printf ready')).resolves.toBe(channel)
expect(execMock).toHaveBeenCalledTimes(2)
})
it('surfaces the session-limit error once open retries are exhausted', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
const refusal = Object.assign(new Error('(SSH) Channel open failure: open failed'), {
reason: 2
})
const execMock = vi
.fn<(cmd: string, cb: (err: Error | undefined, ch: unknown) => void) => void>()
.mockImplementation((_cmd, cb) => cb(refusal, undefined))
clientInstances[0].exec = execMock as never
await expect(conn.exec('printf ready')).rejects.toBe(refusal)
expect(execMock).toHaveBeenCalledTimes(4)
})
it('does not retry non-session-limit exec open failures', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
const failure = new Error('Not connected')
const execMock = vi
.fn<(cmd: string, cb: (err: Error | undefined, ch: unknown) => void) => void>()
.mockImplementation((_cmd, cb) => cb(failure, undefined))
clientInstances[0].exec = execMock as never
await expect(conn.exec('printf ready')).rejects.toBe(failure)
expect(execMock).toHaveBeenCalledTimes(1)
})
it('bounds an aborted exec to the close grace when ssh2 never invokes the open callback', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
execBehavior = 'pending'
const controller = new AbortController()
vi.useFakeTimers()
try {
const outcomePromise = conn
.exec('printf ready', { signal: controller.signal })
.catch((error: Error) => error)
controller.abort()
// Why: a hung socket must not pin the aborted caller for the full 30s
// connect timeout — the abort settles at the 5s grace bound instead.
await vi.advanceTimersByTimeAsync(5_000)
await expect(outcomePromise).resolves.toMatchObject({
name: 'AbortError',
sshChannelCloseConfirmed: false
})
} finally {
vi.useRealTimers()
}
})
it('drains an exec channel that opens after the abort grace has settled', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
execBehavior = 'pending'
const controller = new AbortController()
const lateChannel = Object.assign(new EventEmitter(), {
close: vi.fn(),
resume: vi.fn(),
stderr: { resume: vi.fn() }
})
vi.useFakeTimers()
try {
const outcomePromise = conn
.exec('printf ready', { signal: controller.signal })
.catch((error: Error) => error)
controller.abort()
await vi.advanceTimersByTimeAsync(5_000)
const outcome = await outcomePromise
expect(outcome).toMatchObject({
name: 'AbortError',
sshChannelCloseConfirmed: false
})
pendingExecCallback?.(undefined, lateChannel)
expect(lateChannel.resume).toHaveBeenCalledTimes(1)
expect(lateChannel.stderr.resume).toHaveBeenCalledTimes(1)
expect(lateChannel.close).toHaveBeenCalledTimes(1)
lateChannel.emit('close')
expect(outcome).toMatchObject({ sshChannelCloseConfirmed: true })
} finally {
vi.useRealTimers()
}
})
it('rejects without waiting out the backoff when aborted during a session-limit retry delay', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
const controller = new AbortController()
const refusal = Object.assign(new Error('(SSH) Channel open failure: open failed'), {
reason: 2
})
const execMock = vi
.fn<(cmd: string, cb: (err: Error | undefined, ch: unknown) => void) => void>()
.mockImplementation((_cmd, cb) => cb(refusal, undefined))
clientInstances[0].exec = execMock as never
vi.useFakeTimers()
try {
const outcomePromise = conn
.exec('printf ready', { signal: controller.signal })
.then(() => 'opened')
.catch((error: Error) => error.name)
// Flush microtasks so the first refused attempt lands in the backoff.
await vi.advanceTimersByTimeAsync(0)
controller.abort()
// No timer advance: the abort alone must release the backoff delay.
await expect(outcomePromise).resolves.toBe('AbortError')
expect(execMock).toHaveBeenCalledTimes(1)
} finally {
vi.useRealTimers()
}
})
it('settles an abort during channel open only after the late channel closes', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
execBehavior = 'pending'
const controller = new AbortController()
const lateChannel = Object.assign(new EventEmitter(), {
close: vi.fn(),
resume: vi.fn(),
stderr: Object.assign(new EventEmitter(), { resume: vi.fn() })
})
const outcomePromise = conn
.exec('printf ready', { signal: controller.signal })
.then(() => 'opened')
.catch((error: Error) => error.name)
await Promise.resolve()
controller.abort()
pendingExecCallback?.(undefined, lateChannel)
// Why: the sshd session slot is freed only when the channel finishes
// closing — settling before 'close' lets the next open race the close.
const early = await Promise.race([outcomePromise, Promise.resolve('pending')])
expect(early).toBe('pending')
expect(lateChannel.close).toHaveBeenCalledTimes(1)
expect(lateChannel.resume).toHaveBeenCalled()
expect(() => lateChannel.emit('error', new Error('late channel teardown'))).not.toThrow()
expect(() => lateChannel.stderr.emit('error', new Error('late stderr teardown'))).not.toThrow()
lateChannel.emit('close')
await expect(outcomePromise).resolves.toBe('AbortError')
})
it('removes the late-channel close listener when abort grace expires', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
vi.useFakeTimers()
try {
sftpBehavior = 'pending'
const controller = new AbortController()
const lateSftp = Object.assign(new EventEmitter(), { end: vi.fn() })
const outcomePromise = conn
.sftp(controller.signal)
.then(() => 'opened')
.catch((error: Error) => error.name)
await vi.advanceTimersByTimeAsync(0)
controller.abort()
pendingSftpCallback?.(undefined, lateSftp)
expect(lateSftp.listenerCount('close')).toBe(1)
expect(() => lateSftp.emit('error', new Error('late SFTP teardown'))).not.toThrow()
await vi.advanceTimersByTimeAsync(5_000)
await expect(outcomePromise).resolves.toBe('AbortError')
expect(lateSftp.listenerCount('close')).toBe(0)
} finally {
vi.useRealTimers()
}
})
it('times out when ssh2 never opens an SFTP channel', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
sftpBehavior = 'pending'
vi.useFakeTimers()
try {
const outcomePromise = conn.sftp().catch((error: Error) => error)
await vi.advanceTimersByTimeAsync(30_000)
const outcome = await Promise.race([outcomePromise, Promise.resolve('pending')])
expect(outcome).toMatchObject({ message: 'SSH SFTP channel timed out' })
expect(outcome).not.toHaveProperty('sshChannelCloseConfirmed')
} finally {
vi.useRealTimers()
}
})
it('ends a late SFTP callback after the channel-open timeout settles', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
sftpBehavior = 'pending'
const lateSftp = { end: vi.fn() }
vi.useFakeTimers()
try {
const outcomePromise = conn
.sftp()
.then(() => 'opened')
.catch((error: Error) => error.message)
await vi.advanceTimersByTimeAsync(30_000)
pendingSftpCallback?.(undefined, lateSftp)
await expect(outcomePromise).resolves.toBe('SSH SFTP channel timed out')
expect(lateSftp.end).toHaveBeenCalledTimes(1)
} finally {
vi.useRealTimers()
}
})
it('cancels a pending SFTP channel open and ends the late channel', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
sftpBehavior = 'pending'
const controller = new AbortController()
const lateSftp = { end: vi.fn() }
const outcomePromise = conn
.sftp({ signal: controller.signal })
.then(() => 'opened')
.catch((error: Error) => error.name)
await Promise.resolve()
controller.abort()
pendingSftpCallback?.(undefined, lateSftp)
await expect(outcomePromise).resolves.toBe('AbortError')
expect(lateSftp.end).toHaveBeenCalledTimes(1)
})
it('removes the late SFTP close listener when the bounded grace expires', async () => {
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
sftpBehavior = 'pending'
const controller = new AbortController()
const lateSftp = Object.assign(new EventEmitter(), { end: vi.fn() })
vi.useFakeTimers()
try {
const outcomePromise = conn
.sftp({ signal: controller.signal })
.then(() => 'opened')
.catch((error: Error) => error.name)
await Promise.resolve()
controller.abort()
pendingSftpCallback?.(undefined, lateSftp)
expect(lateSftp.listenerCount('close')).toBe(1)
await vi.advanceTimersByTimeAsync(5_000)
await expect(outcomePromise).resolves.toBe('AbortError')
expect(lateSftp.listenerCount('close')).toBe(0)
expect(lateSftp.end).toHaveBeenCalledTimes(1)
} finally {
vi.useRealTimers()
}
})
it('uses system SSH transport when ProxyUseFdpass is resolved by OpenSSH', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
expect(conn.getState().supportsFolderDownload).toBe(false)
expect(clientInstances).toHaveLength(0)
expect(spawnSystemSshCommandMock).toHaveBeenCalledWith(
expect.objectContaining({ configHost: 'fdpass-host' }),
'echo ORCA-SYSTEM-SSH-OK',
{
wrapCommand: false,
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
}
)
})
it('allows concurrent exec commands for system SSH with an Orca ControlMaster socket', async () => {
getOrcaControlSocketPathMock.mockReturnValue('/tmp/orca-ssh-501/live-socket')
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
await conn.connect()
expect(conn.usesSystemSshTransport()).toBe(true)
expect(conn.canRunConcurrentExecCommands()).toBe(true)
})
it('keeps concurrent exec commands disabled for system SSH without a reusable socket', async () => {
getOrcaControlSocketPathMock.mockReturnValue(null)
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const conn = new SshConnection(
createTarget({ configHost: 'fdpass-host', systemSshConnectionReuse: false }),
createCallbacks()
)
await conn.connect()
expect(conn.usesSystemSshTransport()).toBe(true)
expect(conn.canRunConcurrentExecCommands()).toBe(false)
})
it('accepts GitHub restricted-shell SSH probes with resolved user fallback', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
createResolvedConfig({ hostname: 'github.com', user: 'git' })
)
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
)
const conn = new SshConnection(
createTarget({
configHost: 'github.com',
host: 'github.com',
username: undefined
}),
createCallbacks()
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
})
it('uses fresh OpenSSH user for imported GitHub targets', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
createResolvedConfig({ hostname: 'github.com', user: 'git' })
)
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
)
const conn = new SshConnection(
createTarget({
source: 'ssh-config',
configHost: 'github.com',
host: 'github.com',
username: 'stale-user'
}),
createCallbacks()
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
})
it('accepts GitHub restricted-shell SSH probes with resolved host and target username', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
createResolvedConfig({ hostname: 'github.com', user: undefined })
)
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
)
const conn = new SshConnection(
createTarget({
configHost: 'github.com',
host: 'github.com',
username: 'git'
}),
createCallbacks()
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
})
it('accepts ssh.github.com restricted-shell SSH probes', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
createResolvedConfig({ hostname: 'ssh.github.com', user: 'git' })
)
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
)
const conn = new SshConnection(
createTarget({
configHost: 'ssh.github.com',
host: 'ssh.github.com',
username: 'git'
}),
createCallbacks()
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
})
it('accepts GitHub restricted-shell SSH probes with the real git:// advisory transcript', async () => {
// Real 4-line stderr GitHub returns for an invalid command (issue #6988).
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
createResolvedConfig({ hostname: 'github.com', user: 'git' })
)
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(
1,
'Invalid command: echo ORCA-SYSTEM-SSH-OK\n' +
' You appear to be using ssh to clone a git:// URL.\n' +
' Make sure your core.gitProxy config option and the\n' +
' GIT_PROXY_COMMAND environment variable are NOT set.'
)
)
const conn = new SshConnection(
createTarget({
configHost: 'github.com',
host: 'github.com',
username: 'git'
}),
createCallbacks()
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
})
it('accepts GitHub restricted-shell SSH probes when OpenSSH config resolution fails', async () => {
vi.stubEnv('ORCA_SSH_FORCE_SYSTEM_TRANSPORT', '1')
vi.mocked(resolveWithSshG).mockRejectedValueOnce(new Error('ssh -G failed'))
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
)
const conn = new SshConnection(
createTarget({
configHost: 'github.com',
host: 'github.com',
username: 'git'
}),
createCallbacks()
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
expect(conn.getSystemSshResolvedConfig()).toBeNull()
})
it('rejects non-GitHub SSH probes with GitHub invalid-command text', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
createResolvedConfig({ hostname: 'gitlab.com', user: 'git' })
)
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
)
const conn = new SshConnection(
createTarget({
configHost: 'github.com',
host: 'github.com',
username: 'git'
}),
createCallbacks()
)
await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)')
expect(conn.usesSystemSshTransport()).toBe(false)
})
it('accepts GitHub restricted-shell SSH probes when target username overrides resolved user', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
createResolvedConfig({ hostname: 'github.com', user: 'deploy' })
)
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
)
const conn = new SshConnection(
createTarget({
configHost: 'github.com',
host: 'github.com',
username: 'git'
}),
createCallbacks()
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
})
it('rejects GitHub restricted-shell SSH probes when target username overrides resolved git user', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
createResolvedConfig({ hostname: 'github.com', user: 'git' })
)
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
)
const conn = new SshConnection(
createTarget({
configHost: 'github.com',
host: 'github.com',
username: 'deploy'
}),
createCallbacks()
)
await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)')
expect(conn.usesSystemSshTransport()).toBe(false)
})
it('rejects GitHub restricted-shell SSH probes with extra stderr text', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
createResolvedConfig({ hostname: 'github.com', user: 'git' })
)
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(
1,
'remote: rejected\nInvalid command: echo ORCA-SYSTEM-SSH-OK\ntry again'
)
)
const conn = new SshConnection(
createTarget({
configHost: 'github.com',
host: 'github.com',
username: 'git'
}),
createCallbacks()
)
await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)')
expect(conn.usesSystemSshTransport()).toBe(false)
})
it('rejects GitHub restricted-shell SSH probes for non-git users', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(
createResolvedConfig({ hostname: 'github.com', user: 'deploy' })
)
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(1, 'Invalid command: echo ORCA-SYSTEM-SSH-OK')
)
const conn = new SshConnection(
createTarget({
configHost: 'github.com',
host: 'github.com',
username: 'deploy'
}),
createCallbacks()
)
await expect(conn.connect()).rejects.toThrow('System SSH probe failed (exit 1)')
expect(conn.usesSystemSshTransport()).toBe(false)
})
it('retries a failed system SSH probe without ControlMaster and disables mux for the session', async () => {
getOrcaControlSocketPathMock.mockImplementation(
(_target: SshTarget, options?: { disableControlMaster?: boolean }) =>
options?.disableControlMaster ? null : '/tmp/orca-ssh-501/stale-socket'
)
spawnSystemSshCommandMock
.mockImplementationOnce(() => createFailingSystemCommandChannel(255, 'mux client failed'))
.mockImplementation(() => createSystemCommandChannel())
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
await conn.connect()
await conn.exec('echo after-connect')
await conn.writeFile('/tmp/after-connect', 'contents')
expect(removeControlSocketPathMock).toHaveBeenCalledWith('/tmp/orca-ssh-501/stale-socket')
expect(spawnSystemSshCommandMock).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ configHost: 'fdpass-host' }),
'echo ORCA-SYSTEM-SSH-OK',
expect.objectContaining({
wrapCommand: false,
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
})
)
expect(spawnSystemSshCommandMock).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ configHost: 'fdpass-host' }),
'echo ORCA-SYSTEM-SSH-OK',
expect.objectContaining({
disableControlMaster: true,
wrapCommand: false,
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
})
)
expect(spawnSystemSshCommandMock).toHaveBeenNthCalledWith(
3,
expect.objectContaining({ configHost: 'fdpass-host' }),
'echo after-connect',
expect.objectContaining({
disableControlMaster: true,
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
})
)
expect(writeFileViaSystemSsh).toHaveBeenCalledWith(
expect.objectContaining({ configHost: 'fdpass-host' }),
'/tmp/after-connect',
'contents',
expect.objectContaining({
disableControlMaster: true,
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
})
)
expect(conn.canRunConcurrentExecCommands()).toBe(false)
})
it('retries a generic system SSH probe timeout without ControlMaster', async () => {
vi.useFakeTimers()
try {
getOrcaControlSocketPathMock.mockImplementation(
(_target: SshTarget, options?: { disableControlMaster?: boolean }) =>
options?.disableControlMaster ? null : '/tmp/orca-ssh-501/stale-socket'
)
spawnSystemSshCommandMock
.mockImplementationOnce(() => createHangingSystemCommandChannel())
.mockImplementation(() => createSystemCommandChannel())
vi.mocked(resolveWithSshG).mockResolvedValue(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
const settled = conn.connect()
await vi.advanceTimersByTimeAsync(CONNECT_TIMEOUT_MS)
await expect(settled).resolves.toBeUndefined()
expect(spawnSystemSshCommandMock).toHaveBeenCalledTimes(2)
expect(removeControlSocketPathMock).toHaveBeenCalledWith('/tmp/orca-ssh-501/stale-socket')
expect(spawnSystemSshCommandMock).toHaveBeenNthCalledWith(
2,
expect.anything(),
'echo ORCA-SYSTEM-SSH-OK',
expect.objectContaining({ disableControlMaster: true })
)
} finally {
vi.useRealTimers()
}
})
it('skips a second probe for a definite host failure', async () => {
getOrcaControlSocketPathMock.mockReturnValue('/tmp/orca-ssh-501/stale-socket')
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(255, 'ssh: connect to host box port 22: No route to host')
)
vi.mocked(resolveWithSshG).mockResolvedValue(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
await expect(conn.connect()).rejects.toThrow('No route to host')
expect(spawnSystemSshCommandMock).toHaveBeenCalledTimes(1)
expect(removeControlSocketPathMock).toHaveBeenCalledWith('/tmp/orca-ssh-501/stale-socket')
})
it.each([
'Permission denied (publickey,password).',
'Encrypted private key detected, but no passphrase given'
])('skips a second probe for terminal credential failures: %s', async (stderr) => {
getOrcaControlSocketPathMock.mockReturnValue('/tmp/orca-ssh-501/stale-socket')
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(255, stderr)
)
vi.mocked(resolveWithSshG).mockResolvedValue(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
await expect(conn.connect()).rejects.toThrow(stderr)
expect(spawnSystemSshCommandMock).toHaveBeenCalledTimes(1)
expect(removeControlSocketPathMock).toHaveBeenCalledWith('/tmp/orca-ssh-501/stale-socket')
})
it('retries a generic direct system SSH timeout without ControlMaster', async () => {
vi.useFakeTimers()
try {
getOrcaControlSocketPathMock.mockImplementation(
(_target: SshTarget, options?: { disableControlMaster?: boolean }) =>
options?.disableControlMaster ? null : '/tmp/orca-ssh-501/stale-socket'
)
spawnSystemSshMock
.mockImplementationOnce(() => createPendingSystemSshProcess())
.mockImplementation(() => createSystemSshProcess())
vi.mocked(resolveWithSshG).mockResolvedValue(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
const settled = conn.connectViaSystemSsh()
await vi.advanceTimersByTimeAsync(CONNECT_TIMEOUT_MS)
await expect(settled).resolves.toBeDefined()
expect(spawnSystemSshMock).toHaveBeenCalledTimes(2)
expect(removeControlSocketPathMock).toHaveBeenCalledWith('/tmp/orca-ssh-501/stale-socket')
expect(spawnSystemSshMock).toHaveBeenNthCalledWith(
2,
expect.anything(),
expect.objectContaining({ disableControlMaster: true })
)
} finally {
vi.useRealTimers()
}
})
it('uses system SSH transport for ProxyCommand targets before ssh2 auth', async () => {
const conn = new SshConnection(
createTarget({ proxyCommand: 'ssh -W %h:%p bastion.example.com' }),
createCallbacks()
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
expect(clientInstances).toHaveLength(0)
expect(spawnSystemSshCommandMock).toHaveBeenCalledWith(
expect.objectContaining({ proxyCommand: 'ssh -W %h:%p bastion.example.com' }),
'echo ORCA-SYSTEM-SSH-OK',
{ wrapCommand: false }
)
})
it('uses system SSH before ssh2 parses a security-key private key', async () => {
findSystemSshMock.mockReturnValue('/usr/bin/ssh')
const directory = mkdtempSync(join(tmpdir(), 'orca-security-key-connect-'))
const keyPath = join(directory, 'id_ed25519_sk')
writeFileSync(
keyPath,
createOpenSshPrivateKeyFixture(['sk-ssh-ed25519@openssh.com'], { encrypted: true })
)
const conn = new SshConnection(createTarget({ identityFile: keyPath }), createCallbacks())
try {
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
expect(clientInstances).toHaveLength(0)
expect(spawnSystemSshCommandMock).toHaveBeenCalledTimes(1)
} finally {
rmSync(directory, { recursive: true })
}
})
it('uses system SSH for an agent-backed security-key public identity', async () => {
findSystemSshMock.mockReturnValue('/usr/bin/ssh')
const directory = mkdtempSync(join(tmpdir(), 'orca-security-key-agent-connect-'))
const identityPath = join(directory, 'id_ed25519_sk')
writeFileSync(
`${identityPath}.pub`,
createOpenSshPublicKeyFixture('sk-ssh-ed25519@openssh.com')
)
const conn = new SshConnection(createTarget({ identityFile: identityPath }), createCallbacks())
try {
await conn.connect()
expect(conn.usesSystemSshTransport()).toBe(true)
expect(clientInstances).toHaveLength(0)
expect(spawnSystemSshCommandMock).toHaveBeenCalledTimes(1)
} finally {
rmSync(directory, { recursive: true })
}
})
it('falls back to system SSH when ssh2 hits a local network policy reachability error', async () => {
connectBehavior = 'error'
connectErrorMessage = 'connect EHOSTUNREACH 192.168.0.210:22 - Local (192.168.0.2:52112)'
connectErrorCode = 'EHOSTUNREACH'
const conn = new SshConnection(
createTarget({ host: '192.168.0.210', label: 'LAN Linux', username: 'hydra' }),
createCallbacks()
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
expect(clientInstances).toHaveLength(1)
expect(spawnSystemSshCommandMock).toHaveBeenCalledWith(
expect.objectContaining({ host: '192.168.0.210' }),
'echo ORCA-SYSTEM-SSH-OK',
{ wrapCommand: false }
)
})
it('keeps the original ssh2 reachability error when the system SSH probe fails', async () => {
connectBehavior = 'error'
connectErrorMessage = 'connect EHOSTUNREACH 192.168.0.210:22 - Local (192.168.0.2:52112)'
connectErrorCode = 'EHOSTUNREACH'
spawnSystemSshCommandMock.mockImplementation(() => {
throw new Error('No system ssh binary found. Install OpenSSH to use system SSH transport.')
})
const conn = new SshConnection(
createTarget({ host: '192.168.0.210', label: 'LAN Linux', username: 'hydra' }),
createCallbacks()
)
const privateConn = conn as unknown as {
attemptConnect: () => Promise<void>
}
await expect(privateConn.attemptConnect()).rejects.toThrow(
'connect EHOSTUNREACH 192.168.0.210:22'
)
expect(conn.usesSystemSshTransport()).toBe(false)
})
it('tries system SSH first for targets that explicitly request GSSAPI authentication', async () => {
const conn = new SshConnection(createTarget({ gssapiAuthentication: true }), createCallbacks())
await conn.connect()
await conn.exec('echo after-connect')
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
expect(clientInstances).toHaveLength(0)
expect(spawnSystemSshCommandMock).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ gssapiAuthentication: true }),
'echo ORCA-SYSTEM-SSH-OK',
{
gssapiOnly: true,
wrapCommand: false
}
)
expect(spawnSystemSshCommandMock).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ gssapiAuthentication: true }),
'echo after-connect',
{ gssapiOnly: true }
)
})
it('tries GSSAPI first for a manually owned config-picker target', async () => {
vi.mocked(resolveWithSshG).mockResolvedValue(
createResolvedConfig({ proxyUseFdpass: false, gssapiAuthentication: true })
)
const conn = new SshConnection(
createTarget({
source: 'manual',
configHost: 'prod',
host: 'prod.internal',
gssapiAuthentication: true
}),
createCallbacks()
)
await conn.connect()
expect(spawnSystemSshCommandMock).toHaveBeenCalledWith(
expect.objectContaining({ source: 'manual', configHost: 'prod' }),
'echo ORCA-SYSTEM-SSH-OK',
expect.objectContaining({ gssapiOnly: true, wrapCommand: false })
)
expect(clientInstances).toHaveLength(0)
})
it('falls back to ssh2 when the GSSAPI-first system SSH attempt fails', async () => {
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(255, 'Permission denied (gssapi-with-mic,publickey)')
)
const conn = new SshConnection(createTarget({ gssapiAuthentication: true }), createCallbacks())
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(false)
expect(clientInstances).toHaveLength(1)
// Why: proves the GSSAPI-first probe actually ran before the ssh2 fallback,
// so the test fails if the proactive block is removed.
expect(spawnSystemSshCommandMock).toHaveBeenCalledWith(
expect.objectContaining({ gssapiAuthentication: true }),
'echo ORCA-SYSTEM-SSH-OK',
{
gssapiOnly: true,
wrapCommand: false
}
)
})
it('ignores stale imported GSSAPI when fresh OpenSSH config disables it', async () => {
vi.mocked(resolveWithSshG).mockResolvedValue(
createResolvedConfig({ proxyUseFdpass: false, gssapiAuthentication: false })
)
const conn = new SshConnection(
createTarget({
source: 'ssh-config',
configHost: 'krb-host',
gssapiAuthentication: true
}),
createCallbacks()
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(false)
expect(spawnSystemSshCommandMock).not.toHaveBeenCalled()
})
it('falls back to system SSH after an ssh2 auth failure when resolved config enables GSSAPI', async () => {
connectBehavior = 'error'
connectErrorMessage = 'All configured authentication methods failed'
vi.mocked(resolveWithSshG).mockResolvedValue(
createResolvedConfig({ proxyUseFdpass: false, gssapiAuthentication: true })
)
const onCredentialRequest = vi.fn(async () => 'password-123')
const conn = new SshConnection(
createTarget({ configHost: 'krb-host' }),
createCallbacks({ onCredentialRequest })
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
expect(conn.getHostKeyFingerprint()).toBeUndefined()
expect(onCredentialRequest).not.toHaveBeenCalled()
})
it('connects through the GSSAPI fallback without credential callbacks (headless)', async () => {
connectBehavior = 'error'
connectErrorMessage = 'All configured authentication methods failed'
vi.mocked(resolveWithSshG).mockResolvedValue(
createResolvedConfig({ proxyUseFdpass: false, gssapiAuthentication: true })
)
const conn = new SshConnection(createTarget({ configHost: 'krb-host' }), createCallbacks())
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
})
it('keeps prompting for credentials when the GSSAPI fallback probe fails', async () => {
// Why: identityAgent 'none' makes resolveAgentSocket return undefined on
// every platform (SSH_AUTH_SOCK='' alone leaves the Windows agent pipe), so
// ssh2's first connect carries any default key directly and the agent
// fallback retry never consumes the second connectSequence entry —
// deterministic on dev machines with both ~/.ssh/id_* and a live agent.
vi.stubEnv('SSH_AUTH_SOCK', '')
connectSequence = [new Error('All configured authentication methods failed'), 'ready']
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(255, 'Permission denied (gssapi-with-mic,password)')
)
vi.mocked(resolveWithSshG).mockResolvedValue(
createResolvedConfig({
proxyUseFdpass: false,
gssapiAuthentication: true,
identityAgent: 'none'
})
)
const onCredentialRequest = vi.fn(async () => 'password-123')
const conn = new SshConnection(
createTarget({ configHost: 'krb-host' }),
createCallbacks({ onCredentialRequest })
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(false)
// Why: proves the reactive GSSAPI probe actually ran before prompting, so
// the test fails if the reactive fallback block is removed.
expect(spawnSystemSshCommandMock).toHaveBeenCalledWith(
expect.objectContaining({ configHost: 'krb-host' }),
'echo ORCA-SYSTEM-SSH-OK',
expect.objectContaining({ wrapCommand: false })
)
expect(onCredentialRequest).toHaveBeenCalledWith('target-1', 'password', expect.any(String))
})
it('tries the GSSAPI probe before prompting for an encrypted key passphrase', async () => {
// Why: a valid Kerberos ticket should connect silently before the user is
// ever asked for the key passphrase. Agent auth fails, the explicit-key
// retry fails with a passphrase error, and resolved GSSAPI is on — so the
// reactive probe must run before onCredentialRequest.
vi.stubEnv('SSH_AUTH_SOCK', '/tmp/agent.sock')
const tempDir = mkdtempSync(join(tmpdir(), 'orca-ssh-key-'))
const keyPath = join(tempDir, 'id_ed25519')
writeFileSync(keyPath, 'test-key')
connectSequence = [
new Error('All configured authentication methods failed'),
new Error('Encrypted private OpenSSH key detected, but no passphrase given')
]
vi.mocked(resolveWithSshG).mockResolvedValue(
createResolvedConfig({ proxyUseFdpass: false, gssapiAuthentication: true })
)
const order: string[] = []
spawnSystemSshCommandMock.mockImplementation(() => {
order.push('probe')
return createSystemCommandChannel()
})
const onCredentialRequest = vi.fn(async () => {
order.push('prompt')
return 'secret'
})
try {
const conn = new SshConnection(
createTarget({ configHost: 'krb-host', identityFile: keyPath }),
createCallbacks({ onCredentialRequest })
)
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
// Why: the probe must precede any passphrase prompt (which here never runs).
expect(order[0]).toBe('probe')
expect(onCredentialRequest).not.toHaveBeenCalled()
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
})
it('does not try system SSH for auth failures when resolved config leaves GSSAPI off', async () => {
connectBehavior = 'error'
connectErrorMessage = 'All configured authentication methods failed'
vi.mocked(resolveWithSshG).mockResolvedValue(createResolvedConfig({ proxyUseFdpass: false }))
const conn = new SshConnection(createTarget({ configHost: 'plain-host' }), createCallbacks())
await expect(conn.connect()).rejects.toThrow('All configured authentication methods failed')
expect(conn.getState().status).toBe('auth-failed')
expect(spawnSystemSshCommandMock).not.toHaveBeenCalled()
})
it('publishes auth-failed when OpenSSH denies reconnect credentials', async () => {
vi.mocked(resolveWithSshG).mockResolvedValue(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
await conn.connect()
spawnSystemSshCommandMock.mockImplementation(() =>
createFailingSystemCommandChannel(255, 'Permission denied (publickey,password).')
)
await conn.reconnect()
expect(conn.getState().status).toBe('auth-failed')
})
it('clears system SSH transport when the GSSAPI-first probe throws synchronously', async () => {
// Why: no system ssh binary makes spawnSystemSshCommand throw before the
// probe's try/catch, so the ssh2 fall-through must still reset the flag —
// otherwise exec/sftp keep routing through the unusable system transport.
spawnSystemSshCommandMock.mockImplementation(() => {
throw new Error('No system ssh binary found. Install OpenSSH.')
})
connectSequence = ['ready']
const conn = new SshConnection(createTarget({ gssapiAuthentication: true }), createCallbacks())
await conn.connect()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(false)
expect(clientInstances).toHaveLength(1)
})
it('keeps disconnected state when a disconnect cancels the reactive GSSAPI probe', async () => {
connectBehavior = 'error'
connectErrorMessage = 'All configured authentication methods failed'
vi.mocked(resolveWithSshG).mockResolvedValue(
createResolvedConfig({ proxyUseFdpass: false, gssapiAuthentication: true })
)
// Why: a probe channel that stays open until close() leaves the reactive
// fallback pending, so we can disconnect mid-probe; disconnect() then calls
// close() (bumping the generation first), which settles the probe as a
// cancellation rather than a probe failure.
let pendingChannel: ReturnType<typeof createSystemCommandChannel> | null = null
spawnSystemSshCommandMock.mockImplementation(() => {
const channel = new EventEmitter() as ReturnType<typeof createSystemCommandChannel>
channel.stdin = { end: vi.fn(), write: vi.fn() }
channel.stderr = new EventEmitter()
channel.close = vi.fn(() => channel.emit('close', null))
pendingChannel = channel
return channel
})
const onStateChange = vi.fn()
const conn = new SshConnection(
createTarget({ configHost: 'krb-host' }),
createCallbacks({ onStateChange })
)
const connectPromise = conn.connect()
// Wait until the reactive probe has spawned its (never-closing) channel.
await vi.waitFor(() => expect(pendingChannel).not.toBeNull())
await conn.disconnect()
await connectPromise.catch(() => {})
expect(conn.getState().status).toBe('disconnected')
const statuses = onStateChange.mock.calls.map((call) => call[1].status)
expect(statuses).not.toContain('auth-failed')
expect(statuses).not.toContain('error')
})
it('passes the detected host platform to system SSH file operations', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
const hostPlatform = getRemoteHostPlatform('win32-x64')
await conn.connect()
await conn.uploadDirectory('/tmp/local-relay', 'C:/Users/me/.orca-remote/relay', {
hostPlatform
})
await conn.writeFile('C:/Users/me/.orca-remote/relay/.version', '0.1.0', {
hostPlatform
})
await conn.writeBuffer('C:/Users/me/.orca-remote/relay/logo.png', Buffer.from('png'), {
hostPlatform,
exclusive: true
})
await conn.downloadFile('C:/Users/me/.orca-remote/relay/logo.png', '/tmp/logo.png', {
hostPlatform
})
const uploadSession = await conn.openFileUploadSession({ hostPlatform })
await uploadSession.uploadFile('/tmp/logo.png', 'C:/Users/me/project/logo.png', {
exclusive: true
})
uploadSession.close()
expect(uploadDirectoryViaSystemSsh).toHaveBeenCalledWith(
expect.objectContaining({ configHost: 'fdpass-host' }),
'/tmp/local-relay',
'C:/Users/me/.orca-remote/relay',
expect.objectContaining({
hostPlatform,
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
})
)
expect(writeFileViaSystemSsh).toHaveBeenCalledWith(
expect.objectContaining({ configHost: 'fdpass-host' }),
'C:/Users/me/.orca-remote/relay/.version',
'0.1.0',
expect.objectContaining({
hostPlatform,
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
})
)
expect(writeBufferViaSystemSsh).toHaveBeenCalledWith(
expect.objectContaining({ configHost: 'fdpass-host' }),
'C:/Users/me/.orca-remote/relay/logo.png',
Buffer.from('png'),
expect.objectContaining({
hostPlatform,
exclusive: true,
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
})
)
expect(downloadFileViaSystemSsh).toHaveBeenCalledWith(
expect.objectContaining({ configHost: 'fdpass-host' }),
'C:/Users/me/.orca-remote/relay/logo.png',
'/tmp/logo.png',
expect.objectContaining({
hostPlatform,
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
})
)
expect(uploadFileViaSystemSsh).toHaveBeenCalledWith(
expect.objectContaining({ configHost: 'fdpass-host' }),
'/tmp/logo.png',
'C:/Users/me/project/logo.png',
expect.objectContaining({
hostPlatform,
exclusive: true,
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
})
)
})
it('composes a caller abort into system SSH relay uploads', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
const controller = new AbortController()
let transferSignal: AbortSignal | undefined
vi.mocked(uploadDirectoryViaSystemSsh).mockImplementationOnce(
(_target, _localDir, _remoteDir, options) => {
transferSignal = options?.signal
return new Promise((_resolve, reject) => {
transferSignal?.addEventListener('abort', () => reject(transferSignal?.reason), {
once: true
})
})
}
)
await conn.connect()
const upload = conn.uploadDirectory('/tmp/local-relay', '/remote/relay', {
signal: controller.signal
})
await vi.waitFor(() => expect(transferSignal).toBeDefined())
controller.abort()
await expect(upload).rejects.toMatchObject({ name: 'AbortError' })
expect(transferSignal?.aborted).toBe(true)
})
it('keeps connection disconnect cancellation linked to caller-scoped relay writes', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
const controller = new AbortController()
let transferSignal: AbortSignal | undefined
vi.mocked(writeFileViaSystemSsh).mockImplementationOnce(
(_target, _remotePath, _contents, options) => {
transferSignal = options?.signal
return new Promise((_resolve, reject) => {
transferSignal?.addEventListener('abort', () => reject(transferSignal?.reason), {
once: true
})
})
}
)
await conn.connect()
const write = conn.writeFile('/remote/relay/.version', '0.1.0', {
signal: controller.signal
})
await vi.waitFor(() => expect(transferSignal).toBeDefined())
await conn.disconnect()
await expect(write).rejects.toMatchObject({ name: 'AbortError' })
expect(controller.signal.aborted).toBe(false)
expect(transferSignal?.aborted).toBe(true)
})
it('keeps an upload session cancelled after the connection disconnects', async () => {
const conn = new SshConnection(
createTarget({ proxyCommand: 'ssh -W %h:%p bastion.example.com' }),
createCallbacks()
)
vi.mocked(uploadFileViaSystemSsh).mockImplementation(
async (_target, _localPath, _remotePath, options) => {
if (options?.signal?.aborted) {
const error = new Error('System SSH operation was cancelled')
error.name = 'AbortError'
throw error
}
}
)
await conn.connect()
const uploadSession = await conn.openFileUploadSession()
await conn.disconnect()
await expect(
uploadSession.uploadFile('/tmp/late.txt', '/remote/late.txt')
).rejects.toMatchObject({ name: 'AbortError' })
expect(uploadFileViaSystemSsh).toHaveBeenCalledWith(
expect.anything(),
'/tmp/late.txt',
'/remote/late.txt',
expect.objectContaining({ signal: expect.objectContaining({ aborted: true }) })
)
})
it('removes system SSH probe listeners after timeout', async () => {
vi.useFakeTimers()
const channel = new EventEmitter() as ReturnType<typeof createSystemCommandChannel>
channel.stdin = { end: vi.fn(), write: vi.fn() }
channel.stderr = new EventEmitter()
channel.close = vi.fn()
spawnSystemSshCommandMock.mockReturnValueOnce(channel)
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
try {
const connect = expect(conn.connect()).rejects.toThrow('System SSH connection timed out')
await vi.advanceTimersByTimeAsync(30_000)
await connect
expect(channel.close).toHaveBeenCalled()
expect(channel.listenerCount('data')).toBe(0)
expect(channel.listenerCount('error')).toBe(1)
expect(channel.listenerCount('close')).toBe(1)
expect(channel.stderr.listenerCount('data')).toBe(0)
expect(
(conn as unknown as { systemCommandChannels: Set<unknown> }).systemCommandChannels.size
).toBe(0)
} finally {
vi.useRealTimers()
}
})
it('passes resolved OpenSSH config to direct system SSH connections', async () => {
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
await conn.connectViaSystemSsh()
expect(conn.getState().status).toBe('connected')
expect(conn.usesSystemSshTransport()).toBe(true)
expect(spawnSystemSshMock).toHaveBeenCalledWith(
expect.objectContaining({ configHost: 'fdpass-host' }),
{
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
}
)
})
it('retries direct system SSH connections without ControlMaster after mux startup failure', async () => {
getOrcaControlSocketPathMock.mockImplementation(
(_target: SshTarget, options?: { disableControlMaster?: boolean }) =>
options?.disableControlMaster ? null : '/tmp/orca-ssh-501/stale-socket'
)
spawnSystemSshMock
.mockReturnValueOnce(createFailingSystemSshProcess(255))
.mockImplementation(() => createSystemSshProcess())
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
await conn.connectViaSystemSsh()
expect(removeControlSocketPathMock).toHaveBeenCalledWith('/tmp/orca-ssh-501/stale-socket')
expect(spawnSystemSshMock).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ configHost: 'fdpass-host' }),
{
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
}
)
expect(spawnSystemSshMock).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ configHost: 'fdpass-host' }),
{
disableControlMaster: true,
resolvedConfig: expect.objectContaining({ proxyUseFdpass: true })
}
)
expect(conn.canRunConcurrentExecCommands()).toBe(false)
})
it.each([
'Permission denied (publickey,password).',
'Encrypted private key detected, but no passphrase given'
])('skips a direct retry for terminal credential failures: %s', async (message) => {
getOrcaControlSocketPathMock.mockReturnValue('/tmp/orca-ssh-501/stale-socket')
spawnSystemSshMock.mockImplementation(() => {
throw new Error(message)
})
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
await expect(conn.connectViaSystemSsh()).rejects.toThrow(message)
expect(spawnSystemSshMock).toHaveBeenCalledTimes(1)
expect(removeControlSocketPathMock).toHaveBeenCalledWith('/tmp/orca-ssh-501/stale-socket')
})
it('kills delayed direct system SSH startup on disconnect and ignores late stdout', async () => {
const proc = createPendingSystemSshProcess()
spawnSystemSshMock.mockReturnValueOnce(proc)
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const callbacks = createCallbacks()
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), callbacks)
const connectResult = conn.connectViaSystemSsh().catch((err: Error) => err)
for (let i = 0; i < 5 && spawnSystemSshMock.mock.calls.length === 0; i++) {
await Promise.resolve()
}
expect(spawnSystemSshMock).toHaveBeenCalledTimes(1)
await conn.disconnect()
expect(proc.kill).toHaveBeenCalled()
proc.stdout.emit('data', Buffer.from('ORCA-SYSTEM-SSH-READY'))
await expect(connectResult).resolves.toMatchObject({
message: 'SSH connection attempt was cancelled'
})
expect(conn.getState().status).toBe('disconnected')
expect(conn.usesSystemSshTransport()).toBe(false)
expect(callbacks.onStateChange).not.toHaveBeenCalledWith(
'target-1',
expect.objectContaining({ status: 'connected' })
)
})
it('treats delayed direct system SSH exit after disconnect as cancellation', async () => {
const proc = createPendingSystemSshProcess()
let capturedExit: ((exitCode: number | null) => void) | null = null
proc.onExit = vi.fn((handler: (exitCode: number | null) => void) => {
capturedExit = handler
})
proc.kill = vi.fn(() => {
queueMicrotask(() => capturedExit?.(null))
})
spawnSystemSshMock.mockReturnValueOnce(proc)
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
const callbacks = createCallbacks()
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), callbacks)
const connectResult = conn.connectViaSystemSsh().catch((err: Error) => err)
for (let i = 0; i < 5 && spawnSystemSshMock.mock.calls.length === 0; i++) {
await Promise.resolve()
}
expect(spawnSystemSshMock).toHaveBeenCalledTimes(1)
await conn.disconnect()
const result = await connectResult
expect(result).toBeInstanceOf(Error)
if (!(result instanceof Error)) {
throw new Error('Expected direct system SSH startup to reject')
}
expect(result.message).toBe('SSH connection attempt was cancelled')
expect(conn.getState()).toMatchObject({ status: 'disconnected', error: null })
expect(conn.usesSystemSshTransport()).toBe(false)
expect(callbacks.onStateChange).not.toHaveBeenCalledWith(
'target-1',
expect.objectContaining({ status: 'error' })
)
expect(callbacks.onStateChange).not.toHaveBeenCalledWith(
'target-1',
expect.objectContaining({ status: 'connected' })
)
})
it('does not spawn direct system SSH after disconnect while OpenSSH config is resolving', async () => {
let resolveConfig!: (config: SshResolvedConfig | null) => void
vi.mocked(resolveWithSshG).mockReturnValueOnce(
new Promise<SshResolvedConfig | null>((resolve) => {
resolveConfig = resolve
})
)
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
const connectResult = conn.connectViaSystemSsh().catch((err: Error) => err)
await Promise.resolve()
await conn.disconnect()
resolveConfig(createResolvedConfig())
await expect(connectResult).resolves.toMatchObject({
message: 'SSH connection attempt was cancelled'
})
expect(spawnSystemSshMock).not.toHaveBeenCalled()
expect(conn.getState().status).toBe('disconnected')
})
it('does not spawn direct system SSH retry after cancellation between mux failure and retry', async () => {
getOrcaControlSocketPathMock.mockReturnValue('/tmp/orca-ssh-501/stale-socket')
const firstProc = createPendingSystemSshProcess()
let conn!: SshConnection
firstProc.onExit = vi.fn((handler: (exitCode: number | null) => void) => {
queueMicrotask(() => {
handler(255)
void conn.disconnect()
})
})
spawnSystemSshMock
.mockReturnValueOnce(firstProc)
.mockImplementation(() => createSystemSshProcess())
vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig())
conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
const result = await conn.connectViaSystemSsh().catch((err: Error) => err)
expect(result).toMatchObject({ message: 'SSH connection attempt was cancelled' })
expect(spawnSystemSshMock).toHaveBeenCalledTimes(1)
expect(conn.getState().status).toBe('disconnected')
})
})
describe('shouldUseSystemSshTransport', () => {
it('uses system transport for target or resolved OpenSSH proxy directives', () => {
expect(shouldUseSystemSshTransport(createTarget(), { proxyUseFdpass: true })).toBe(true)
expect(shouldUseSystemSshTransport(createTarget(), { proxyUseFdpass: false })).toBe(false)
expect(
shouldUseSystemSshTransport(createTarget({ proxyCommand: 'ssh -W %h:%p bastion' }), null)
).toBe(true)
expect(shouldUseSystemSshTransport(createTarget({ jumpHost: 'bastion' }), null)).toBe(true)
expect(
shouldUseSystemSshTransport(createTarget(), {
proxyUseFdpass: false,
proxyCommand: 'ssh -W %h:%p bastion'
})
).toBe(true)
expect(
shouldUseSystemSshTransport(createTarget(), {
proxyUseFdpass: false,
proxyJump: 'bastion'
})
).toBe(true)
})
it('allows an environment override for e2e coverage', () => {
vi.stubEnv('ORCA_SSH_FORCE_SYSTEM_TRANSPORT', '1')
expect(shouldUseSystemSshTransport(createTarget(), null)).toBe(true)
vi.unstubAllEnvs()
})
it('ignores stale imported proxy fields when fresh OpenSSH config has no proxy', () => {
expect(
shouldUseSystemSshTransport(
createTarget({
source: 'ssh-config',
configHost: 'workbox',
proxyCommand: 'ssh -W %h:%p stale-bastion'
}),
{
proxyUseFdpass: false
}
)
).toBe(false)
})
})
describe('SshConnectionManager', () => {
beforeEach(() => {
eventHandlers = new Map()
connectBehavior = 'ready'
connectErrorMessage = ''
connectSequence = []
clientInstances = []
})
it('connect creates and stores a connection', async () => {
const mgr = new SshConnectionManager(createCallbacks())
const target = createTarget()
const conn = await mgr.connect(target)
expect(conn.getState().status).toBe('connected')
expect(mgr.getConnection(target.id)).toBe(conn)
})
it('getState returns connection state', async () => {
const mgr = new SshConnectionManager(createCallbacks())
const target = createTarget()
await mgr.connect(target)
const state = mgr.getState(target.id)
expect(state).toBeTruthy()
expect(state!.status).toBe('connected')
})
it('getState returns null for unknown targets', () => {
const mgr = new SshConnectionManager(createCallbacks())
expect(mgr.getState('unknown')).toBeNull()
})
it('disconnect removes the connection', async () => {
const mgr = new SshConnectionManager(createCallbacks())
const target = createTarget()
await mgr.connect(target)
await mgr.disconnect(target.id)
expect(mgr.getConnection(target.id)).toBeUndefined()
})
it('disconnect is a no-op for unknown targets', async () => {
const mgr = new SshConnectionManager(createCallbacks())
await mgr.disconnect('unknown')
})
it('reuses existing connected connection for same target', async () => {
const mgr = new SshConnectionManager(createCallbacks())
const target = createTarget()
const conn1 = await mgr.connect(target)
const conn2 = await mgr.connect(target)
expect(conn2).toBe(conn1)
})
it('getAllStates returns all connection states', async () => {
const mgr = new SshConnectionManager(createCallbacks())
await mgr.connect(createTarget({ id: 'a' }))
await mgr.connect(createTarget({ id: 'b' }))
const states = mgr.getAllStates()
expect(states.size).toBe(2)
expect(states.get('a')?.status).toBe('connected')
expect(states.get('b')?.status).toBe('connected')
})
it('disconnectAll disconnects all connections', async () => {
const mgr = new SshConnectionManager(createCallbacks())
await mgr.connect(createTarget({ id: 'a' }))
await mgr.connect(createTarget({ id: 'b' }))
await mgr.disconnectAll()
expect(mgr.getConnection('a')).toBeUndefined()
expect(mgr.getConnection('b')).toBeUndefined()
})
})