test(electrobun): workspace-store + plugin-store unit tests
This commit is contained in:
parent
e75f90407b
commit
c0eca4964a
2 changed files with 448 additions and 0 deletions
191
ui-electrobun/tests/unit/plugin-store.test.ts
Normal file
191
ui-electrobun/tests/unit/plugin-store.test.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
// 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<string, Set<EventHandler>>();
|
||||
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<typeof createCommandRegistry>;
|
||||
|
||||
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<typeof createEventBus>;
|
||||
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue