Files
orca/src/main/ssh/ssh-connection.test.ts
T
Jinwoo-HandOrca 2a523e7b15 fix(ssh): enable TCP_NODELAY on ssh2 client to eliminate per-keystroke typing lag (#1660)
ssh2 leaves Nagle's algorithm on by default. For single-byte keystrokes
through a remote PTY, Nagle interacts with the kernel's delayed-ACK timer
and adds up to ~40 ms per keystroke — visible as the typing lag reported
in #1660. OpenSSH's `ssh` client sets TCP_NODELAY whenever a PTY is
allocated; this change mirrors that on the ssh2 client right after the
`ready` event in doSsh2Connect, covering both initial connect and
auto-reconnect.

Proxy-command / proxy-jump connections (where ssh2's underlying socket
is a custom Duplex over a child-process pipe) are a no-op by design,
gated by the public Client.setNoDelay()'s own type guard. A discriminating
log line records which path each connect took.

Tests cover initial connect and a full reconnect cycle to guard against
the regression class "Nagle is re-enabled because someone refactored
only the initial connect path."

Co-authored-by: Orca <help@stably.ai>
2026-05-10 21:11:47 -04:00

290 lines
8.7 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from 'vitest'
import { Socket } from 'net'
let eventHandlers: Map<string, (...args: unknown[]) => void>
let connectBehavior: 'ready' | 'error' = 'ready'
let connectErrorMessage = ''
type MockSshClient = {
setNoDelay: ReturnType<typeof vi.fn>
_sock: Socket | undefined
}
let clientInstances: MockSshClient[] = []
vi.mock('ssh2', () => {
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()
constructor() {
clientInstances.push(this)
}
on(event: string, handler: (...args: unknown[]) => void) {
eventHandlers?.set(event, handler)
}
connect() {
setTimeout(() => {
if (connectBehavior === 'error') {
eventHandlers?.get('error')?.(new Error(connectErrorMessage))
} else {
eventHandlers?.get('ready')?.()
}
}, 0)
}
end() {}
destroy() {}
exec() {}
sftp() {}
}
return { Client: MockSshClient }
})
vi.mock('./ssh-system-fallback', () => ({
spawnSystemSsh: vi.fn().mockReturnValue({
stdin: {},
stdout: {},
stderr: {},
kill: vi.fn(),
onExit: vi.fn(),
pid: 99999
})
}))
vi.mock('./ssh-config-parser', () => ({
resolveWithSshG: vi.fn().mockResolvedValue(null)
}))
import { SshConnection, SshConnectionManager, type SshConnectionCallbacks } from './ssh-connection'
import { resolveWithSshG } from './ssh-config-parser'
import type { SshTarget } from '../../shared/ssh-types'
function createTarget(overrides?: Partial<SshTarget>): SshTarget {
return {
id: 'target-1',
label: 'Test Server',
host: 'example.com',
port: 22,
username: 'deploy',
...overrides
}
}
function createCallbacks(overrides?: Partial<SshConnectionCallbacks>): SshConnectionCallbacks {
return {
onStateChange: vi.fn(),
...overrides
}
}
describe('SshConnection', () => {
beforeEach(() => {
eventHandlers = new Map()
connectBehavior = 'ready'
connectErrorMessage = ''
clientInstances = []
})
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(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)
})
it('enables TCP_NODELAY on the new ssh2 client after a reconnect cycle', async () => {
// Why: guards the "Nagle is re-enabled because someone refactored only
// the initial connect path" regression class. attemptConnect bumps
// connectGeneration on every call, and both the initial connect and the
// explicit reconnect path go through doSsh2Connect → client.on('ready').
// The new client must also receive setNoDelay(true).
const conn = new SshConnection(createTarget(), createCallbacks())
await conn.connect()
expect(clientInstances).toHaveLength(1)
expect(clientInstances[0].setNoDelay).toHaveBeenCalledWith(true)
// Simulate the reconnect path: a fresh attemptConnect run via the
// internal helper that scheduleReconnect uses. Easiest from the public
// API is to call connect() again — disposed/connected guard rejects, so
// we exercise the path via a private call. Use the bracket-access
// form to keep the test free of `any` casts.
const privateConn = conn as unknown as {
attemptConnect: () => Promise<void>
}
await privateConn.attemptConnect()
expect(clientInstances).toHaveLength(2)
expect(clientInstances[1].setNoDelay).toHaveBeenCalledWith(true)
})
it('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('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('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')
})
})
describe('SshConnectionManager', () => {
beforeEach(() => {
eventHandlers = new Map()
connectBehavior = 'ready'
connectErrorMessage = ''
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()
})
})