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; } function setMultiToolRegistry() { return setRegistry([ { pluginId: "multi", optional: false, source: "/tmp/multi.js", factory: () => [makeTool("message"), makeTool("other_tool")], }, ]); } function resolveWithConflictingCoreName(options?: { suppressNameConflicts?: boolean }) { return resolvePluginTools({ context: createContext() as never, existingToolNames: new Set(["message"]), ...(options?.suppressNameConflicts ? { suppressNameConflicts: true } : {}), }); } function setOptionalDemoRegistry() { setRegistry([ { pluginId: "optional-demo", optional: true, source: "/tmp/optional-demo.js", factory: () => makeTool("optional_tool"), }, ]); } function resolveOptionalDemoTools(toolAllowlist?: string[]) { return resolvePluginTools({ context: createContext() as never, ...(toolAllowlist ? { toolAllowlist } : {}), }); } describe("resolvePluginTools optional tools", () => { beforeEach(() => { loadOpenClawPluginsMock.mockClear(); }); it("skips optional tools without explicit allowlist", () => { setOptionalDemoRegistry(); const tools = resolveOptionalDemoTools(); expect(tools).toHaveLength(0); }); it("allows optional tools by tool name", () => { setOptionalDemoRegistry(); const tools = resolveOptionalDemoTools(["optional_tool"]); expect(tools.map((tool) => tool.name)).toEqual(["optional_tool"]); }); it("allows optional tools via plugin-scoped allowlist entries", () => { setOptionalDemoRegistry(); const toolsByPlugin = resolveOptionalDemoTools(["optional-demo"]); const toolsByGroup = resolveOptionalDemoTools(["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 = setMultiToolRegistry(); const tools = resolveWithConflictingCoreName(); expect(tools.map((tool) => tool.name)).toEqual(["other_tool"]); expect(registry.diagnostics).toHaveLength(1); expect(registry.diagnostics[0]?.message).toContain("plugin tool name conflict"); }); it("suppresses conflict diagnostics when requested", () => { const registry = setMultiToolRegistry(); const tools = resolveWithConflictingCoreName({ suppressNameConflicts: true }); expect(tools.map((tool) => tool.name)).toEqual(["other_tool"]); expect(registry.diagnostics).toHaveLength(0); }); });