| import { beforeEach, describe, expect, it, vi } from "vitest"; |
| import { resolvePluginTools } from "./tools.js"; |
|
|
| type MockRegistryToolEntry = { |
| pluginId: string; |
| optional: boolean; |
| source: string; |
| factory: (ctx: unknown) => unknown; |
| }; |
|
|
| const loadOpenClawPluginsMock = vi.fn(); |
|
|
| vi.mock("./loader.js", () => ({ |
| loadOpenClawPlugins: (params: unknown) => loadOpenClawPluginsMock(params), |
| })); |
|
|
| function makeTool(name: string) { |
| return { |
| name, |
| description: `${name} tool`, |
| parameters: { type: "object", properties: {} }, |
| async execute() { |
| return { content: [{ type: "text", text: "ok" }] }; |
| }, |
| }; |
| } |
|
|
| function createContext() { |
| return { |
| config: { |
| plugins: { |
| enabled: true, |
| allow: ["optional-demo", "message", "multi"], |
| load: { paths: ["/tmp/plugin.js"] }, |
| }, |
| }, |
| workspaceDir: "/tmp", |
| }; |
| } |
|
|
| function setRegistry(entries: MockRegistryToolEntry[]) { |
| const registry = { |
| tools: entries, |
| diagnostics: [] as Array<{ |
| level: string; |
| pluginId: string; |
| source: string; |
| message: string; |
| }>, |
| }; |
| loadOpenClawPluginsMock.mockReturnValue(registry); |
| return registry; |
| } |
|
|
| describe("resolvePluginTools optional tools", () => { |
| beforeEach(() => { |
| loadOpenClawPluginsMock.mockReset(); |
| }); |
|
|
| it("skips optional tools without explicit allowlist", () => { |
| setRegistry([ |
| { |
| pluginId: "optional-demo", |
| optional: true, |
| source: "/tmp/optional-demo.js", |
| factory: () => makeTool("optional_tool"), |
| }, |
| ]); |
|
|
| const tools = resolvePluginTools({ |
| context: createContext() as never, |
| }); |
|
|
| expect(tools).toHaveLength(0); |
| }); |
|
|
| it("allows optional tools by tool name", () => { |
| setRegistry([ |
| { |
| pluginId: "optional-demo", |
| optional: true, |
| source: "/tmp/optional-demo.js", |
| factory: () => makeTool("optional_tool"), |
| }, |
| ]); |
|
|
| const tools = resolvePluginTools({ |
| context: createContext() as never, |
| toolAllowlist: ["optional_tool"], |
| }); |
|
|
| expect(tools.map((tool) => tool.name)).toEqual(["optional_tool"]); |
| }); |
|
|
| it("allows optional tools via plugin-scoped allowlist entries", () => { |
| setRegistry([ |
| { |
| pluginId: "optional-demo", |
| optional: true, |
| source: "/tmp/optional-demo.js", |
| factory: () => makeTool("optional_tool"), |
| }, |
| ]); |
|
|
| const toolsByPlugin = resolvePluginTools({ |
| context: createContext() as never, |
| toolAllowlist: ["optional-demo"], |
| }); |
| const toolsByGroup = resolvePluginTools({ |
| context: createContext() as never, |
| toolAllowlist: ["group:plugins"], |
| }); |
|
|
| expect(toolsByPlugin.map((tool) => tool.name)).toEqual(["optional_tool"]); |
| expect(toolsByGroup.map((tool) => tool.name)).toEqual(["optional_tool"]); |
| }); |
|
|
| it("rejects plugin id collisions with core tool names", () => { |
| const registry = setRegistry([ |
| { |
| pluginId: "message", |
| optional: false, |
| source: "/tmp/message.js", |
| factory: () => makeTool("optional_tool"), |
| }, |
| ]); |
|
|
| const tools = resolvePluginTools({ |
| context: createContext() as never, |
| existingToolNames: new Set(["message"]), |
| }); |
|
|
| expect(tools).toHaveLength(0); |
| expect(registry.diagnostics).toHaveLength(1); |
| expect(registry.diagnostics[0]?.message).toContain("plugin id conflicts with core tool name"); |
| }); |
|
|
| it("skips conflicting tool names but keeps other tools", () => { |
| const registry = setRegistry([ |
| { |
| pluginId: "multi", |
| optional: false, |
| source: "/tmp/multi.js", |
| factory: () => [makeTool("message"), makeTool("other_tool")], |
| }, |
| ]); |
|
|
| const tools = resolvePluginTools({ |
| context: createContext() as never, |
| existingToolNames: new Set(["message"]), |
| }); |
|
|
| expect(tools.map((tool) => tool.name)).toEqual(["other_tool"]); |
| expect(registry.diagnostics).toHaveLength(1); |
| expect(registry.diagnostics[0]?.message).toContain("plugin tool name conflict"); |
| }); |
| }); |
|
|