import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { createShade, type Shade } from '@shade/sdk'; import { createPrekeyServer, MemoryPrekeyStore, PrekeyServerEvents } from '@shade/server'; import { SubtleCryptoProvider } from '@shade/crypto-web'; import type { FileEntry } from '../../src/index.js'; const crypto = new SubtleCryptoProvider(); /** * End-to-end test of `Shade.files` — the high-level SDK entrypoint. * Verifies that `shade.files.serve(...)` and `shade.files.client(peer)` * compose correctly and share a single channel + bridges per Shade. */ describe('Shade.files namespace — end-to-end via SDK getter', () => { let alice: Shade; let bob: Shade; let prekeyServer: { stop(): void }; let aliceServer: { stop(): void }; let bobServer: { stop(): void }; let stopBobFiles: (() => Promise) | null = null; beforeAll(async () => { const prekey = createPrekeyServer({ crypto, store: new MemoryPrekeyStore(), disableRateLimit: true, events: new PrekeyServerEvents(), }); prekeyServer = Bun.serve({ port: 0, fetch: prekey.fetch }); const prekeyUrl = `http://localhost:${(prekeyServer as unknown as { port: number }).port}`; alice = await createShade({ prekeyServer: prekeyUrl, address: 'alice' }); bob = await createShade({ prekeyServer: prekeyUrl, address: 'bob' }); let aliceUrl = ''; let bobUrl = ''; alice.configureTransfers({ resolveBaseUrl: async (peer) => { if (peer === 'bob') return bobUrl; throw new Error(`unknown peer: ${peer}`); }, }); bob.configureTransfers({ resolveBaseUrl: async (peer) => { if (peer === 'alice') return aliceUrl; throw new Error(`unknown peer: ${peer}`); }, }); const aliceApp = await alice.transferRoute(); const bobApp = await bob.transferRoute(); aliceServer = Bun.serve({ port: 0, fetch: aliceApp.fetch }); bobServer = Bun.serve({ port: 0, fetch: bobApp.fetch }); aliceUrl = `http://localhost:${(aliceServer as unknown as { port: number }).port}`; bobUrl = `http://localhost:${(bobServer as unknown as { port: number }).port}`; }); afterAll(async () => { if (stopBobFiles !== null) await stopBobFiles(); await alice.files.destroy(); await bob.files.destroy(); await alice.shutdown(); await bob.shutdown(); aliceServer.stop(); bobServer.stop(); prekeyServer.stop(); }); test('shade.files getter is memoized', () => { const a = bob.files; const b = bob.files; expect(a).toBe(b); }); test('serve() + client() round-trip stat through the SDK', async () => { stopBobFiles = await bob.files.serve({ stat: async (ctx) => { const e: FileEntry = { name: ctx.path.split('/').filter(Boolean).pop() ?? '', kind: 'file', size: 42, mtime: 1234, metadata: {}, }; return e; }, }); const fs = await alice.files.client('bob'); const result = await fs.stat('/answer.txt'); expect(result.name).toBe('answer.txt'); expect(result.size).toBe(42); expect(result.mtime).toBe(1234); }); test('second serve() throws (one handler per Shade)', async () => { await expect( bob.files.serve({ stat: async () => ({ name: 'x', kind: 'file', size: 0, mtime: 0, metadata: {} }) }), ).rejects.toThrow(/handler is already registered/); }); });