// Tests for Electrobun plugin-store — pure logic. // Uses bun:test. Tests command registry, event bus, and permission validation. import { describe, it, expect, beforeEach } from 'bun:test'; // ── Replicated types ────────────────────────────────────────────────────────── interface PluginMeta { id: string; name: string; version: string; description: string; main: string; permissions: string[]; allowedOrigins?: string[]; maxRuntime?: number; } interface PluginCommand { pluginId: string; label: string; callback: () => void; } // ── Replicated event bus ──────────────────────────────────────────────────── type EventHandler = (data: unknown) => void; function createEventBus() { const listeners = new Map>(); return { on(event: string, handler: EventHandler): void { let set = listeners.get(event); if (!set) { set = new Set(); listeners.set(event, set); } set.add(handler); }, off(event: string, handler: EventHandler): void { listeners.get(event)?.delete(handler); }, emit(event: string, data: unknown): void { const set = listeners.get(event); if (!set) return; for (const handler of set) { try { handler(data); } catch { /* swallow */ } } }, listenerCount(event: string): number { return listeners.get(event)?.size ?? 0; }, }; } // ── Replicated command registry ────────────────────────────────────────────── function createCommandRegistry() { let commands: PluginCommand[] = []; return { add(pluginId: string, label: string, callback: () => void): void { commands = [...commands, { pluginId, label, callback }]; }, remove(pluginId: string): void { commands = commands.filter(c => c.pluginId !== pluginId); }, getAll(): PluginCommand[] { return commands; }, getByPlugin(pluginId: string): PluginCommand[] { return commands.filter(c => c.pluginId === pluginId); }, }; } // ── Permission validation (replicated from plugin-host.ts) ────────────────── const VALID_PERMISSIONS = new Set(['palette', 'notifications', 'messages', 'events', 'network']); function validatePermissions(perms: string[]): { valid: boolean; invalid: string[] } { const invalid = perms.filter(p => !VALID_PERMISSIONS.has(p)); return { valid: invalid.length === 0, invalid }; } // ── Tests ─────────────────────────────────────────────────────────────────── describe('command registry', () => { let registry: ReturnType; beforeEach(() => { registry = createCommandRegistry(); }); it('starts empty', () => { expect(registry.getAll()).toHaveLength(0); }); it('add registers a command', () => { registry.add('my-plugin', 'Say Hello', () => {}); expect(registry.getAll()).toHaveLength(1); expect(registry.getAll()[0].label).toBe('Say Hello'); expect(registry.getAll()[0].pluginId).toBe('my-plugin'); }); it('remove clears commands for a plugin', () => { registry.add('p1', 'Cmd A', () => {}); registry.add('p1', 'Cmd B', () => {}); registry.add('p2', 'Cmd C', () => {}); registry.remove('p1'); expect(registry.getAll()).toHaveLength(1); expect(registry.getAll()[0].pluginId).toBe('p2'); }); it('getByPlugin filters correctly', () => { registry.add('p1', 'A', () => {}); registry.add('p2', 'B', () => {}); registry.add('p1', 'C', () => {}); expect(registry.getByPlugin('p1')).toHaveLength(2); expect(registry.getByPlugin('p2')).toHaveLength(1); expect(registry.getByPlugin('p3')).toHaveLength(0); }); }); describe('event bus', () => { let bus: ReturnType; beforeEach(() => { bus = createEventBus(); }); it('on registers listener', () => { bus.on('test', () => {}); expect(bus.listenerCount('test')).toBe(1); }); it('emit calls registered handlers', () => { let received: unknown = null; bus.on('data', (d) => { received = d; }); bus.emit('data', { value: 42 }); expect(received).toEqual({ value: 42 }); }); it('off removes listener', () => { const handler = () => {}; bus.on('event', handler); bus.off('event', handler); expect(bus.listenerCount('event')).toBe(0); }); it('emit does not throw for unregistered events', () => { expect(() => bus.emit('nonexistent', null)).not.toThrow(); }); it('handler error is swallowed', () => { bus.on('crash', () => { throw new Error('boom'); }); expect(() => bus.emit('crash', null)).not.toThrow(); }); }); describe('permission validation', () => { it('accepts all valid permissions', () => { const result = validatePermissions(['palette', 'notifications', 'messages', 'events', 'network']); expect(result.valid).toBe(true); expect(result.invalid).toHaveLength(0); }); it('rejects unknown permissions', () => { const result = validatePermissions(['palette', 'filesystem', 'exec']); expect(result.valid).toBe(false); expect(result.invalid).toEqual(['filesystem', 'exec']); }); it('accepts empty permissions list', () => { const result = validatePermissions([]); expect(result.valid).toBe(true); }); }); describe('plugin meta validation', () => { it('maxRuntime defaults to 30 seconds', () => { const meta: PluginMeta = { id: 'test', name: 'Test', version: '1.0', description: 'desc', main: 'index.js', permissions: [], }; const maxRuntime = (meta.maxRuntime ?? 30) * 1000; expect(maxRuntime).toBe(30_000); }); it('allowedOrigins defaults to empty array', () => { const meta: PluginMeta = { id: 'test', name: 'Test', version: '1.0', description: 'desc', main: 'index.js', permissions: ['network'], }; expect(meta.allowedOrigins ?? []).toEqual([]); }); });