Spaces:
Configuration error
Configuration error
| import { randomUUID } from "node:crypto"; | |
| import fs from "node:fs"; | |
| import os from "node:os"; | |
| import path from "node:path"; | |
| import { afterEach, describe, expect, it } from "vitest"; | |
| import { resolvePluginTools } from "./tools.js"; | |
| type TempPlugin = { dir: string; file: string; id: string }; | |
| const tempDirs: string[] = []; | |
| const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} }; | |
| function makeTempDir() { | |
| const dir = path.join(os.tmpdir(), `moltbot-plugin-tools-${randomUUID()}`); | |
| fs.mkdirSync(dir, { recursive: true }); | |
| tempDirs.push(dir); | |
| return dir; | |
| } | |
| function writePlugin(params: { id: string; body: string }): TempPlugin { | |
| const dir = makeTempDir(); | |
| const file = path.join(dir, `${params.id}.js`); | |
| fs.writeFileSync(file, params.body, "utf-8"); | |
| fs.writeFileSync( | |
| path.join(dir, "moltbot.plugin.json"), | |
| JSON.stringify( | |
| { | |
| id: params.id, | |
| configSchema: EMPTY_PLUGIN_SCHEMA, | |
| }, | |
| null, | |
| 2, | |
| ), | |
| "utf-8", | |
| ); | |
| return { dir, file, id: params.id }; | |
| } | |
| afterEach(() => { | |
| for (const dir of tempDirs.splice(0)) { | |
| try { | |
| fs.rmSync(dir, { recursive: true, force: true }); | |
| } catch { | |
| // ignore cleanup failures | |
| } | |
| } | |
| }); | |
| describe("resolvePluginTools optional tools", () => { | |
| const pluginBody = ` | |
| export default { register(api) { | |
| api.registerTool( | |
| { | |
| name: "optional_tool", | |
| description: "optional tool", | |
| parameters: { type: "object", properties: {} }, | |
| async execute() { | |
| return { content: [{ type: "text", text: "ok" }] }; | |
| }, | |
| }, | |
| { optional: true }, | |
| ); | |
| } } | |
| `; | |
| it("skips optional tools without explicit allowlist", () => { | |
| const plugin = writePlugin({ id: "optional-demo", body: pluginBody }); | |
| const tools = resolvePluginTools({ | |
| context: { | |
| config: { | |
| plugins: { | |
| load: { paths: [plugin.file] }, | |
| allow: [plugin.id], | |
| }, | |
| }, | |
| workspaceDir: plugin.dir, | |
| }, | |
| }); | |
| expect(tools).toHaveLength(0); | |
| }); | |
| it("allows optional tools by name", () => { | |
| const plugin = writePlugin({ id: "optional-demo", body: pluginBody }); | |
| const tools = resolvePluginTools({ | |
| context: { | |
| config: { | |
| plugins: { | |
| load: { paths: [plugin.file] }, | |
| allow: [plugin.id], | |
| }, | |
| }, | |
| workspaceDir: plugin.dir, | |
| }, | |
| toolAllowlist: ["optional_tool"], | |
| }); | |
| expect(tools.map((tool) => tool.name)).toContain("optional_tool"); | |
| }); | |
| it("allows optional tools via plugin groups", () => { | |
| const plugin = writePlugin({ id: "optional-demo", body: pluginBody }); | |
| const toolsAll = resolvePluginTools({ | |
| context: { | |
| config: { | |
| plugins: { | |
| load: { paths: [plugin.file] }, | |
| allow: [plugin.id], | |
| }, | |
| }, | |
| workspaceDir: plugin.dir, | |
| }, | |
| toolAllowlist: ["group:plugins"], | |
| }); | |
| expect(toolsAll.map((tool) => tool.name)).toContain("optional_tool"); | |
| const toolsPlugin = resolvePluginTools({ | |
| context: { | |
| config: { | |
| plugins: { | |
| load: { paths: [plugin.file] }, | |
| allow: [plugin.id], | |
| }, | |
| }, | |
| workspaceDir: plugin.dir, | |
| }, | |
| toolAllowlist: ["optional-demo"], | |
| }); | |
| expect(toolsPlugin.map((tool) => tool.name)).toContain("optional_tool"); | |
| }); | |
| it("rejects plugin id collisions with core tool names", () => { | |
| const plugin = writePlugin({ id: "message", body: pluginBody }); | |
| const tools = resolvePluginTools({ | |
| context: { | |
| config: { | |
| plugins: { | |
| load: { paths: [plugin.file] }, | |
| allow: [plugin.id], | |
| }, | |
| }, | |
| workspaceDir: plugin.dir, | |
| }, | |
| existingToolNames: new Set(["message"]), | |
| toolAllowlist: ["message"], | |
| }); | |
| expect(tools).toHaveLength(0); | |
| }); | |
| it("skips conflicting tool names but keeps other tools", () => { | |
| const plugin = writePlugin({ | |
| id: "multi", | |
| body: ` | |
| export default { register(api) { | |
| api.registerTool({ | |
| name: "message", | |
| description: "conflict", | |
| parameters: { type: "object", properties: {} }, | |
| async execute() { | |
| return { content: [{ type: "text", text: "nope" }] }; | |
| }, | |
| }); | |
| api.registerTool({ | |
| name: "other_tool", | |
| description: "ok", | |
| parameters: { type: "object", properties: {} }, | |
| async execute() { | |
| return { content: [{ type: "text", text: "ok" }] }; | |
| }, | |
| }); | |
| } } | |
| `, | |
| }); | |
| const tools = resolvePluginTools({ | |
| context: { | |
| config: { | |
| plugins: { | |
| load: { paths: [plugin.file] }, | |
| allow: [plugin.id], | |
| }, | |
| }, | |
| workspaceDir: plugin.dir, | |
| }, | |
| existingToolNames: new Set(["message"]), | |
| }); | |
| expect(tools.map((tool) => tool.name)).toEqual(["other_tool"]); | |
| }); | |
| }); | |