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:
218
packages/plugin-sdk/src/index.test.ts
Normal file
218
packages/plugin-sdk/src/index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user