This commit is contained in:
237
packages/server/tests/plugin-autoload.test.ts
Normal file
237
packages/server/tests/plugin-autoload.test.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { MindDB, FrameStore, SessionStore } from '@waggle/core';
|
||||
import { buildLocalServer } from '../src/local/index.js';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
/**
|
||||
* Plugin auto-load on startup tests.
|
||||
*
|
||||
* Verifies that the server scans ~/.waggle/plugins/ at boot and loads
|
||||
* plugins with valid plugin.json manifests into the PluginRuntimeManager.
|
||||
*/
|
||||
|
||||
function createTmpDataDir(): string {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-plugin-autoload-'));
|
||||
// Create personal.mind so the server can boot
|
||||
const personalPath = path.join(tmpDir, 'personal.mind');
|
||||
const mind = new MindDB(personalPath);
|
||||
const sessions = new SessionStore(mind);
|
||||
const frames = new FrameStore(mind);
|
||||
const s = sessions.create('boot-test');
|
||||
frames.createIFrame(s.gop_id, 'boot test', 'normal');
|
||||
mind.close();
|
||||
return tmpDir;
|
||||
}
|
||||
|
||||
describe('Plugin auto-load on startup', () => {
|
||||
describe('no plugins directory', () => {
|
||||
let server: FastifyInstance;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = createTmpDataDir();
|
||||
// Deliberately do NOT create plugins/ dir
|
||||
server = await buildLocalServer({ dataDir: tmpDir });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('starts without error when plugins directory does not exist', () => {
|
||||
expect(server).toBeDefined();
|
||||
});
|
||||
|
||||
it('has zero active plugins', () => {
|
||||
const active = server.agentState.pluginRuntimeManager.getActive();
|
||||
expect(active).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty plugins directory', () => {
|
||||
let server: FastifyInstance;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = createTmpDataDir();
|
||||
fs.mkdirSync(path.join(tmpDir, 'plugins'), { recursive: true });
|
||||
server = await buildLocalServer({ dataDir: tmpDir });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('starts without error with empty plugins directory', () => {
|
||||
expect(server).toBeDefined();
|
||||
});
|
||||
|
||||
it('has zero active plugins', () => {
|
||||
const active = server.agentState.pluginRuntimeManager.getActive();
|
||||
expect(active).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid plugin with plugin.json', () => {
|
||||
let server: FastifyInstance;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = createTmpDataDir();
|
||||
const pluginDir = path.join(tmpDir, 'plugins', 'test-greeter');
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pluginDir, 'plugin.json'), JSON.stringify({
|
||||
name: 'test-greeter',
|
||||
version: '1.0.0',
|
||||
description: 'A test greeting plugin',
|
||||
skills: ['greet'],
|
||||
tools: [
|
||||
{
|
||||
name: 'greet',
|
||||
description: 'Greet a user',
|
||||
parameters: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
||||
},
|
||||
],
|
||||
}));
|
||||
server = await buildLocalServer({ dataDir: tmpDir });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('loads the plugin and marks it active', () => {
|
||||
const active = server.agentState.pluginRuntimeManager.getActive();
|
||||
expect(active).toHaveLength(1);
|
||||
expect(active[0].getManifest().name).toBe('test-greeter');
|
||||
});
|
||||
|
||||
it('registers plugin tools', () => {
|
||||
const tools = server.agentState.pluginRuntimeManager.getAllTools();
|
||||
expect(tools).toHaveLength(1);
|
||||
expect(tools[0].name).toBe('greet');
|
||||
});
|
||||
|
||||
it('registers plugin skills', () => {
|
||||
const skills = server.agentState.pluginRuntimeManager.getAllSkills();
|
||||
expect(skills).toContain('greet');
|
||||
});
|
||||
});
|
||||
|
||||
describe('directory without plugin.json is skipped', () => {
|
||||
let server: FastifyInstance;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = createTmpDataDir();
|
||||
// Create a plugin dir with no manifest
|
||||
const noManifest = path.join(tmpDir, 'plugins', 'broken-plugin');
|
||||
fs.mkdirSync(noManifest, { recursive: true });
|
||||
fs.writeFileSync(path.join(noManifest, 'README.md'), '# not a plugin');
|
||||
server = await buildLocalServer({ dataDir: tmpDir });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('starts without error', () => {
|
||||
expect(server).toBeDefined();
|
||||
});
|
||||
|
||||
it('has zero active plugins', () => {
|
||||
const active = server.agentState.pluginRuntimeManager.getActive();
|
||||
expect(active).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid manifest is skipped gracefully', () => {
|
||||
let server: FastifyInstance;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = createTmpDataDir();
|
||||
const pluginDir = path.join(tmpDir, 'plugins', 'bad-manifest');
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
// Missing required 'version' and 'description' fields
|
||||
fs.writeFileSync(path.join(pluginDir, 'plugin.json'), JSON.stringify({
|
||||
name: 'bad-manifest',
|
||||
}));
|
||||
server = await buildLocalServer({ dataDir: tmpDir });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('starts without error', () => {
|
||||
expect(server).toBeDefined();
|
||||
});
|
||||
|
||||
it('does not load the invalid plugin', () => {
|
||||
const active = server.agentState.pluginRuntimeManager.getActive();
|
||||
expect(active).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple plugins — valid + invalid mix', () => {
|
||||
let server: FastifyInstance;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = createTmpDataDir();
|
||||
const pluginsBase = path.join(tmpDir, 'plugins');
|
||||
|
||||
// Valid plugin
|
||||
const good = path.join(pluginsBase, 'good-plugin');
|
||||
fs.mkdirSync(good, { recursive: true });
|
||||
fs.writeFileSync(path.join(good, 'plugin.json'), JSON.stringify({
|
||||
name: 'good-plugin',
|
||||
version: '2.0.0',
|
||||
description: 'A good plugin',
|
||||
}));
|
||||
|
||||
// Invalid plugin (malformed JSON)
|
||||
const bad = path.join(pluginsBase, 'malformed');
|
||||
fs.mkdirSync(bad, { recursive: true });
|
||||
fs.writeFileSync(path.join(bad, 'plugin.json'), '{ not valid json');
|
||||
|
||||
// Another valid plugin
|
||||
const good2 = path.join(pluginsBase, 'another-good');
|
||||
fs.mkdirSync(good2, { recursive: true });
|
||||
fs.writeFileSync(path.join(good2, 'plugin.json'), JSON.stringify({
|
||||
name: 'another-good',
|
||||
version: '1.0.0',
|
||||
description: 'Another good plugin',
|
||||
skills: ['analyze'],
|
||||
}));
|
||||
|
||||
server = await buildLocalServer({ dataDir: tmpDir });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('loads only the valid plugins', () => {
|
||||
const active = server.agentState.pluginRuntimeManager.getActive();
|
||||
expect(active).toHaveLength(2);
|
||||
const names = active.map(p => p.getManifest().name).sort();
|
||||
expect(names).toEqual(['another-good', 'good-plugin']);
|
||||
});
|
||||
|
||||
it('aggregates skills from all loaded plugins', () => {
|
||||
const skills = server.agentState.pluginRuntimeManager.getAllSkills();
|
||||
expect(skills).toContain('analyze');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user