/* 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 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 _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 { return new Promise((resolve) => { notifyClientCreated = resolve }) } async function connectWithFakeTimers(conn: SshConnection): Promise { const clientCreated = nextSshClientCreation() const connected = conn.connect() await clientCreated await vi.advanceTimersByTimeAsync(1) await connected } async function advanceToNextSshClient(delayMs: number): Promise { 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 { return { id: 'target-1', label: 'Test Server', host: 'example.com', port: 22, username: 'deploy', ...overrides } } function createResolvedConfig(overrides?: Partial): 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 { return { onStateChange: vi.fn(), ...overrides } } function createSystemCommandChannel(): EventEmitter & { stdin: { end: ReturnType; write: ReturnType } stderr: EventEmitter close: ReturnType } { const channel = new EventEmitter() as EventEmitter & { stdin: { end: ReturnType; write: ReturnType } stderr: EventEmitter close: ReturnType } 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 { const channel = new EventEmitter() as ReturnType 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 { const channel = new EventEmitter() as ReturnType 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) }) // The point of refusing the key is that we do not trust who is on the other end. ssh2 reports a // denied key as a generic auth failure, and an encrypted identity file makes the passphrase branch // eligible on message shape alone — so without an explicit abort we would hand the secret to the // party we just refused. it('never asks for a credential after refusing a host key', 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') mockPresentedHostKey = Buffer.from('not-a-real-host-key-blob') const onCredentialRequest = vi.fn(async () => 'secret') try { const conn = new SshConnection( createTarget({ identityFile: keyPath }), createCallbacks({ onCredentialRequest }) ) await expect(conn.connect()).rejects.toThrow(/host key verification failed/i) expect(onCredentialRequest).not.toHaveBeenCalled() } finally { rmSync(tempDir, { recursive: true, force: true }) } }) // 'auth-failed' invites the user to re-enter credentials, which is the wrong thing to offer and // the wrong thing to blame; nothing about their credentials is wrong. it('surfaces a refused host key as an error rather than an auth failure', async () => { mockPresentedHostKey = Buffer.from('not-a-real-host-key-blob') const conn = new SshConnection(createTarget(), createCallbacks()) await expect(conn.connect()).rejects.toThrow() expect(conn.getState().status).toBe('error') expect(conn.getState().error).toMatch(/host key verification failed/i) }) // Everyone's first connection happens before ~/.ssh/known_hosts exists — ssh creates it on its // own first connect. Treating a file that was never there as a source we failed to read would // refuse every connection a new profile ever makes, which is how a fail-closed rule turns into a // product that does not work. it('connects on first contact when no known_hosts file exists yet', async () => { const emptyHome = mkdtempSync(join(tmpdir(), 'orca-ssh-home-')) vi.stubEnv('HOME', emptyHome) try { const conn = new SshConnection(createTarget(), createCallbacks()) await conn.connect() expect(lastHostKeyAccepted).toBe(true) expect(conn.getState().status).toBe('connected') } finally { rmSync(emptyHome, { recursive: true, force: true }) } }) // Retrying re-derives the same decision, so a ladder that treated this as transient would back off // against a host it has already refused until it gave up — burying the reason. it('does not retry a refused host key', async () => { mockPresentedHostKey = Buffer.from('not-a-real-host-key-blob') const conn = new SshConnection(createTarget(), createCallbacks()) await expect(conn.connect()).rejects.toThrow() expect(connectAttempts).toBe(1) }) 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 } 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 } 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((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((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 } 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 | null = null spawnSystemSshCommandMock.mockImplementation(() => { const channel = new EventEmitter() as ReturnType 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 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 }).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((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() }) })