238 lines
7.4 KiB
TypeScript
238 lines
7.4 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|