import { describe, expect, it, vi } from 'vitest'; import { createHostBridge, type CapabilityViolation } from './host'; import { createRpcEndpoint, type RpcMessage, type RpcTransport } from './rpc'; /** Two directly-wired in-memory transports (host side, plugin side). */ function transportPair(): [RpcTransport, RpcTransport] { const toA: Array<(message: RpcMessage) => void> = []; const toB: Array<(message: RpcMessage) => void> = []; const a: RpcTransport = { post: (message) => queueMicrotask(() => toB.forEach((listener) => listener(message))), listen: (onMessage) => { toA.push(onMessage); return () => toA.splice(toA.indexOf(onMessage), 1); }, }; const b: RpcTransport = { post: (message) => queueMicrotask(() => toA.forEach((listener) => listener(message))), listen: (onMessage) => { toB.push(onMessage); return () => toB.splice(toB.indexOf(onMessage), 1); }, }; return [a, b]; } describe('createHostBridge gating', () => { it('answers declared capabilities, rejects undeclared ones, and reports violations', async () => { const [hostSide, pluginSide] = transportPair(); const violations: CapabilityViolation[] = []; const getOutline = vi.fn().mockResolvedValue([{ id: 'h1', level: 1, text: 'Hello' }]); const bridge = createHostBridge({ manifest: { permissions: ['readCurrentPage'] }, capabilities: { getOutline, listPages: vi.fn() }, transport: hostSide, onViolation: (violation) => violations.push(violation), }); const plugin = createRpcEndpoint({ ...pluginSide, timeoutMs: 500 }); // Declared capability → answered by the host implementation. await expect(plugin.request('getOutline')).resolves.toEqual([ { id: 'h1', level: 1, text: 'Hello' }, ]); // Undeclared capability → rejected before the implementation, and logged. await expect(plugin.request('listPages')).rejects.toMatchObject({ code: 'capability_not_permitted', }); expect(violations).toEqual([ { method: 'listPages', capability: 'readPond', code: 'capability_not_permitted' }, ]); // A method outside the v1 surface → unknown_method (no handler installed). await expect(plugin.request('formatHardDrive')).rejects.toMatchObject({ code: 'unknown_method', }); bridge.dispose(); plugin.dispose(); }); it('refuses to invoke non-lifecycle methods on the plugin', async () => { const [hostSide] = transportPair(); const bridge = createHostBridge({ manifest: { permissions: [] }, capabilities: {}, transport: hostSide, }); await expect( bridge.invoke('getOutline' as never, undefined, { timeoutMs: 100 }), ).rejects.toMatchObject({ code: 'unknown_method' }); bridge.dispose(); }); });