import { afterEach, describe, expect, it } from 'vitest'; import { RpcError, RpcTimeoutError, TransportClosedError, } from '../../src/background/transport/types.js'; import { NativeTransport } from '../../src/background/transport/native.js'; import { FakeNativePort, tick } from './helpers.js'; let transport: NativeTransport | undefined; afterEach(() => { transport?.disconnect(); transport = undefined; }); describe('NativeTransport', () => { it('completes session.hello over the port and connects', async () => { const port = new FakeNativePort(); transport = new NativeTransport({ connectNative: () => port }); await transport.connect(); expect(transport.state).toBe('connected'); expect(transport.kind).toBe('uds'); expect(transport.status.daemonVersion).toBe('1.2.3-nm'); expect(transport.status.capabilities).toEqual(['media']); expect((port.sent[0] as { method: string }).method).toBe('session.hello'); expect((port.sent[0] as { params: { token: unknown } }).params.token).toBeNull(); }); it('marks the transport fatal when the host is not installed', async () => { const port = new FakeNativePort({ autoHello: false }); transport = new NativeTransport({ connectNative: () => port, backoff: { baseMs: 10 } }); const p = transport.connect(); queueMicrotask(() => port.fireDisconnect('No such native application com.velox.host')); await expect(p).rejects.toBeInstanceOf(RpcError); expect(transport.status.fatal).toMatch(/not installed/); // fatal => no reconnect const calls = [] as unknown[]; const t2 = new NativeTransport({ connectNative: () => { calls.push(1); const fp = new FakeNativePort({ autoHello: false }); queueMicrotask(() => fp.fireDisconnect('No such native application')); return fp; }, backoff: { baseMs: 10 }, }); await t2.connect().catch(() => undefined); await tick(60); expect(calls).toHaveLength(1); t2.disconnect(); }); it('reconnects when an installed host drops during the handshake', async () => { let attempts = 0; transport = new NativeTransport({ connectNative: () => { attempts += 1; const fp = new FakeNativePort({ autoHello: attempts > 1 }); if (attempts === 1) queueMicrotask(() => fp.fireDisconnect('pipe closed')); return fp; }, backoff: { baseMs: 10, factor: 1, jitter: 0 }, }); await expect(transport.connect()).rejects.toBeInstanceOf(TransportClosedError); for (let i = 0; i < 40 && transport.state !== 'connected'; i += 1) await tick(10); expect(attempts).toBeGreaterThanOrEqual(2); expect(transport.state).toBe('connected'); }); it('treats a protocol-major mismatch as fatal', async () => { const port = new FakeNativePort({ helloError: { code: -32001, message: 'daemon speaks 9.x' } }); transport = new NativeTransport({ connectNative: () => port }); await expect(transport.connect()).rejects.toBeInstanceOf(RpcError); expect(transport.status.fatal).toMatch(/protocol mismatch/); }); it('forwards calls and fails them on a drop', async () => { const port = new FakeNativePort(); transport = new NativeTransport({ connectNative: () => port, backoff: { baseMs: 10_000 }, }); await transport.connect(); const pending = transport.call('download.list', { filter: null }, { timeoutMs: 5000 }); expect((port.sent.at(-1) as { method: string }).method).toBe('download.list'); port.fireDisconnect('host exited'); await expect(pending).rejects.toBeInstanceOf(TransportClosedError); expect(transport.state).toBe('disconnected'); }); it('times a call out when the host goes quiet', async () => { const port = new FakeNativePort(); transport = new NativeTransport({ connectNative: () => port }); await transport.connect(); await expect( transport.call('download.list', { filter: null }, { timeoutMs: 30 }), ).rejects.toBeInstanceOf(RpcTimeoutError); }); });