feat(FN-1111): merge fusion/fn-1111 (auto-resolved)

- docs(FN-1111): complete Step 8 - plugin system documentation
- feat(FN-1111): complete Step 7 - testing and build fixes
- feat(FN-1111): complete Step 6 - export plugin types from core
- feat(FN-1111): complete Step 5 - plugin SDK package
- feat(FN-1111): complete Step 4 - plugin loader with lifecycle management
- feat(FN-1111): complete Step 3 - plugin store with CRUD
- feat(FN-1111): complete Step 2 - add plugins table schema migration
- feat(FN-1111): complete Step 1 - plugin type definitions
This commit is contained in:
gsxdsm
2026-04-09 14:45:32 -07:00
parent cdc430598d
commit 8c5327a660
16 changed files with 3577 additions and 11 deletions

View File

@@ -0,0 +1,218 @@
import { describe, it, expect, beforeAll } from "vitest";
import { definePlugin } from "./index.js";
import type { FusionPlugin } from "../../core/src/plugin-types.js";
import { validatePluginManifest } from "../../core/src/plugin-types.js";
let validateFn: typeof validatePluginManifest;
beforeAll(async () => {
validateFn = validatePluginManifest;
});
describe("Plugin SDK", () => {
// ── definePlugin ────────────────────────────────────────────────────
describe("definePlugin", () => {
it("returns the input unchanged (identity function)", () => {
const plugin = {
manifest: {
id: "test-plugin",
name: "Test Plugin",
version: "1.0.0",
},
state: "installed" as const,
hooks: {},
tools: [],
routes: [],
};
const result = definePlugin(plugin);
expect(result).toBe(plugin);
});
it("provides type narrowing for plugin definitions", () => {
// This test verifies that definePlugin provides proper TypeScript inference
const plugin = definePlugin({
manifest: {
id: "my-plugin",
name: "My Plugin",
version: "1.0.0",
description: "A test plugin",
},
state: "installed",
hooks: {
onLoad: async (ctx) => {
ctx.logger.info("Loaded!");
},
},
tools: [
{
name: "my_tool",
description: "A useful tool",
parameters: {},
execute: async (params, ctx) => ({
content: [{ type: "text" as const, text: "Hello!" }],
}),
},
],
routes: [],
});
expect(plugin.manifest.id).toBe("my-plugin");
expect(plugin.manifest.name).toBe("My Plugin");
expect(plugin.hooks.onLoad).toBeDefined();
expect(plugin.tools).toHaveLength(1);
expect(plugin.tools![0].name).toBe("my_tool");
});
it("works with minimal plugin definition", () => {
const plugin = definePlugin({
manifest: {
id: "minimal",
name: "Minimal",
version: "0.1.0",
},
state: "installed",
hooks: {},
tools: [],
routes: [],
});
expect(plugin.manifest.id).toBe("minimal");
});
it("preserves all plugin properties", () => {
const plugin = definePlugin({
manifest: {
id: "full-plugin",
name: "Full Plugin",
version: "2.0.0",
description: "A full-featured plugin",
author: "Test Author",
homepage: "https://example.com",
fusionVersion: "1.0.0",
dependencies: ["other-plugin"],
settingsSchema: {
apiKey: {
type: "string" as const,
required: true,
},
},
},
state: "installed",
hooks: {
onLoad: async () => {},
onUnload: async () => {},
onTaskCreated: async () => {},
onTaskMoved: async () => {},
onTaskCompleted: async () => {},
onError: async () => {},
},
tools: [
{
name: "tool1",
description: "Tool 1",
parameters: {},
execute: async () => ({ content: [] }),
},
],
routes: [
{
method: "GET" as const,
path: "/status",
handler: async () => ({ status: "ok" }),
description: "Health check",
},
],
});
expect(plugin.manifest.id).toBe("full-plugin");
expect(plugin.manifest.description).toBe("A full-featured plugin");
expect(plugin.manifest.author).toBe("Test Author");
expect(plugin.manifest.homepage).toBe("https://example.com");
expect(plugin.manifest.fusionVersion).toBe("1.0.0");
expect(plugin.manifest.dependencies).toEqual(["other-plugin"]);
expect(plugin.manifest.settingsSchema).toBeDefined();
expect(plugin.hooks.onLoad).toBeDefined();
expect(plugin.hooks.onUnload).toBeDefined();
expect(plugin.hooks.onTaskCreated).toBeDefined();
expect(plugin.hooks.onTaskMoved).toBeDefined();
expect(plugin.hooks.onTaskCompleted).toBeDefined();
expect(plugin.hooks.onError).toBeDefined();
expect(plugin.tools).toHaveLength(1);
expect(plugin.routes).toHaveLength(1);
});
});
// ── Type exports ────────────────────────────────────────────────────
describe("type exports", () => {
it("exports PluginManifest type", () => {
// This is a compile-time test - if types are exported correctly, this will compile
const manifest: import("../../core/src/plugin-types.js").PluginManifest = {
id: "test",
name: "Test",
version: "1.0.0",
};
expect(manifest.id).toBe("test");
});
it("exports FusionPlugin type", () => {
const plugin: FusionPlugin = {
manifest: {
id: "test",
name: "Test",
version: "1.0.0",
},
state: "installed",
hooks: {},
tools: [],
routes: [],
};
expect(plugin.manifest.id).toBe("test");
});
it("exports PluginContext type", () => {
const ctx: import("../../core/src/plugin-types.js").PluginContext = {
pluginId: "test",
taskStore: {} as any,
settings: {},
logger: {
info: () => {},
warn: () => {},
error: () => {},
debug: () => {},
},
emitEvent: () => {},
};
expect(ctx.pluginId).toBe("test");
});
});
// ── validatePluginManifest ───────────────────────────────────────────
describe("validatePluginManifest", () => {
it("validates a valid manifest", () => {
const result = validateFn({
id: "valid-plugin",
name: "Valid Plugin",
version: "1.0.0",
});
expect(result.valid).toBe(true);
expect(result.errors).toEqual([]);
});
it("rejects invalid manifest", () => {
const result = validateFn({
id: "",
name: "",
version: "",
});
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
});
});