feat(plugins): add PluginLoader with manifest discovery and validation

Amit Haridas
This commit is contained in:
2026-04-23 22:55:12 +05:30
parent 36e26318cd
commit 54c9484cb5
2 changed files with 177 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
const path = require('path');
const fs = require('fs');
const os = require('os');
const { PluginLoader } = require('../src/plugins/plugin-loader');
describe('PluginLoader', () => {
let tempDir;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'plugin-test-'));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
function writeManifest(dir, manifest) {
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2));
}
test('discoverPlugins — finds manifests in built-in directory', () => {
const pluginDir = path.join(tempDir, 'my-plugin');
writeManifest(pluginDir, {
id: 'my-plugin',
name: 'My Plugin',
version: '1.0.0',
description: 'Test'
});
const loader = new PluginLoader([tempDir]);
const plugins = loader.discoverPlugins();
expect(plugins).toHaveLength(1);
expect(plugins[0].id).toBe('my-plugin');
});
test('discoverPlugins — skips directories without manifest.json', () => {
const emptyDir = path.join(tempDir, 'no-manifest');
fs.mkdirSync(emptyDir, { recursive: true });
const loader = new PluginLoader([emptyDir]);
const plugins = loader.discoverPlugins();
expect(plugins).toHaveLength(0);
});
test('validateManifest — accepts valid manifest', () => {
const manifest = {
id: 'test-plugin',
name: 'Test Plugin',
version: '1.0.0',
description: 'A test plugin'
};
const loader = new PluginLoader([]);
expect(loader.validateManifest(manifest)).toBe(true);
});
test('validateManifest — rejects manifest missing id', () => {
const manifest = { name: 'No ID', version: '1.0.0', description: 'x' };
const loader = new PluginLoader([]);
expect(() => loader.validateManifest(manifest)).toThrow(/id/);
});
test('validateManifest — rejects manifest missing name', () => {
const manifest = { id: 'test', version: '1.0.0', description: 'x' };
const loader = new PluginLoader([]);
expect(() => loader.validateManifest(manifest)).toThrow(/name/);
});
test('validateManifest — rejects manifest with duplicate id', () => {
const loader = new PluginLoader([]);
loader.loadedIds = new Set(['existing-plugin']);
expect(() => loader.validateManifest({ id: 'existing-plugin', name: 'Dup', version: '1.0.0', description: 'x' }))
.toThrow(/duplicate/i);
});
test('discoverPlugins — loads index.js Plugin export if present', () => {
const pluginDir = path.join(tempDir, 'with-index');
writeManifest(pluginDir, { id: 'with-index', name: 'With Index', version: '1.0.0', description: 'Test' });
// Write a simple index.js with a Plugin export
fs.writeFileSync(path.join(pluginDir, 'index.js'), `
class SimplePlugin { init() {} }
module.exports = { Plugin: SimplePlugin };
`);
const loader = new PluginLoader([tempDir]);
const plugins = loader.discoverPlugins();
expect(plugins).toHaveLength(1);
expect(plugins[0].PluginClass).toBeDefined();
expect(plugins[0].PluginClass.name).toBe('SimplePlugin');
});
test('discoverPlugins — continues if one plugin fails to load', () => {
const badDir = path.join(tempDir, 'bad');
writeManifest(badDir, { id: 'bad', name: 'Bad', version: '1.0.0', description: 'Broken' });
fs.writeFileSync(path.join(badDir, 'index.js'), 'throw new Error("broken");');
const goodDir = path.join(tempDir, 'good');
writeManifest(goodDir, { id: 'good', name: 'Good', version: '1.0.0', description: 'Works' });
const loader = new PluginLoader([tempDir]);
const plugins = loader.discoverPlugins();
expect(plugins).toHaveLength(1);
expect(plugins[0].id).toBe('good');
});
});