import { describe, expect, it, vi } from 'vitest'; import { PanelBridge, type PanelRequest, type PanelResponse, type PortLike } from '../../src/background/bridge.js'; import type { TransportStatus, VeloxTransport } from '../../src/background/transport/index.js'; function fakePort(): PortLike & { received: PanelResponse[]; emit(msg: PanelRequest): void; close(): void } { const msgListeners = new Set<(msg: PanelRequest) => void>(); const discListeners = new Set<() => void>(); const received: PanelResponse[] = []; return { name: 'velox-panel', received, postMessage: (m) => received.push(m), onMessage: { addListener: (cb) => msgListeners.add(cb) }, onDisconnect: { addListener: (cb) => discListeners.add(cb) }, emit: (msg) => msgListeners.forEach((cb) => cb(msg)), close: () => discListeners.forEach((cb) => cb()), }; } function fakeTransport(status: TransportStatus): VeloxTransport & { fireStatus(s: TransportStatus): void; calls: unknown[] } { const stateListeners = new Set<(s: TransportStatus) => void>(); const eventListeners = new Map void>>(); const calls: unknown[] = []; return { kind: 'ws', state: status.state, status, calls, connect: async () => undefined, disconnect: () => undefined, call: (async (method: string, params: unknown) => { calls.push({ method, params }); if (method === 'boom') throw Object.assign(new Error('nope'), { code: -32010 }); return { echoed: params }; }) as VeloxTransport['call'], on: ((event: string, cb: (p: unknown) => void) => { let s = eventListeners.get(event); if (!s) eventListeners.set(event, (s = new Set())); s.add(cb); }) as VeloxTransport['on'], off: (event: string, cb: (p: unknown) => void) => { eventListeners.get(event)?.delete(cb); }, onStateChange: (cb: (s: TransportStatus) => void) => { stateListeners.add(cb); return () => stateListeners.delete(cb); }, fireStatus: (s: TransportStatus) => stateListeners.forEach((cb) => cb(s)), } as unknown as VeloxTransport & { fireStatus(s: TransportStatus): void; calls: unknown[] }; } const CONNECTED: TransportStatus = { state: 'connected', needsPairing: false, fatal: null, retryAfterSec: null, sessionId: 's1', daemonVersion: '1.0.0', capabilities: [], }; describe('PanelBridge', () => { it('sends the current status on connect', () => { const t = fakeTransport(CONNECTED); const bridge = new PanelBridge({ getTransport: () => t, setOverride: async () => undefined }); const port = fakePort(); bridge.attach({ addListener: (cb) => cb(port) }); expect(port.received[0]).toEqual({ type: 'status', status: { ...CONNECTED, kind: 'ws' } }); }); it('forwards a call and relays the result', async () => { const t = fakeTransport(CONNECTED); const bridge = new PanelBridge({ getTransport: () => t, setOverride: async () => undefined }); const port = fakePort(); bridge.attach({ addListener: (cb) => cb(port) }); port.emit({ type: 'call', id: 7, method: 'download.list', params: {} }); await vi.waitFor(() => expect(port.received.some((m) => m.type === 'result')).toBe(true)); const result = port.received.find((m) => m.type === 'result'); expect(result).toEqual({ type: 'result', id: 7, ok: true, result: { echoed: {} } }); }); it('relays a call error with its code', async () => { const t = fakeTransport(CONNECTED); const bridge = new PanelBridge({ getTransport: () => t, setOverride: async () => undefined }); const port = fakePort(); bridge.attach({ addListener: (cb) => cb(port) }); port.emit({ type: 'call', id: 1, method: 'boom' as never, params: {} }); await vi.waitFor(() => expect(port.received.some((m) => m.type === 'result')).toBe(true)); const result = port.received.find((m) => m.type === 'result'); expect(result).toEqual({ type: 'result', id: 1, ok: false, error: { code: -32010, message: 'nope' } }); }); it('relays a call error when there is no transport yet', async () => { const bridge = new PanelBridge({ getTransport: () => undefined, setOverride: async () => undefined }); const port = fakePort(); bridge.attach({ addListener: (cb) => cb(port) }); port.emit({ type: 'call', id: 2, method: 'download.list', params: {} }); await vi.waitFor(() => expect(port.received.some((m) => m.type === 'result')).toBe(true)); const result = port.received.find((m) => m.type === 'result'); expect(result).toEqual({ type: 'result', id: 2, ok: false, error: { message: 'transport not ready' } }); }); it('a disconnected port stops receiving after disconnect', () => { const t = fakeTransport(CONNECTED); const bridge = new PanelBridge({ getTransport: () => t, setOverride: async () => undefined }); const port = fakePort(); bridge.attach({ addListener: (cb) => cb(port) }); port.close(); expect(() => port.emit({ type: 'getStatus' })).not.toThrow(); }); it('relays event.speed.global to the port after subscribing', () => { const stateListeners = new Set<(s: TransportStatus) => void>(); const eventCbs = new Map void>(); const t: VeloxTransport = { kind: 'ws', state: 'connected', status: CONNECTED, connect: async () => undefined, disconnect: () => undefined, call: (async () => ({})) as VeloxTransport['call'], on: ((event: string, cb: (p: unknown) => void) => { eventCbs.set(event, cb); }) as VeloxTransport['on'], off: () => undefined, onStateChange: (cb) => { stateListeners.add(cb); return () => stateListeners.delete(cb); }, }; const bridge = new PanelBridge({ getTransport: () => t, setOverride: async () => undefined }); const port = fakePort(); bridge.attach({ addListener: (cb) => cb(port) }); port.emit({ type: 'subscribe', events: ['event.speed.global'] }); eventCbs.get('event.speed.global')?.({ bytesPerSec: 4096 }); expect(port.received).toContainEqual({ type: 'event', event: 'event.speed.global', payload: { bytesPerSec: 4096 } }); }); it('pairs with a code and reports the resulting status', async () => { const pairWithCode = vi.fn(async () => undefined); const t = { ...fakeTransport(CONNECTED), pairWithCode }; const bridge = new PanelBridge({ getTransport: () => t, setOverride: async () => undefined }); const port = fakePort(); bridge.attach({ addListener: (cb) => cb(port) }); port.emit({ type: 'pair', code: '4821' }); await vi.waitFor(() => expect(pairWithCode).toHaveBeenCalledWith('4821')); }); it('reports a pairError when the transport has no pairWithCode (e.g. native transport)', async () => { const t = fakeTransport(CONNECTED); // no pairWithCode const bridge = new PanelBridge({ getTransport: () => t, setOverride: async () => undefined }); const port = fakePort(); bridge.attach({ addListener: (cb) => cb(port) }); port.emit({ type: 'pair', code: '4821' }); await vi.waitFor(() => expect(port.received.some((m) => m.type === 'pairError')).toBe(true)); }); it('unpairs via the transport and reports status', async () => { const unpair = vi.fn(async () => undefined); const t = { ...fakeTransport(CONNECTED), unpair }; const bridge = new PanelBridge({ getTransport: () => t, setOverride: async () => undefined }); const port = fakePort(); bridge.attach({ addListener: (cb) => cb(port) }); port.emit({ type: 'unpair' }); await vi.waitFor(() => expect(unpair).toHaveBeenCalled()); }); it('delegates setOverride to the deps and reports the new status', async () => { const setOverride = vi.fn(async () => undefined); const t = fakeTransport(CONNECTED); const bridge = new PanelBridge({ getTransport: () => t, setOverride }); const port = fakePort(); bridge.attach({ addListener: (cb) => cb(port) }); port.emit({ type: 'setOverride', override: 'uds' }); await vi.waitFor(() => expect(setOverride).toHaveBeenCalledWith('uds')); }); it('getStatus answers with the disconnected sentinel when there is no transport', () => { const bridge = new PanelBridge({ getTransport: () => undefined, setOverride: async () => undefined }); const port = fakePort(); bridge.attach({ addListener: (cb) => cb(port) }); port.received.length = 0; port.emit({ type: 'getStatus' }); expect(port.received[0]).toEqual({ type: 'status', status: { state: 'disconnected', needsPairing: false, fatal: null, retryAfterSec: null, sessionId: null, daemonVersion: null, capabilities: [], kind: null }, }); }); });