import { afterEach, describe, expect, it, vi, beforeEach } from 'vitest' import { join } from 'node:path' import { BaseAgent, utils, type ParsedKey } from 'ssh2' vi.mock('os', () => ({ homedir: () => '/home/testuser' })) const mockExistsSync = vi.fn().mockReturnValue(false) const mockReadFileSync = vi.fn() const TEST_HOME = '/home/testuser' function testHomePath(...parts: string[]): string { return join(TEST_HOME, ...parts) } vi.mock('fs', () => ({ existsSync: (...args: unknown[]) => mockExistsSync(...args), readFileSync: (...args: unknown[]) => mockReadFileSync(...args) })) import { isTransientError, isSystemSshFallbackError, isGssapiSystemSshFallbackCandidate, isAuthError, isAgentFallbackError, sleep, shellEscape, findDefaultKeyFile, buildConnectConfig, resolveAgentSocket } from './ssh-connection-utils' import { resolveEffectiveProxy } from './ssh-proxy-command' import type { SshTarget } from '../../shared/ssh-types' import type { SshResolvedConfig } from './ssh-config-parser' // ── isTransientError ───────────────────────────────────────────────── describe('isTransientError', () => { it('returns true for ETIMEDOUT code', () => { const err = new Error('timed out') as NodeJS.ErrnoException err.code = 'ETIMEDOUT' expect(isTransientError(err)).toBe(true) }) it('returns true for ECONNREFUSED code', () => { const err = new Error('refused') as NodeJS.ErrnoException err.code = 'ECONNREFUSED' expect(isTransientError(err)).toBe(true) }) it('returns true for ECONNRESET code', () => { const err = new Error('reset') as NodeJS.ErrnoException err.code = 'ECONNRESET' expect(isTransientError(err)).toBe(true) }) it('returns true for EHOSTUNREACH code', () => { const err = new Error('host unreachable') as NodeJS.ErrnoException err.code = 'EHOSTUNREACH' expect(isTransientError(err)).toBe(true) }) it('returns true for ENETUNREACH code', () => { const err = new Error('net unreachable') as NodeJS.ErrnoException err.code = 'ENETUNREACH' expect(isTransientError(err)).toBe(true) }) it('returns true for EAI_AGAIN code', () => { const err = new Error('dns') as NodeJS.ErrnoException err.code = 'EAI_AGAIN' expect(isTransientError(err)).toBe(true) }) it('returns true for ETIMEDOUT in message (no code)', () => { expect(isTransientError(new Error('connect ETIMEDOUT 1.2.3.4:22'))).toBe(true) }) it('returns true for ECONNREFUSED in message', () => { expect(isTransientError(new Error('connect ECONNREFUSED 1.2.3.4:22'))).toBe(true) }) it('returns true for ECONNRESET in message', () => { expect(isTransientError(new Error('read ECONNRESET'))).toBe(true) }) it('returns true for the bounded SSH authentication watchdog', () => { const timeout = Object.assign(new Error('Timed out while waiting for SSH authentication'), { level: 'client-timeout' }) expect(isTransientError(timeout)).toBe(true) expect(isTransientError(new Error('Timed out while waiting for SSH authentication'))).toBe(true) }) it('returns false for auth errors', () => { expect(isTransientError(new Error('All configured authentication methods failed'))).toBe(false) }) it('returns false for generic errors', () => { expect(isTransientError(new Error('something went wrong'))).toBe(false) }) }) // ── isSystemSshFallbackError ───────────────────────────────────────── describe('isSystemSshFallbackError', () => { it('returns true for local reachability errors that system ssh may bypass', () => { const hostErr = new Error('host unreachable') as NodeJS.ErrnoException hostErr.code = 'EHOSTUNREACH' const netErr = new Error('net unreachable') as NodeJS.ErrnoException netErr.code = 'ENETUNREACH' expect(isSystemSshFallbackError(hostErr)).toBe(true) expect(isSystemSshFallbackError(netErr)).toBe(true) }) it('returns false for transient errors that should keep the normal retry path', () => { const refused = new Error('refused') as NodeJS.ErrnoException refused.code = 'ECONNREFUSED' expect(isSystemSshFallbackError(refused)).toBe(false) expect(isSystemSshFallbackError(new Error('connect ETIMEDOUT 1.2.3.4:22'))).toBe(false) }) }) // ── isGssapiSystemSshFallbackCandidate ─────────────────────────────── describe('isGssapiSystemSshFallbackCandidate', () => { const authErr = new Error('All configured authentication methods failed') it('returns true for auth failures when resolved config enables GSSAPI', () => { expect(isGssapiSystemSshFallbackCandidate(authErr, {}, { gssapiAuthentication: true })).toBe( true ) }) it('returns true for passphrase failures so Kerberos SSO runs before prompting', () => { const passphraseErr = new Error('Encrypted private OpenSSH key detected, but no passphrase') expect( isGssapiSystemSshFallbackCandidate(passphraseErr, {}, { gssapiAuthentication: true }) ).toBe(true) }) it('returns false when the target already tried system ssh proactively', () => { expect( isGssapiSystemSshFallbackCandidate( authErr, { gssapiAuthentication: true }, { gssapiAuthentication: true } ) ).toBe(false) }) it('returns false without GSSAPI in the resolved config', () => { expect(isGssapiSystemSshFallbackCandidate(authErr, {}, { gssapiAuthentication: false })).toBe( false ) expect(isGssapiSystemSshFallbackCandidate(authErr, {}, null)).toBe(false) }) it('returns false for network errors so retry semantics stay unchanged', () => { const netErr = new Error('connect ETIMEDOUT 1.2.3.4:22') expect(isGssapiSystemSshFallbackCandidate(netErr, {}, { gssapiAuthentication: true })).toBe( false ) }) }) // ── isAuthError ────────────────────────────────────────────────────── describe('isAuthError', () => { it('returns true for "All configured authentication methods failed"', () => { expect(isAuthError(new Error('All configured authentication methods failed'))).toBe(true) }) it('returns true for "Authentication failed"', () => { expect(isAuthError(new Error('Authentication failed'))).toBe(true) }) it('returns true for client-authentication level', () => { const err = new Error('auth') as Error & { level: string } err.level = 'client-authentication' expect(isAuthError(err)).toBe(true) }) it('returns true for server auth-attempt exhaustion', () => { expect(isAuthError(new Error('Received disconnect: Too many authentication failures'))).toBe( true ) }) it.each([ 'Permission denied (publickey).', 'Permission denied (publickey,password).', 'Permission denied, please try again.' ])('detects OpenSSH credential rejection: %s', (message) => { expect(isAuthError(new Error(message))).toBe(true) }) it('returns false for transient errors', () => { expect(isAuthError(new Error('connect ETIMEDOUT'))).toBe(false) }) it('does not classify a local filesystem permission failure as authentication', () => { expect(isAuthError(new Error('Permission denied (os error 13)'))).toBe(false) }) }) // ── isAgentFallbackError ──────────────────────────────────────────── describe('isAgentFallbackError', () => { it('returns true for ssh2 agent-level failures', () => { const err = new Error('Failed to connect to agent') as Error & { level: string } err.level = 'agent' expect(isAgentFallbackError(err)).toBe(true) }) it('returns true when agent auth exhausts the server auth attempt limit', () => { expect( isAgentFallbackError(new Error('Received disconnect: Too many authentication failures')) ).toBe(true) }) it('keeps unrelated transport errors out of agent fallback handling', () => { expect(isAgentFallbackError(new Error('connect ECONNRESET'))).toBe(false) }) }) // ── sleep ──────────────────────────────────────────────────────────── describe('sleep', () => { it('resolves after the given delay', async () => { const start = Date.now() await sleep(50) expect(Date.now() - start).toBeGreaterThanOrEqual(40) }) }) // ── shellEscape ────────────────────────────────────────────────────── describe('shellEscape', () => { it('wraps string in single quotes', () => { expect(shellEscape('hello')).toBe("'hello'") }) it('escapes embedded single quotes', () => { expect(shellEscape("it's")).toBe("'it'\\''s'") }) it('handles empty string', () => { expect(shellEscape('')).toBe("''") }) it('handles special characters', () => { expect(shellEscape('foo bar; rm -rf /')).toBe("'foo bar; rm -rf /'") }) }) // ── findDefaultKeyFile ─────────────────────────────────────────────── describe('findDefaultKeyFile', () => { beforeEach(() => { mockExistsSync.mockReset() mockExistsSync.mockReturnValue(false) mockReadFileSync.mockReset() }) it('returns undefined when no default keys exist', () => { expect(findDefaultKeyFile()).toBeUndefined() }) it('returns the first existing key file', () => { mockExistsSync.mockImplementation((path: unknown) => { return path === testHomePath('.ssh', 'id_ed25519') }) mockReadFileSync.mockReturnValue(Buffer.from('key-contents')) const result = findDefaultKeyFile() expect(result).toBeDefined() expect(result!.path).toBe('~/.ssh/id_ed25519') expect(result!.contents).toEqual(Buffer.from('key-contents')) }) it('probes regular and FIDO2 keys in stable default order', () => { const checkedPaths: string[] = [] mockExistsSync.mockImplementation((path: unknown) => { checkedPaths.push(String(path)) return false }) findDefaultKeyFile() expect(checkedPaths).toEqual([ testHomePath('.ssh', 'id_ed25519'), testHomePath('.ssh', 'id_rsa'), testHomePath('.ssh', 'id_ecdsa'), testHomePath('.ssh', 'id_dsa'), testHomePath('.ssh', 'id_xmss') ]) }) it('keeps a regular default ahead of a malformed FIDO2 default', () => { mockExistsSync.mockImplementation((path: unknown) => { return ( path === testHomePath('.ssh', 'id_rsa') || path === testHomePath('.ssh', 'id_ed25519_sk') ) }) mockReadFileSync.mockImplementation((path: unknown) => { if (String(path) === testHomePath('.ssh', 'id_ed25519_sk')) { throw new Error('malformed FIDO2 key') } return Buffer.from('rsa-key') }) expect(findDefaultKeyFile()).toEqual({ path: '~/.ssh/id_rsa', contents: Buffer.from('rsa-key') }) }) it('leaves FIDO2 defaults out of the ssh2 private-key fallback', () => { mockExistsSync.mockImplementation((path: unknown) => { return path === testHomePath('.ssh', 'id_ed25519_sk') }) expect(findDefaultKeyFile()).toBeUndefined() expect(mockReadFileSync).not.toHaveBeenCalled() }) it('skips unreadable key files and tries next', () => { mockExistsSync.mockImplementation((path: unknown) => { return path === testHomePath('.ssh', 'id_ed25519') || path === testHomePath('.ssh', 'id_rsa') }) mockReadFileSync.mockImplementation((path: unknown) => { if (String(path) === testHomePath('.ssh', 'id_ed25519')) { throw new Error('permission denied') } return Buffer.from('rsa-key') }) const result = findDefaultKeyFile() expect(result).toBeDefined() expect(result!.path).toBe('~/.ssh/id_rsa') }) }) // ── buildConnectConfig ────────────────────────────────────────────── function makeTarget(overrides?: Partial): SshTarget { return { id: 'test-1', label: 'myhost', host: 'example.com', port: 22, username: 'deploy', ...overrides } } function makeResolved(overrides?: Partial): SshResolvedConfig { return { hostname: '10.0.0.1', port: 22, identityFile: [], forwardAgent: false, identitiesOnly: false, proxyUseFdpass: false, controlMaster: 'no', controlPersist: 'no', userKnownHostsFiles: [], globalKnownHostsFiles: [], strictHostKeyChecking: 'ask', hashKnownHosts: false, updateHostKeys: 'no', ...overrides } } describe('buildConnectConfig', () => { const originalEnv = process.env.SSH_AUTH_SOCK beforeEach(() => { mockExistsSync.mockReset() mockExistsSync.mockReturnValue(false) mockReadFileSync.mockReset() process.env.SSH_AUTH_SOCK = '/tmp/agent.sock' }) afterEach(() => { vi.restoreAllMocks() if (originalEnv !== undefined) { process.env.SSH_AUTH_SOCK = originalEnv } else { delete process.env.SSH_AUTH_SOCK } }) it('uses target host/port/username', () => { const config = buildConnectConfig(makeTarget(), null) expect(config.host).toBe('example.com') expect(config.port).toBe(22) expect(config.username).toBe('deploy') }) it('falls back to resolved config when target fields are empty', () => { const config = buildConnectConfig( makeTarget({ host: '', port: 0, username: '' }), makeResolved({ hostname: '10.0.0.1', port: 2222, user: 'admin' }) ) expect(config.host).toBe('10.0.0.1') expect(config.port).toBe(2222) expect(config.username).toBe('admin') }) it('uses ssh -G HostName when a config-host target still points at its alias', () => { const config = buildConnectConfig( makeTarget({ label: 'workbox', configHost: 'workbox', host: 'workbox' }), makeResolved({ hostname: 'workbox.internal' }) ) expect(config.host).toBe('workbox.internal') }) it('uses ssh -G Port when a config-host target still has the default port', () => { const config = buildConnectConfig( makeTarget({ configHost: 'workbox', host: 'workbox', port: 22 }), makeResolved({ port: 2202 }) ) expect(config.port).toBe(2202) }) it('keeps explicit non-default target ports ahead of ssh -G Port', () => { const config = buildConnectConfig( makeTarget({ configHost: 'workbox', host: 'workbox', port: 2022 }), makeResolved({ port: 2202 }) ) expect(config.port).toBe(2022) }) it('uses fresh OpenSSH endpoint authority for imported config targets', () => { const config = buildConnectConfig( makeTarget({ source: 'ssh-config', configHost: 'workbox', host: 'stale.example.com', port: 2022, username: 'stale-user' }), makeResolved({ hostname: 'current.example.com', port: 2202, user: 'current-user' }) ) expect(config.host).toBe('current.example.com') expect(config.port).toBe(2202) expect(config.username).toBe('current-user') }) it('keeps imported endpoint fields as the fallback when ssh -G is unavailable', () => { const config = buildConnectConfig( makeTarget({ source: 'ssh-config', configHost: 'workbox', host: 'fallback.example.com', port: 2022, username: 'fallback-user' }), null ) expect(config.host).toBe('fallback.example.com') expect(config.port).toBe(2022) expect(config.username).toBe('fallback-user') }) it('sets readyTimeout to CONNECT_TIMEOUT_MS', () => { const config = buildConnectConfig(makeTarget(), null) expect(config.readyTimeout).toBe(30_000) }) it('sets keepaliveInterval to 15s', () => { const config = buildConnectConfig(makeTarget(), null) expect(config.keepaliveInterval).toBe(15_000) }) it('enables keyboard-interactive auth so MFA challenges can be answered', () => { const config = buildConnectConfig(makeTarget(), null) expect(config.tryKeyboard).toBe(true) }) it('uses agent auth when no explicit key and SSH_AUTH_SOCK is set', () => { const config = buildConnectConfig(makeTarget(), null) expect(config.agent).toBe('/tmp/agent.sock') }) it('enables agent forwarding when OpenSSH config requests it and an agent is available', () => { const config = buildConnectConfig(makeTarget(), makeResolved({ forwardAgent: true })) expect(config.agent).toBe('/tmp/agent.sock') expect(config.agentForward).toBe(true) }) it('does not enable agent forwarding without a usable agent', () => { const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux') delete process.env.SSH_AUTH_SOCK try { const config = buildConnectConfig(makeTarget(), makeResolved({ forwardAgent: true })) expect(config.agent).toBeUndefined() expect(config.agentForward).toBeUndefined() } finally { platformSpy.mockRestore() } }) it('uses configured IdentityAgent before SSH_AUTH_SOCK', () => { const config = buildConnectConfig( makeTarget(), makeResolved({ identityAgent: '/tmp/one-password.sock' }) ) expect(config.agent).toBe('/tmp/one-password.sock') }) it('prefers ssh -G resolved IdentityAgent for config-host targets', () => { const config = buildConnectConfig( makeTarget({ configHost: 'work', identityAgent: '%d/.1password/agent.sock' }), makeResolved({ identityAgent: testHomePath('.1password', 'agent.sock') }) ) expect(config.agent).toBe(testHomePath('.1password', 'agent.sock')) }) it('allows IdentityAgent none to disable agent auth', () => { const config = buildConnectConfig(makeTarget(), makeResolved({ identityAgent: 'none' })) expect(config.agent).toBeUndefined() }) it('resolves IdentityAgent SSH_AUTH_SOCK from the environment', () => { expect(resolveAgentSocket(makeTarget(), makeResolved({ identityAgent: 'SSH_AUTH_SOCK' }))).toBe( '/tmp/agent.sock' ) expect( resolveAgentSocket(makeTarget(), makeResolved({ identityAgent: '$SSH_AUTH_SOCK' })) ).toBe('/tmp/agent.sock') }) it('uses the Windows OpenSSH agent pipe when no environment socket is available on Windows', () => { const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32') delete process.env.SSH_AUTH_SOCK try { expect(resolveAgentSocket(makeTarget(), null)).toBe('\\\\.\\pipe\\openssh-ssh-agent') } finally { platformSpy.mockRestore() } }) it('wraps agent auth with IdentityFile filtering when IdentitiesOnly is enabled', () => { mockReadFileSync.mockImplementation((path: unknown) => { if (String(path) === '/home/user/.ssh/work_key.pub') { return 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILI4wa2zRZoB26D015dsafYmu3jDCI7rh26bFXZrUiAp test-key' } throw new Error('unexpected read') }) const config = buildConnectConfig( makeTarget(), makeResolved({ identityFile: ['/home/user/.ssh/work_key'], identitiesOnly: true }) ) expect(config.agent).toMatchObject({ kind: 'identity-filtered-agent' }) expect(config.agent).toBeInstanceOf(BaseAgent) expect(config.privateKey).toBeUndefined() expect(mockReadFileSync).toHaveBeenCalledWith('/home/user/.ssh/work_key.pub') }) it('does not offer broad agent auth when IdentitiesOnly keys cannot be parsed', () => { mockReadFileSync.mockReturnValue(Buffer.from('not-a-key')) const config = buildConnectConfig( makeTarget(), makeResolved({ identityFile: ['/home/user/.ssh/work_key'], identitiesOnly: true }) ) expect(config.agent).toBeUndefined() expect(config.privateKey).toEqual(Buffer.from('not-a-key')) }) it('includes unencrypted target.identityFile auth when an agent is available', () => { vi.spyOn(utils, 'parseKey').mockReturnValue({ isPrivateKey: () => true } as ParsedKey) mockReadFileSync.mockReturnValue(Buffer.from('key')) const config = buildConnectConfig(makeTarget({ identityFile: '/home/user/.ssh/custom' }), null) expect(config.agent).toBe('/tmp/agent.sock') expect(config.privateKey).toEqual(Buffer.from('key')) expect(mockReadFileSync).toHaveBeenCalledWith('/home/user/.ssh/custom') }) it('defers encrypted target.identityFile auth when an agent is available', () => { vi.spyOn(utils, 'parseKey').mockReturnValue( new Error('Encrypted private OpenSSH key detected, but no passphrase given') ) mockReadFileSync.mockReturnValue(Buffer.from('encrypted-key')) const config = buildConnectConfig(makeTarget({ identityFile: '/home/user/.ssh/custom' }), null) expect(config.agent).toBe('/tmp/agent.sock') expect(config.privateKey).toBeUndefined() expect(mockReadFileSync).toHaveBeenCalledWith('/home/user/.ssh/custom') }) it('uses keyFile auth when target.identityFile is set and no agent is available', () => { const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux') delete process.env.SSH_AUTH_SOCK mockReadFileSync.mockReturnValue(Buffer.from('key')) try { const config = buildConnectConfig( makeTarget({ identityFile: '/home/user/.ssh/custom' }), null ) expect(config.privateKey).toEqual(Buffer.from('key')) expect(config.agent).toBeUndefined() } finally { platformSpy.mockRestore() } }) it('uses fresh OpenSSH IdentityFile authority for imported config targets', () => { mockReadFileSync.mockImplementation((path: unknown) => Buffer.from(String(path))) const config = buildConnectConfig( makeTarget({ source: 'ssh-config', configHost: 'workbox', identityFile: '/home/user/.ssh/stale' }), makeResolved({ identityFile: ['/home/user/.ssh/current'] }), { includeAgent: false, includePrivateKey: true } ) expect(config.privateKey).toEqual(Buffer.from('/home/user/.ssh/current')) expect(mockReadFileSync).toHaveBeenCalledWith('/home/user/.ssh/current') }) it('expands Windows-style target.identityFile before reading private key', () => { mockReadFileSync.mockReturnValue(Buffer.from('key')) const config = buildConnectConfig(makeTarget({ identityFile: '~\\.ssh\\custom' }), null, { includeAgent: false, includePrivateKey: true }) expect(config.privateKey).toEqual(Buffer.from('key')) expect(mockReadFileSync).toHaveBeenCalledWith(testHomePath('.ssh', 'custom')) }) it('includes unencrypted resolved identityFile auth when an agent is available', () => { vi.spyOn(utils, 'parseKey').mockReturnValue({ isPrivateKey: () => true } as ParsedKey) mockReadFileSync.mockReturnValue(Buffer.from('custom-key')) const config = buildConnectConfig( makeTarget(), makeResolved({ identityFile: ['/home/user/.ssh/work_key'] }) ) expect(config.agent).toBe('/tmp/agent.sock') expect(config.privateKey).toEqual(Buffer.from('custom-key')) }) it('uses agent auth without probing when resolved identityFile is a default path (expanded)', () => { const config = buildConnectConfig( makeTarget(), makeResolved({ identityFile: [testHomePath('.ssh', 'id_ed25519')] }) ) expect(config.agent).toBe('/tmp/agent.sock') expect(config.privateKey).toBeUndefined() expect(mockReadFileSync).not.toHaveBeenCalled() }) it('does not probe default key files before agent auth', () => { mockExistsSync.mockImplementation( (p: unknown) => String(p) === testHomePath('.ssh', 'id_ed25519') ) const config = buildConnectConfig(makeTarget(), null) expect(config.agent).toBe('/tmp/agent.sock') expect(config.privateKey).toBeUndefined() expect(mockExistsSync).not.toHaveBeenCalled() }) it('provides fallback key when no agent is available', () => { const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux') delete process.env.SSH_AUTH_SOCK mockExistsSync.mockImplementation( (p: unknown) => String(p) === testHomePath('.ssh', 'id_ed25519') ) mockReadFileSync.mockReturnValue(Buffer.from('fallback')) try { const config = buildConnectConfig(makeTarget(), null) expect(config.agent).toBeUndefined() expect(config.privateKey).toEqual(Buffer.from('fallback')) } finally { platformSpy.mockRestore() } }) it('can force private key inclusion for the post-agent fallback path', () => { mockReadFileSync.mockReturnValue(Buffer.from('key')) const config = buildConnectConfig( makeTarget({ identityFile: '/home/user/.ssh/custom' }), null, { includeAgent: false, includePrivateKey: true } ) expect(config.agent).toBeUndefined() expect(config.privateKey).toEqual(Buffer.from('key')) }) }) // ── resolveEffectiveProxy ─────────────────────────────────────────── describe('resolveEffectiveProxy', () => { it('returns target.proxyCommand first', () => { const target = { ...makeTarget(), proxyCommand: 'cloudflared access ssh --hostname %h' } const resolved = makeResolved({ proxyCommand: 'other' }) expect(resolveEffectiveProxy(target, resolved)).toEqual({ kind: 'proxy-command', command: 'cloudflared access ssh --hostname %h' }) }) it('uses fresh OpenSSH proxy authority for imported config targets', () => { const target = { ...makeTarget(), source: 'ssh-config' as const, configHost: 'workbox', proxyCommand: 'ssh -W %h:%p stale-bastion' } expect(resolveEffectiveProxy(target, makeResolved())).toBeUndefined() expect( resolveEffectiveProxy(target, makeResolved({ proxyCommand: 'ssh -W %h:%p current-bastion' })) ).toEqual({ kind: 'proxy-command', command: 'ssh -W %h:%p current-bastion' }) }) it('falls back to resolved proxyCommand', () => { expect( resolveEffectiveProxy(makeTarget(), makeResolved({ proxyCommand: 'ssh -W %h:%p gw' })) ).toEqual({ kind: 'proxy-command', command: 'ssh -W %h:%p gw' }) }) it('returns structured jump-host config for target.jumpHost', () => { const target = { ...makeTarget(), jumpHost: 'bastion.example.com' } expect(resolveEffectiveProxy(target, null)).toEqual({ kind: 'jump-host', jumpHost: 'bastion.example.com' }) }) it('returns structured jump-host config for resolved proxyJump', () => { expect(resolveEffectiveProxy(makeTarget(), makeResolved({ proxyJump: 'jump.host' }))).toEqual({ kind: 'jump-host', jumpHost: 'jump.host' }) }) it('returns undefined when no proxy is configured', () => { expect(resolveEffectiveProxy(makeTarget(), null)).toBeUndefined() }) })