import { afterEach, describe, expect, it, vi } from 'vitest' import { clearForwardIntent, forgetForwardIntent, forwardIntentKey, listForwardIntent, rememberForwardIntent, resetForwardIntentForTests, restoreForwardIntent, tunnelCoversRule, } from '../../../src/components/connection/forwardIntent' import type { ForwardRule } from '../../../src/components/connection/forwardRules' import type { SSHForwardStatus } from '../../../shared/types/ipc.types' const localRule: ForwardRule = { id: 'pf-local', type: 'local', name: 'db', localPort: 3306, remoteHost: '127.0.0.1', remotePort: 3306, } function tunnel(overrides: Partial = {}): SSHForwardStatus { return { forwardId: 'pf-local', connectionId: 'conn-1', kind: 'local', bindHost: '127.0.0.1', boundPort: 3306, startedAt: 1, running: true, activeConnections: 0, bytesUp: 0, bytesDown: 0, ...overrides, } } afterEach(() => { resetForwardIntentForTests() }) describe('forward reconnect intent', () => { it('keys by session so a new connection id does not drop remembered rules', () => { expect(forwardIntentKey('sess-1', 'tab-1')).toBe('session:sess-1') expect(forwardIntentKey(undefined, 'tab-1')).toBe('tab:tab-1') }) it('remembers start intent and forgets explicit stop', () => { const key = forwardIntentKey('sess-1', 'tab-1') rememberForwardIntent(key, localRule) expect(listForwardIntent(key)).toEqual([localRule]) forgetForwardIntent(key, localRule.id) expect(listForwardIntent(key)).toEqual([]) }) it('matches a running tunnel by rule id or listen port after reconnect', () => { expect(tunnelCoversRule([tunnel()], localRule)).toBe(true) expect( tunnelCoversRule([tunnel({ forwardId: 'legacy', boundPort: 3306 })], localRule) ).toBe(true) expect( tunnelCoversRule([tunnel({ running: false })], localRule) ).toBe(false) }) it('matches remote forwards by remote listen port, not local target port', () => { const remoteRule: ForwardRule = { id: 'pf-remote', type: 'remote', name: 'expose', localPort: 8080, remoteHost: '127.0.0.1', remotePort: 18080, } expect( tunnelCoversRule( [tunnel({ forwardId: 'x', kind: 'remote', boundPort: 18080 })], remoteRule ) ).toBe(true) expect( tunnelCoversRule( [tunnel({ forwardId: 'x', kind: 'remote', boundPort: 8080 })], remoteRule ) ).toBe(false) }) it('clears remembered intent when a tab is disposed', () => { const key = forwardIntentKey('sess-1', 'tab-1') rememberForwardIntent(key, localRule) clearForwardIntent(key) expect(listForwardIntent(key)).toEqual([]) }) it('restores only rules that are not already running', async () => { const key = forwardIntentKey('sess-1', 'tab-1') rememberForwardIntent(key, localRule) rememberForwardIntent(key, { ...localRule, id: 'pf-other', localPort: 8080 }) const start = vi.fn().mockResolvedValue(true) const restored = await restoreForwardIntent( key, (rule) => rule.id === 'pf-local', start ) expect(restored).toBe(1) expect(start).toHaveBeenCalledTimes(1) expect(start).toHaveBeenCalledWith(expect.objectContaining({ id: 'pf-other' })) }) it('shares one restore pass when StatusBar and PortForwardView race', async () => { const key = forwardIntentKey('sess-1', 'tab-1') rememberForwardIntent(key, localRule) let release!: (value: boolean) => void const start = vi.fn( () => new Promise((resolve) => { release = resolve }) ) const first = restoreForwardIntent(key, () => false, start) const second = restoreForwardIntent(key, () => false, start) release(true) await expect(first).resolves.toBe(1) await expect(second).resolves.toBe(1) expect(start).toHaveBeenCalledTimes(1) }) })