import { describe, test, expect, afterAll } from 'bun:test'; import { SubtleCryptoProvider } from '@shade/crypto-web'; import { sha256Once } from '@shade/streams'; import { TransferEngine, MemoryControlChannel, ShadeTransferHttpTransport, ShadeTransferWsTransport, FallbackTransferTransport, createTransferRoutes, type TransferHandle, type TransferResult, } from '../src/index.js'; const crypto = new SubtleCryptoProvider(); function hex(b: Uint8Array): string { return Array.from(b, (x) => x.toString(16).padStart(2, '0')).join(''); } const cleanups: Array<() => void> = []; afterAll(() => { for (const c of cleanups) c(); }); describe('WS opt-in transport with HTTP fallback', () => { test('WS connect failure → falls back to HTTP transparently', async () => { const { a: ctrlA, b: ctrlB } = MemoryControlChannel.linked('alice', 'bob'); const receiverEngine = new TransferEngine({ crypto, controlChannel: ctrlB, transport: { probe: async () => undefined, sendChunk: async () => { throw new Error('not used'); }, fetchResumeState: async () => null, }, myAddress: 'bob', }); const httpApp = await createTransferRoutes(receiverEngine); const httpPort = 23000 + Math.floor(Math.random() * 500); const httpServer = Bun.serve({ port: httpPort, fetch: httpApp.fetch }); const httpBaseUrl = `http://localhost:${httpPort}`; cleanups.push(() => httpServer.stop()); const ws = new ShadeTransferWsTransport({ // Resolve to a closed port — guaranteed connect failure. resolveWsUrl: async () => `ws://127.0.0.1:1`, connectTimeoutMs: 500, }); const http = new ShadeTransferHttpTransport({ resolveBaseUrl: async () => httpBaseUrl, authenticator: { signChunk: async () => ({ 'X-Shade-Sender-Address': 'alice' }), signControl: async () => ({ 'X-Shade-Sender-Address': 'alice' }), }, }); const fallback = new FallbackTransferTransport(ws, http); const senderEngine = new TransferEngine({ crypto, controlChannel: ctrlA, transport: fallback, myAddress: 'alice', }); cleanups.push(() => senderEngine.destroy()); cleanups.push(() => receiverEngine.destroy()); let resolveRecv!: (h: TransferHandle) => void; const recvHandlePromise = new Promise((r) => { resolveRecv = r; }); receiverEngine.onIncomingTransfer(async (incoming) => { const h = await incoming.accept({ output: { kind: 'buffer' } }); resolveRecv(h); }); const input = crypto.randomBytes(64 * 1024); const handle = await senderEngine.upload({ to: 'bob', input, lanes: 2, chunkSize: 8 * 1024, }); const recvHandle = await recvHandlePromise; const [senderResult, recvResult] = await Promise.all([ handle.done(), recvHandle.done(), ]); expect(fallback.fellBack).toBe(true); const received = (recvResult as TransferResult & { bytes?: Uint8Array }).bytes ?? new Uint8Array(); expect(received).toEqual(input); expect(senderResult.sha256).toBe(hex(sha256Once(input))); }); });