git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
278 lines
7.4 KiB
TypeScript
278 lines
7.4 KiB
TypeScript
/**
|
|
* Plugin Manager Unit Tests
|
|
*
|
|
* Tests for plugin discovery, lifecycle, and execution.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import {
|
|
PluginManager,
|
|
createPluginManager,
|
|
createPluginManifest,
|
|
PluginManifestSchema,
|
|
DEFAULT_PLUGIN_CONFIG,
|
|
type PluginInstance,
|
|
type PluginManifest,
|
|
} from '../../../src/plugins/PluginManager.js';
|
|
|
|
describe('PluginManager', () => {
|
|
let manager: PluginManager;
|
|
|
|
beforeEach(() => {
|
|
manager = createPluginManager({
|
|
pluginsDir: './test-plugins',
|
|
autoLoad: false,
|
|
sandboxed: true,
|
|
});
|
|
});
|
|
|
|
describe('Configuration', () => {
|
|
it('should use default config values', () => {
|
|
const defaultManager = createPluginManager();
|
|
expect(DEFAULT_PLUGIN_CONFIG.pluginsDir).toBe('./plugins');
|
|
expect(DEFAULT_PLUGIN_CONFIG.autoLoad).toBe(true);
|
|
expect(DEFAULT_PLUGIN_CONFIG.maxPlugins).toBe(50);
|
|
});
|
|
|
|
it('should override config values', () => {
|
|
const customManager = createPluginManager({
|
|
pluginsDir: './custom-plugins',
|
|
maxPlugins: 10,
|
|
});
|
|
expect(customManager).toBeInstanceOf(PluginManager);
|
|
});
|
|
});
|
|
|
|
describe('Plugin Manifest', () => {
|
|
it('should validate valid manifest', () => {
|
|
const manifest = createPluginManifest({
|
|
name: 'test-plugin',
|
|
version: '1.0.0',
|
|
description: 'A test plugin',
|
|
});
|
|
|
|
expect(manifest.name).toBe('test-plugin');
|
|
expect(manifest.version).toBe('1.0.0');
|
|
expect(manifest.license).toBe('MIT');
|
|
});
|
|
|
|
it('should reject invalid manifest', () => {
|
|
expect(() => {
|
|
PluginManifestSchema.parse({
|
|
name: '', // Invalid: empty name
|
|
version: 'invalid', // Invalid: not semver
|
|
});
|
|
}).toThrow();
|
|
});
|
|
|
|
it('should set default values', () => {
|
|
const manifest = createPluginManifest({
|
|
name: 'minimal',
|
|
version: '1.0.0',
|
|
description: 'Minimal plugin',
|
|
});
|
|
|
|
expect(manifest.main).toBe('index.js');
|
|
expect(manifest.permissions).toEqual([]);
|
|
expect(manifest.keywords).toEqual([]);
|
|
});
|
|
|
|
it('should accept permissions', () => {
|
|
const manifest = createPluginManifest({
|
|
name: 'with-permissions',
|
|
version: '1.0.0',
|
|
description: 'Plugin with permissions',
|
|
permissions: ['memory:read', 'llm:invoke'],
|
|
});
|
|
|
|
expect(manifest.permissions).toContain('memory:read');
|
|
expect(manifest.permissions).toContain('llm:invoke');
|
|
});
|
|
});
|
|
|
|
describe('Plugin Listing', () => {
|
|
it('should return empty list initially', () => {
|
|
const plugins = manager.listPlugins();
|
|
expect(plugins).toEqual([]);
|
|
});
|
|
|
|
it('should return undefined for non-existent plugin', () => {
|
|
const plugin = manager.getPlugin('non-existent');
|
|
expect(plugin).toBeUndefined();
|
|
});
|
|
|
|
it('should filter enabled plugins', () => {
|
|
const enabled = manager.getEnabledPlugins();
|
|
expect(enabled).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('Plugin Skills', () => {
|
|
it('should return empty skills list', () => {
|
|
const skills = manager.getPluginSkills();
|
|
expect(skills).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('Plugin Commands', () => {
|
|
it('should return empty commands list', () => {
|
|
const commands = manager.getPluginCommands();
|
|
expect(commands).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('Message Dispatch', () => {
|
|
it('should return null when no plugins handle message', async () => {
|
|
const response = await manager.dispatchMessage({
|
|
content: 'Hello',
|
|
userId: 'user-123',
|
|
});
|
|
expect(response).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Skill Invocation', () => {
|
|
it('should throw when skill not found', async () => {
|
|
await expect(
|
|
manager.invokeSkill('non-existent-skill', {})
|
|
).rejects.toThrow('Skill non-existent-skill not found');
|
|
});
|
|
});
|
|
|
|
describe('Events', () => {
|
|
it('should emit events', () => {
|
|
const loadHandler = vi.fn();
|
|
const errorHandler = vi.fn();
|
|
|
|
manager.on('plugin:loaded', loadHandler);
|
|
manager.on('plugin:error', errorHandler);
|
|
|
|
// Events would be emitted during plugin loading
|
|
expect(manager.listenerCount('plugin:loaded')).toBe(1);
|
|
expect(manager.listenerCount('plugin:error')).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Registry Search', () => {
|
|
it('should return empty array without IPFS gateway', async () => {
|
|
const managerWithoutIPFS = createPluginManager({
|
|
ipfsGateway: undefined,
|
|
});
|
|
const results = await managerWithoutIPFS.searchRegistry('test');
|
|
expect(results).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('Registry Install', () => {
|
|
it('should throw without IPFS gateway', async () => {
|
|
const managerWithoutIPFS = createPluginManager({
|
|
ipfsGateway: undefined,
|
|
});
|
|
await expect(
|
|
managerWithoutIPFS.installFromRegistry('test-plugin')
|
|
).rejects.toThrow('IPFS gateway not configured');
|
|
});
|
|
});
|
|
|
|
describe('Plugin Enable/Disable', () => {
|
|
it('should return false when plugin not found', async () => {
|
|
const result = await manager.enablePlugin('non-existent');
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when disabling non-existent plugin', async () => {
|
|
const result = await manager.disablePlugin('non-existent');
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Plugin Unload', () => {
|
|
it('should return false when plugin not found', async () => {
|
|
const result = await manager.unloadPlugin('non-existent');
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Max Plugins Limit', () => {
|
|
it('should enforce max plugins config', () => {
|
|
const limitedManager = createPluginManager({
|
|
maxPlugins: 5,
|
|
});
|
|
expect(limitedManager).toBeInstanceOf(PluginManager);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Plugin Manifest Validation', () => {
|
|
it('should validate name length', () => {
|
|
expect(() => {
|
|
PluginManifestSchema.parse({
|
|
name: 'a'.repeat(100), // Too long
|
|
version: '1.0.0',
|
|
description: 'Test',
|
|
});
|
|
}).toThrow();
|
|
});
|
|
|
|
it('should validate semver format', () => {
|
|
const validVersions = ['1.0.0', '0.1.0', '10.20.30', '1.0.0-alpha'];
|
|
const invalidVersions = ['1', '1.0', 'v1.0.0', 'latest'];
|
|
|
|
validVersions.forEach(version => {
|
|
expect(() => {
|
|
PluginManifestSchema.parse({
|
|
name: 'test',
|
|
version,
|
|
description: 'Test',
|
|
});
|
|
}).not.toThrow();
|
|
});
|
|
|
|
invalidVersions.forEach(version => {
|
|
expect(() => {
|
|
PluginManifestSchema.parse({
|
|
name: 'test',
|
|
version,
|
|
description: 'Test',
|
|
});
|
|
}).toThrow();
|
|
});
|
|
});
|
|
|
|
it('should validate permission values', () => {
|
|
expect(() => {
|
|
PluginManifestSchema.parse({
|
|
name: 'test',
|
|
version: '1.0.0',
|
|
description: 'Test',
|
|
permissions: ['invalid:permission'],
|
|
});
|
|
}).toThrow();
|
|
});
|
|
|
|
it('should accept all valid permissions', () => {
|
|
const validPermissions = [
|
|
'memory:read',
|
|
'memory:write',
|
|
'session:read',
|
|
'session:write',
|
|
'skill:register',
|
|
'skill:invoke',
|
|
'llm:invoke',
|
|
'http:outbound',
|
|
'fs:read',
|
|
'fs:write',
|
|
'env:read',
|
|
];
|
|
|
|
expect(() => {
|
|
PluginManifestSchema.parse({
|
|
name: 'test',
|
|
version: '1.0.0',
|
|
description: 'Test',
|
|
permissions: validPermissions,
|
|
});
|
|
}).not.toThrow();
|
|
});
|
|
});
|