feat(FN-1469): create settings-loadable example plugin
- Add fusion-plugin-settings-demo package with settings schema (string, number, boolean, enum types) - Add lifecycle hooks (onLoad, onTaskCreated, onTaskCompleted) - Add tools (suggest_tags, status) with settings-driven behavior - Add comprehensive tests with 24 test cases - Add manifest.json for plugin metadata and README with Settings installation flow - Update PLUGIN_AUTHORING.md with new example and install instructions - Also includes: FN-1468 plugin wiring, FN-1456 abandon flow, FN-1429 always-green tests, FN-1440 Authentication in Settings, FN-1133 plugin hot-reload support
This commit is contained in:
138
plugins/examples/fusion-plugin-settings-demo/README.md
Normal file
138
plugins/examples/fusion-plugin-settings-demo/README.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Settings Demo Plugin
|
||||
|
||||
Example Fusion plugin demonstrating settings schema, hooks, and tools with configurable behavior.
|
||||
|
||||
## Features
|
||||
|
||||
- **Settings Schema**: Four different setting types (string, number, boolean, enum)
|
||||
- **Lifecycle Hooks**: `onLoad`, `onTaskCreated`, `onTaskCompleted` that read settings at runtime
|
||||
- **Plugin Tools**: Two AI-agent-callable tools that expose settings-driven functionality
|
||||
|
||||
## Installation via Dashboard Settings
|
||||
|
||||
### Method 1: Settings → Plugins (Recommended)
|
||||
|
||||
1. Open the Fusion dashboard
|
||||
2. Navigate to **Settings** (gear icon in header)
|
||||
3. Click **Plugins** in the sidebar
|
||||
4. Click the **Install** button
|
||||
5. Enter the absolute path to this plugin directory:
|
||||
```
|
||||
/absolute/path/to/plugins/examples/fusion-plugin-settings-demo
|
||||
```
|
||||
6. Click **Install** to register the plugin
|
||||
7. The plugin will appear in the list with state "installed"
|
||||
8. Click the toggle to enable the plugin
|
||||
9. Click the **Settings** (gear) icon to configure the plugin:
|
||||
- **Greeting Message**: Custom message shown when plugin loads
|
||||
- **Max Tags**: Maximum tags to suggest per task (1-10)
|
||||
- **Enable Logging**: Toggle console logging on/off
|
||||
- **Log Level**: Minimum log level (debug, info, warn, error)
|
||||
10. Click **Save Settings** to apply configuration
|
||||
11. The plugin will reload with the new settings
|
||||
|
||||
### Method 2: Manual Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/gsxdsm/fusion.git
|
||||
cd fusion/plugins/examples/fusion-plugin-settings-demo
|
||||
```
|
||||
|
||||
Then use the dashboard Settings → Plugins UI to install from the local path.
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `greetingMessage` | string | "Hello from Settings Demo!" | Custom greeting shown on load |
|
||||
| `maxTags` | number | 3 | Maximum tags to suggest per task |
|
||||
| `enableLogging` | boolean | true | Enable/disable console logging |
|
||||
| `logLevel` | enum | "info" | Minimum log level: debug, info, warn, error |
|
||||
|
||||
## Tools
|
||||
|
||||
### `settings_demo_suggest_tags`
|
||||
|
||||
Analyze a task description and suggest relevant tags based on keyword matching.
|
||||
|
||||
**Parameters:**
|
||||
- `taskDescription` (string, required): The task description to analyze
|
||||
|
||||
**Returns:** Suggested tags separated by commas
|
||||
|
||||
### `settings_demo_status`
|
||||
|
||||
Get the current plugin configuration status.
|
||||
|
||||
**Parameters:** None
|
||||
|
||||
**Returns:** Current settings values
|
||||
|
||||
## Hooks
|
||||
|
||||
| Hook | When | What it does |
|
||||
|------|------|--------------|
|
||||
| `onLoad` | Plugin starts | Logs greeting message and configuration |
|
||||
| `onTaskCreated` | New task created | Suggests tags for tasks with descriptions |
|
||||
| `onTaskCompleted` | Task reaches "done" | Logs completion message |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Build (if needed)
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
fusion-plugin-settings-demo/
|
||||
├── manifest.json # Plugin metadata and settings schema
|
||||
├── package.json # Package configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── vitest.config.ts # Test configuration
|
||||
├── README.md # This file
|
||||
└── src/
|
||||
├── index.ts # Plugin implementation
|
||||
└── __tests__/
|
||||
└── index.test.ts # Plugin tests
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The plugin includes unit tests that verify:
|
||||
|
||||
- Manifest correctness and metadata consistency
|
||||
- Plugin export validity
|
||||
- Settings schema definition
|
||||
- Hook behavior with different configuration values
|
||||
- Tool execution with settings-driven output
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
After installing and configuring the plugin:
|
||||
|
||||
1. Create a new task with description mentioning keywords like "bug", "fix", "performance"
|
||||
2. The plugin will suggest relevant tags based on the content
|
||||
3. Check the console logs (if enabled) to see plugin activity
|
||||
4. Use the `/settings_demo_suggest_tags` tool to get tag suggestions
|
||||
5. Use the `/settings_demo_status` tool to see current configuration
|
||||
|
||||
## Notes
|
||||
|
||||
- The plugin uses `src/index.ts` as the entrypoint for local installation
|
||||
- Settings changes trigger a plugin reload automatically
|
||||
- Hook errors are isolated and won't crash the host system
|
||||
- Tools use the current settings values at execution time
|
||||
35
plugins/examples/fusion-plugin-settings-demo/manifest.json
Normal file
35
plugins/examples/fusion-plugin-settings-demo/manifest.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "fusion-plugin-settings-demo",
|
||||
"name": "Settings Demo Plugin",
|
||||
"version": "0.1.0",
|
||||
"description": "Example plugin demonstrating settings schema, hooks, and tools",
|
||||
"author": "Fusion Team",
|
||||
"homepage": "https://github.com/gsxdsm/fusion",
|
||||
"settingsSchema": {
|
||||
"greetingMessage": {
|
||||
"type": "string",
|
||||
"label": "Greeting Message",
|
||||
"description": "Custom greeting message shown when the plugin loads",
|
||||
"defaultValue": "Hello from Settings Demo!"
|
||||
},
|
||||
"maxTags": {
|
||||
"type": "number",
|
||||
"label": "Max Tags",
|
||||
"description": "Maximum number of tags to suggest per task",
|
||||
"defaultValue": 3
|
||||
},
|
||||
"enableLogging": {
|
||||
"type": "boolean",
|
||||
"label": "Enable Logging",
|
||||
"description": "Log plugin activity to the console",
|
||||
"defaultValue": true
|
||||
},
|
||||
"logLevel": {
|
||||
"type": "enum",
|
||||
"label": "Log Level",
|
||||
"description": "Minimum log level to output",
|
||||
"enumValues": ["debug", "info", "warn", "error"],
|
||||
"defaultValue": "info"
|
||||
}
|
||||
}
|
||||
}
|
||||
27
plugins/examples/fusion-plugin-settings-demo/package.json
Normal file
27
plugins/examples/fusion-plugin-settings-demo/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@fusion-plugin-examples/settings-demo",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "Example Fusion plugin demonstrating settings schema and runtime configuration",
|
||||
"keywords": [
|
||||
"fusion-plugin"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fusion/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.5.2",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import plugin from "../index.js";
|
||||
|
||||
// ── Types for mocking ─────────────────────────────────────────────────────────
|
||||
|
||||
interface MockLogger {
|
||||
info: ReturnType<typeof vi.fn>;
|
||||
warn: ReturnType<typeof vi.fn>;
|
||||
error: ReturnType<typeof vi.fn>;
|
||||
debug: ReturnType<typeof vi.fn>;
|
||||
}
|
||||
|
||||
interface MockContext {
|
||||
pluginId: string;
|
||||
settings: Record<string, unknown>;
|
||||
logger: MockLogger;
|
||||
emitEvent: ReturnType<typeof vi.fn>;
|
||||
taskStore: {
|
||||
getTask: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
}
|
||||
|
||||
function createMockContext(overrides: Partial<MockContext> = {}): MockContext {
|
||||
return {
|
||||
pluginId: "fusion-plugin-settings-demo",
|
||||
settings: {
|
||||
greetingMessage: "Hello from Settings Demo!",
|
||||
maxTags: 3,
|
||||
enableLogging: true,
|
||||
logLevel: "info",
|
||||
},
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
emitEvent: vi.fn(),
|
||||
taskStore: {
|
||||
getTask: vi.fn(),
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Mock Task ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const mockTask = {
|
||||
id: "FN-001",
|
||||
title: "Test Task",
|
||||
description: "Fix the bug in the performance module",
|
||||
column: "todo" as const,
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
size: "M" as const,
|
||||
reviewLevel: "full" as const,
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
// ── Test Suite ─────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("settings demo plugin", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("plugin export", () => {
|
||||
it("should export a valid FusionPlugin with correct manifest fields", () => {
|
||||
expect(plugin.manifest.id).toBe("fusion-plugin-settings-demo");
|
||||
expect(plugin.manifest.name).toBe("Settings Demo Plugin");
|
||||
expect(plugin.manifest.version).toBe("0.1.0");
|
||||
expect(plugin.manifest.description).toBe(
|
||||
"Example plugin demonstrating settings schema, hooks, and tools",
|
||||
);
|
||||
expect(plugin.state).toBe("installed");
|
||||
expect(plugin.hooks).toBeDefined();
|
||||
expect(plugin.tools).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have manifest matching manifest.json", () => {
|
||||
// Verify consistency between code and manifest.json
|
||||
expect(plugin.manifest.id).toBe("fusion-plugin-settings-demo");
|
||||
expect(plugin.manifest.name).toBe("Settings Demo Plugin");
|
||||
expect(plugin.manifest.version).toBe("0.1.0");
|
||||
});
|
||||
|
||||
it("should have settings schema defined with all four types", () => {
|
||||
expect(plugin.manifest.settingsSchema).toBeDefined();
|
||||
const schema = plugin.manifest.settingsSchema!;
|
||||
|
||||
// String type
|
||||
expect(schema.greetingMessage).toBeDefined();
|
||||
expect(schema.greetingMessage.type).toBe("string");
|
||||
expect(schema.greetingMessage.label).toBe("Greeting Message");
|
||||
|
||||
// Number type
|
||||
expect(schema.maxTags).toBeDefined();
|
||||
expect(schema.maxTags.type).toBe("number");
|
||||
expect(schema.maxTags.label).toBe("Max Tags");
|
||||
|
||||
// Boolean type
|
||||
expect(schema.enableLogging).toBeDefined();
|
||||
expect(schema.enableLogging.type).toBe("boolean");
|
||||
expect(schema.enableLogging.label).toBe("Enable Logging");
|
||||
|
||||
// Enum type
|
||||
expect(schema.logLevel).toBeDefined();
|
||||
expect(schema.logLevel.type).toBe("enum");
|
||||
expect(schema.logLevel.enumValues).toEqual(["debug", "info", "warn", "error"]);
|
||||
});
|
||||
|
||||
it("should have default values in settings schema", () => {
|
||||
const schema = plugin.manifest.settingsSchema!;
|
||||
|
||||
expect(schema.greetingMessage.defaultValue).toBe("Hello from Settings Demo!");
|
||||
expect(schema.maxTags.defaultValue).toBe(3);
|
||||
expect(schema.enableLogging.defaultValue).toBe(true);
|
||||
expect(schema.logLevel.defaultValue).toBe("info");
|
||||
});
|
||||
|
||||
it("should have tools defined", () => {
|
||||
expect(plugin.tools).toBeDefined();
|
||||
expect(plugin.tools!.length).toBe(2);
|
||||
|
||||
const toolNames = plugin.tools!.map((t) => t.name);
|
||||
expect(toolNames).toContain("settings_demo_suggest_tags");
|
||||
expect(toolNames).toContain("settings_demo_status");
|
||||
});
|
||||
|
||||
it("should have all required hooks defined", () => {
|
||||
expect(plugin.hooks.onLoad).toBeDefined();
|
||||
expect(plugin.hooks.onTaskCreated).toBeDefined();
|
||||
expect(plugin.hooks.onTaskCompleted).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hooks.onLoad", () => {
|
||||
it("should log greeting message when logging is enabled", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Custom Greeting",
|
||||
maxTags: 3,
|
||||
enableLogging: true,
|
||||
logLevel: "info",
|
||||
},
|
||||
});
|
||||
|
||||
await plugin.hooks.onLoad?.(ctx as any);
|
||||
|
||||
expect(ctx.logger.info).toHaveBeenCalledWith("Custom Greeting");
|
||||
expect(ctx.logger.info).toHaveBeenCalledWith(
|
||||
expect.stringContaining("maxTags: 3"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not log when logging is disabled", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Custom Greeting",
|
||||
maxTags: 3,
|
||||
enableLogging: false,
|
||||
logLevel: "info",
|
||||
},
|
||||
});
|
||||
|
||||
await plugin.hooks.onLoad?.(ctx as any);
|
||||
|
||||
expect(ctx.logger.info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should use default greeting when not configured", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "",
|
||||
maxTags: 3,
|
||||
enableLogging: true,
|
||||
logLevel: "info",
|
||||
},
|
||||
});
|
||||
|
||||
await plugin.hooks.onLoad?.(ctx as any);
|
||||
|
||||
expect(ctx.logger.info).toHaveBeenCalledWith("Hello from Settings Demo!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hooks.onTaskCreated", () => {
|
||||
it("should log when task is created with logging enabled", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 3,
|
||||
enableLogging: true,
|
||||
logLevel: "debug",
|
||||
},
|
||||
});
|
||||
|
||||
await plugin.hooks.onTaskCreated?.(mockTask as any, ctx as any);
|
||||
|
||||
expect(ctx.logger.debug).toHaveBeenCalledWith(
|
||||
expect.stringContaining("FN-001"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should suggest tags for task with description", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 3,
|
||||
enableLogging: true,
|
||||
logLevel: "debug",
|
||||
},
|
||||
});
|
||||
|
||||
await plugin.hooks.onTaskCreated?.(mockTask as any, ctx as any);
|
||||
|
||||
// "performance" keyword matches performance tag
|
||||
expect(ctx.logger.debug).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Suggested tags"),
|
||||
);
|
||||
expect(ctx.logger.debug).toHaveBeenCalledWith(
|
||||
expect.stringContaining("performance"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not log when logging is disabled", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 3,
|
||||
enableLogging: false,
|
||||
logLevel: "info",
|
||||
},
|
||||
});
|
||||
|
||||
await plugin.hooks.onTaskCreated?.(mockTask as any, ctx as any);
|
||||
|
||||
expect(ctx.logger.debug).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle task without description gracefully", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 3,
|
||||
enableLogging: true,
|
||||
logLevel: "debug",
|
||||
},
|
||||
});
|
||||
|
||||
const taskNoDesc = { ...mockTask, description: undefined };
|
||||
|
||||
await plugin.hooks.onTaskCreated?.(taskNoDesc as any, ctx as any);
|
||||
|
||||
// Should log task creation but not tag suggestions
|
||||
expect(ctx.logger.debug).toHaveBeenCalledWith(
|
||||
expect.stringContaining("FN-001"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hooks.onTaskCompleted", () => {
|
||||
it("should log when task is completed with logging enabled", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 3,
|
||||
enableLogging: true,
|
||||
logLevel: "info",
|
||||
},
|
||||
});
|
||||
|
||||
await plugin.hooks.onTaskCompleted?.(mockTask as any, ctx as any);
|
||||
|
||||
expect(ctx.logger.info).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Task completed"),
|
||||
);
|
||||
expect(ctx.logger.info).toHaveBeenCalledWith(
|
||||
expect.stringContaining("FN-001"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not log when logging is disabled", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 3,
|
||||
enableLogging: false,
|
||||
logLevel: "info",
|
||||
},
|
||||
});
|
||||
|
||||
await plugin.hooks.onTaskCompleted?.(mockTask as any, ctx as any);
|
||||
|
||||
expect(ctx.logger.info).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("tools.suggest_tags", () => {
|
||||
it("should suggest tags based on description keywords", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 3,
|
||||
enableLogging: false,
|
||||
logLevel: "info",
|
||||
},
|
||||
});
|
||||
|
||||
const tool = plugin.tools!.find((t) => t.name === "settings_demo_suggest_tags")!;
|
||||
const result = await tool.execute(
|
||||
{ taskDescription: "Fix the bug in the performance module" },
|
||||
ctx as any,
|
||||
);
|
||||
|
||||
expect(result.content[0].text).toContain("Suggested tags:");
|
||||
expect(result.content[0].text).toContain("performance");
|
||||
});
|
||||
|
||||
it("should respect maxTags setting", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 1,
|
||||
enableLogging: false,
|
||||
logLevel: "info",
|
||||
},
|
||||
});
|
||||
|
||||
const tool = plugin.tools!.find((t) => t.name === "settings_demo_suggest_tags")!;
|
||||
const result = await tool.execute(
|
||||
{ taskDescription: "Fix bug fix fix" },
|
||||
ctx as any,
|
||||
);
|
||||
|
||||
// Should only return 1 tag
|
||||
expect(result.details!.count).toBe(1);
|
||||
});
|
||||
|
||||
it("should return no tags for generic description", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 3,
|
||||
enableLogging: false,
|
||||
logLevel: "info",
|
||||
},
|
||||
});
|
||||
|
||||
const tool = plugin.tools!.find((t) => t.name === "settings_demo_suggest_tags")!;
|
||||
const result = await tool.execute(
|
||||
{ taskDescription: "Do something" },
|
||||
ctx as any,
|
||||
);
|
||||
|
||||
expect(result.content[0].text).toContain("No tags could be suggested");
|
||||
});
|
||||
|
||||
it("should handle empty description", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 3,
|
||||
enableLogging: false,
|
||||
logLevel: "info",
|
||||
},
|
||||
});
|
||||
|
||||
const tool = plugin.tools!.find((t) => t.name === "settings_demo_suggest_tags")!;
|
||||
const result = await tool.execute({ taskDescription: "" }, ctx as any);
|
||||
|
||||
expect(result.content[0].text).toContain("No tags could be suggested");
|
||||
});
|
||||
});
|
||||
|
||||
describe("tools.status", () => {
|
||||
it("should return current configuration status", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Custom Greeting",
|
||||
maxTags: 5,
|
||||
enableLogging: true,
|
||||
logLevel: "debug",
|
||||
},
|
||||
});
|
||||
|
||||
const tool = plugin.tools!.find((t) => t.name === "settings_demo_status")!;
|
||||
const result = await tool.execute({}, ctx as any);
|
||||
|
||||
expect(result.content[0].text).toContain("Settings Demo Plugin Status:");
|
||||
expect(result.content[0].text).toContain("Custom Greeting");
|
||||
expect(result.content[0].text).toContain("Max Tags: 5");
|
||||
expect(result.content[0].text).toContain("Logging: enabled");
|
||||
expect(result.content[0].text).toContain("Log Level: debug");
|
||||
|
||||
expect(result.details!.greetingMessage).toBe("Custom Greeting");
|
||||
expect(result.details!.maxTags).toBe(5);
|
||||
expect(result.details!.enableLogging).toBe(true);
|
||||
expect(result.details!.logLevel).toBe("debug");
|
||||
});
|
||||
|
||||
it("should return raw settings values including undefined", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "",
|
||||
maxTags: undefined,
|
||||
enableLogging: undefined,
|
||||
logLevel: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const tool = plugin.tools!.find((t) => t.name === "settings_demo_status")!;
|
||||
const result = await tool.execute({}, ctx as any);
|
||||
|
||||
// Details returns raw values (not resolved defaults)
|
||||
expect(result.details!.greetingMessage).toBe("");
|
||||
expect(result.details!.maxTags).toBeUndefined();
|
||||
expect(result.details!.enableLogging).toBeUndefined();
|
||||
expect(result.details!.logLevel).toBeUndefined();
|
||||
|
||||
// But display text uses resolved defaults
|
||||
expect(result.content[0].text).toContain("Not configured");
|
||||
expect(result.content[0].text).toContain("Max Tags: 3");
|
||||
expect(result.content[0].text).toContain("Logging: enabled");
|
||||
expect(result.content[0].text).toContain("Log Level: info");
|
||||
});
|
||||
});
|
||||
|
||||
describe("settings-driven behavior", () => {
|
||||
it("should log debug messages only when logLevel allows", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 3,
|
||||
enableLogging: true,
|
||||
logLevel: "error", // Only error level
|
||||
},
|
||||
});
|
||||
|
||||
await plugin.hooks.onLoad?.(ctx as any);
|
||||
await plugin.hooks.onTaskCreated?.(mockTask as any, ctx as any);
|
||||
|
||||
// Debug and info should be filtered out
|
||||
expect(ctx.logger.debug).not.toHaveBeenCalled();
|
||||
expect(ctx.logger.info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should log all levels when logLevel is debug", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 3,
|
||||
enableLogging: true,
|
||||
logLevel: "debug",
|
||||
},
|
||||
});
|
||||
|
||||
await plugin.hooks.onLoad?.(ctx as any);
|
||||
|
||||
// All log levels should work
|
||||
expect(ctx.logger.info).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle maxTags edge case of 0", async () => {
|
||||
const ctx = createMockContext({
|
||||
settings: {
|
||||
greetingMessage: "Hello",
|
||||
maxTags: 0, // Edge case: 0 tags
|
||||
enableLogging: false,
|
||||
logLevel: "info",
|
||||
},
|
||||
});
|
||||
|
||||
const tool = plugin.tools!.find((t) => t.name === "settings_demo_suggest_tags")!;
|
||||
const result = await tool.execute(
|
||||
{ taskDescription: "Fix bug performance ui" },
|
||||
ctx as any,
|
||||
);
|
||||
|
||||
// When maxTags is 0, no tags should be returned
|
||||
expect(result.details!.tags).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
277
plugins/examples/fusion-plugin-settings-demo/src/index.ts
Normal file
277
plugins/examples/fusion-plugin-settings-demo/src/index.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* Settings Demo Plugin
|
||||
*
|
||||
* Example Fusion plugin that demonstrates:
|
||||
* - Settings schema with multiple field types (string, number, boolean, enum)
|
||||
* - Hooks that read configuration at runtime
|
||||
* - Tools that expose settings-driven functionality
|
||||
*
|
||||
* This plugin suggests tags for tasks based on configurable keywords
|
||||
* and provides a status endpoint for checking configuration.
|
||||
*/
|
||||
|
||||
import { definePlugin } from "@fusion/plugin-sdk";
|
||||
import type {
|
||||
FusionPlugin,
|
||||
PluginContext,
|
||||
PluginSettingSchema,
|
||||
PluginToolDefinition,
|
||||
PluginToolResult,
|
||||
} from "@fusion/plugin-sdk";
|
||||
|
||||
// ── Settings Schema ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Settings schema for the plugin.
|
||||
* Users can configure these values via the dashboard Settings → Plugins UI.
|
||||
*/
|
||||
const settingsSchema: Record<string, PluginSettingSchema> = {
|
||||
greetingMessage: {
|
||||
type: "string",
|
||||
label: "Greeting Message",
|
||||
description: "Custom greeting message shown when the plugin loads",
|
||||
defaultValue: "Hello from Settings Demo!",
|
||||
},
|
||||
maxTags: {
|
||||
type: "number",
|
||||
label: "Max Tags",
|
||||
description: "Maximum number of tags to suggest per task",
|
||||
defaultValue: 3,
|
||||
},
|
||||
enableLogging: {
|
||||
type: "boolean",
|
||||
label: "Enable Logging",
|
||||
description: "Log plugin activity to the console",
|
||||
defaultValue: true,
|
||||
},
|
||||
logLevel: {
|
||||
type: "enum",
|
||||
label: "Log Level",
|
||||
description: "Minimum log level to output",
|
||||
enumValues: ["debug", "info", "warn", "error"],
|
||||
defaultValue: "info",
|
||||
},
|
||||
};
|
||||
|
||||
// ── Tag Keywords Configuration ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Keyword-to-tag mappings for automatic tag suggestion.
|
||||
* In a real plugin, this would be user-configurable.
|
||||
*/
|
||||
const TAG_KEYWORDS: Record<string, string[]> = {
|
||||
bug: ["fix", "bug", "error", "crash", "broken", "issue"],
|
||||
feature: ["add", "implement", "create", "new", "feature"],
|
||||
refactor: ["refactor", "cleanup", "improve", "optimize", "restructure"],
|
||||
docs: ["docs", "documentation", "readme", "comment"],
|
||||
test: ["test", "testing", "spec", "coverage"],
|
||||
security: ["security", "vulnerability", "auth", "permission"],
|
||||
performance: ["performance", "speed", "optimize", "fast"],
|
||||
ui: ["ui", "interface", "design", "visual", "frontend"],
|
||||
backend: ["api", "backend", "server", "database"],
|
||||
};
|
||||
|
||||
// ── Helper Functions ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Extract suggested tags from task description based on keywords.
|
||||
*/
|
||||
function suggestTags(
|
||||
description: string,
|
||||
maxTags: number,
|
||||
): string[] {
|
||||
if (!description) return [];
|
||||
|
||||
const lowerDesc = description.toLowerCase();
|
||||
const suggestions: { tag: string; count: number }[] = [];
|
||||
|
||||
for (const [tag, keywords] of Object.entries(TAG_KEYWORDS)) {
|
||||
const matchCount = keywords.filter((kw) => lowerDesc.includes(kw)).length;
|
||||
if (matchCount > 0) {
|
||||
suggestions.push({ tag, count: matchCount });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by match count (most matches first) and take up to maxTags
|
||||
return suggestions
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, maxTags)
|
||||
.map((s) => s.tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a log level should be output based on configured minimum level.
|
||||
*/
|
||||
function shouldLog(
|
||||
configuredLevel: string,
|
||||
messageLevel: string,
|
||||
): boolean {
|
||||
const levels = ["debug", "info", "warn", "error"];
|
||||
const configuredIdx = levels.indexOf(configuredLevel);
|
||||
const messageIdx = levels.indexOf(messageLevel);
|
||||
return messageIdx >= configuredIdx;
|
||||
}
|
||||
|
||||
// ── Plugin Tool ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Tool for getting tag suggestions for a task.
|
||||
* Demonstrates how tools can read and use plugin settings.
|
||||
*/
|
||||
const suggestTagsTool: PluginToolDefinition = {
|
||||
name: "settings_demo_suggest_tags",
|
||||
description: "Suggest tags for a task based on its description using keyword matching. Returns up to the configured max tags.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
taskDescription: {
|
||||
type: "string",
|
||||
description: "The task description to analyze for tag suggestions",
|
||||
},
|
||||
},
|
||||
required: ["taskDescription"],
|
||||
},
|
||||
execute: async (
|
||||
params: Record<string, unknown>,
|
||||
ctx: PluginContext,
|
||||
): Promise<PluginToolResult> => {
|
||||
const description = params.taskDescription as string;
|
||||
const maxTagsSetting = ctx.settings.maxTags as number | undefined;
|
||||
// Use 3 as default only if maxTags is not explicitly set (undefined or NaN)
|
||||
// Allow maxTags=0 to return no tags
|
||||
const maxTags = maxTagsSetting !== undefined && !isNaN(maxTagsSetting) ? maxTagsSetting : 3;
|
||||
|
||||
// Log tool usage if enabled
|
||||
if (ctx.settings.enableLogging) {
|
||||
ctx.logger.info(`Suggesting tags for description: ${description.slice(0, 50)}...`);
|
||||
}
|
||||
|
||||
const tags = suggestTags(description, maxTags);
|
||||
|
||||
const result: PluginToolResult = {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: tags.length === 0
|
||||
? "No tags could be suggested based on the task description."
|
||||
: `Suggested tags: ${tags.join(", ")}`,
|
||||
},
|
||||
],
|
||||
details: {
|
||||
tags,
|
||||
count: tags.length,
|
||||
},
|
||||
};
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool for getting plugin configuration status.
|
||||
* Demonstrates how tools expose current settings.
|
||||
*/
|
||||
const statusTool: PluginToolDefinition = {
|
||||
name: "settings_demo_status",
|
||||
description: "Get the current configuration status of the Settings Demo plugin",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
execute: async (
|
||||
_params: Record<string, unknown>,
|
||||
ctx: PluginContext,
|
||||
): Promise<PluginToolResult> => {
|
||||
// Return raw settings values for status display
|
||||
const greetingMessage = ctx.settings.greetingMessage as string | undefined;
|
||||
const maxTags = ctx.settings.maxTags as number | undefined;
|
||||
const enableLogging = ctx.settings.enableLogging as boolean | undefined;
|
||||
const logLevel = ctx.settings.logLevel as string | undefined;
|
||||
|
||||
// Use resolved values for display text
|
||||
const greeting = greetingMessage || "Not configured";
|
||||
const displayMaxTags = maxTags ?? 3;
|
||||
const displayEnableLogging = enableLogging ?? true;
|
||||
const displayLogLevel = logLevel || "info";
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
"Settings Demo Plugin Status:",
|
||||
`- Greeting: ${greeting}`,
|
||||
`- Max Tags: ${displayMaxTags}`,
|
||||
`- Logging: ${displayEnableLogging ? "enabled" : "disabled"}`,
|
||||
`- Log Level: ${displayLogLevel}`,
|
||||
].join("\n"),
|
||||
},
|
||||
],
|
||||
details: {
|
||||
greetingMessage,
|
||||
maxTags,
|
||||
enableLogging,
|
||||
logLevel,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// ── Plugin Definition ───────────────────────────────────────────────────────────
|
||||
|
||||
const plugin: FusionPlugin = definePlugin({
|
||||
manifest: {
|
||||
id: "fusion-plugin-settings-demo",
|
||||
name: "Settings Demo Plugin",
|
||||
version: "0.1.0",
|
||||
description: "Example plugin demonstrating settings schema, hooks, and tools",
|
||||
settingsSchema,
|
||||
},
|
||||
state: "installed",
|
||||
tools: [suggestTagsTool, statusTool],
|
||||
hooks: {
|
||||
onLoad: (ctx: PluginContext) => {
|
||||
const greeting =
|
||||
(ctx.settings.greetingMessage as string) || "Hello from Settings Demo!";
|
||||
const enableLogging = (ctx.settings.enableLogging as boolean) ?? true;
|
||||
const logLevel = (ctx.settings.logLevel as string) || "info";
|
||||
|
||||
if (enableLogging && shouldLog(logLevel, "info")) {
|
||||
ctx.logger.info(greeting);
|
||||
ctx.logger.info(`Plugin configured with maxTags: ${ctx.settings.maxTags || 3}`);
|
||||
}
|
||||
},
|
||||
|
||||
onTaskCreated: async (task: { id: string; title?: string; description?: string }, ctx: PluginContext) => {
|
||||
const enableLogging = (ctx.settings.enableLogging as boolean) ?? true;
|
||||
const logLevel = (ctx.settings.logLevel as string) || "info";
|
||||
|
||||
if (!enableLogging || !shouldLog(logLevel, "debug")) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.logger.debug(`Task created: ${task.id} - ${task.title || "untitled"}`);
|
||||
|
||||
// Auto-suggest tags for new tasks
|
||||
if (task.description) {
|
||||
const maxTags = (ctx.settings.maxTags as number) || 3;
|
||||
const tags = suggestTags(task.description, maxTags);
|
||||
if (tags.length > 0) {
|
||||
ctx.logger.debug(`Suggested tags for ${task.id}: ${tags.join(", ")}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onTaskCompleted: async (task: { id: string; title?: string }, ctx: PluginContext) => {
|
||||
const enableLogging = (ctx.settings.enableLogging as boolean) ?? true;
|
||||
const logLevel = (ctx.settings.logLevel as string) || "info";
|
||||
|
||||
if (enableLogging && shouldLog(logLevel, "info")) {
|
||||
ctx.logger.info(`Task completed: ${task.id} - ${task.title || "untitled"}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default plugin;
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"types": ["node", "vitest/globals"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["src/**/*.test.ts"],
|
||||
pool: "threads",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user