import { describe, expect, it } from "bun:test"; import { Settings } from "../../src/config/settings"; import { buildDiscoverableToolSearchIndex, type DiscoverableTool, type DiscoverableToolSearchIndex, } from "../../src/tool-discovery/tool-index"; import type { ToolSession } from "../../src/tools/index"; import { SearchToolBm25Tool } from "../../src/tools/search-tool-bm25"; type DiscoveryToolSession = ToolSession & { isMCPDiscoveryEnabled: () => boolean; getDiscoverableTools: (filter?: { source?: DiscoverableTool["source"] }) => DiscoverableTool[]; getDiscoverableToolSearchIndex?: () => DiscoverableToolSearchIndex; getSelectedMCPToolNames: () => string[]; activateDiscoveredMCPTools: (toolNames: string[]) => Promise; getSelected: () => string[]; }; function createSession(tools: DiscoverableTool[], overrides: Partial = {}): DiscoveryToolSession { const selected: string[] = []; return { cwd: "/tmp/test", hasUI: false, getSessionFile: () => null, getSessionSpawns: () => "*", settings: Settings.isolated({ "mcp.discoveryMode": true }), isMCPDiscoveryEnabled: () => true, getDiscoverableTools: () => tools, getSelectedMCPToolNames: () => [...selected], activateDiscoveredMCPTools: async (toolNames: string[]) => { for (const name of toolNames) { if (!selected.includes(name)) { selected.push(name); } } return toolNames; }, getSelected: () => [...selected], ...overrides, }; } /** Helper to create a discoverable MCP tool. */ function mcpTool( name: string, serverName: string, mcpToolName: string, summary: string, schemaKeys: string[], ): DiscoverableTool { return { name, label: `${serverName}/${mcpToolName}`, summary, source: "mcp", serverName, mcpToolName, schemaKeys, }; } /** Helper to create a discoverable built-in tool. */ function builtinTool(name: string, summary: string, schemaKeys: string[] = []): DiscoverableTool { return { name, label: name, summary, source: "builtin", schemaKeys, }; } describe("SearchToolBm25Tool", () => { const discoverableTools: DiscoverableTool[] = [ mcpTool( "mcp__github_create_issue", "github", "create_issue", "Create a GitHub issue in the selected repository", ["owner", "repo", "title", "body"], ), mcpTool("mcp__github_list_pull_requests", "github", "list_pull_requests", "List pull requests for a repository", [ "owner", "repo", "state", ]), mcpTool("mcp__slack_post_message", "slack", "post_message", "Post a message to a Slack channel", [ "channel", "text", ]), ]; it("uses the session-provided cached search index during execution", async () => { let rawToolsCalls = 0; let searchIndexCalls = 0; const searchIndex = buildDiscoverableToolSearchIndex(discoverableTools); const session = createSession(discoverableTools, { getDiscoverableTools: () => { rawToolsCalls++; return discoverableTools; }, getDiscoverableToolSearchIndex: () => { searchIndexCalls++; return searchIndex; }, }); const tool = new SearchToolBm25Tool(session); expect(rawToolsCalls).toBe(0); const result = await tool.execute("call-index", { query: "github" }); expect(searchIndexCalls).toBe(1); expect(rawToolsCalls).toBe(0); expect(result.details?.tools.map(match => match.name)).toEqual([ "mcp__github_create_issue", "mcp__github_list_pull_requests", ]); expect(result.content).toEqual([ { type: "text", text: JSON.stringify({ query: "github", activated_tools: ["mcp__github_create_issue", "mcp__github_list_pull_requests"], match_count: 2, total_tools: 3, }), }, ]); }); it("returns ranked matches and unions activated tools across repeated searches", async () => { const session = createSession(discoverableTools); const tool = new SearchToolBm25Tool(session); const firstResult = await tool.execute("call-1", { query: "github issue", limit: 1 }); const firstDetails = firstResult.details; expect(firstDetails?.tools.map(match => match.name)).toEqual(["mcp__github_create_issue"]); expect(firstDetails?.active_selected_tools).toEqual(["mcp__github_create_issue"]); expect(session.getSelected()).toEqual(["mcp__github_create_issue"]); const secondResult = await tool.execute("call-2", { query: "slack message", limit: 1 }); const secondDetails = secondResult.details; expect(secondDetails?.tools.map(match => match.name)).toEqual(["mcp__slack_post_message"]); expect(secondDetails?.active_selected_tools).toEqual(["mcp__github_create_issue", "mcp__slack_post_message"]); expect(session.getSelected()).toEqual(["mcp__github_create_issue", "mcp__slack_post_message"]); }); it("skips already-selected matches before applying limit", async () => { const session = createSession(discoverableTools); const tool = new SearchToolBm25Tool(session); const firstResult = await tool.execute("call-github-1", { query: "github", limit: 1 }); expect(firstResult.details?.tools.map(match => match.name)).toEqual(["mcp__github_create_issue"]); expect(firstResult.details?.activated_tools).toEqual(["mcp__github_create_issue"]); const secondResult = await tool.execute("call-github-2", { query: "github", limit: 1 }); expect(secondResult.details?.tools.map(match => match.name)).toEqual(["mcp__github_list_pull_requests"]); expect(secondResult.details?.activated_tools).toEqual(["mcp__github_list_pull_requests"]); expect(secondResult.details?.active_selected_tools).toEqual([ "mcp__github_create_issue", "mcp__github_list_pull_requests", ]); const exhaustedResult = await tool.execute("call-github-3", { query: "github", limit: 1 }); expect(exhaustedResult.details?.tools).toEqual([]); expect(exhaustedResult.details?.activated_tools).toEqual([]); expect(exhaustedResult.details?.active_selected_tools).toEqual([ "mcp__github_create_issue", "mcp__github_list_pull_requests", ]); }); it("rejects invalid input", async () => { const tool = new SearchToolBm25Tool(createSession(discoverableTools)); await expect(tool.execute("call-empty", { query: " " })).rejects.toThrow( "Query is required and must not be empty.", ); await expect(tool.execute("call-limit", { query: "github", limit: 0 as never })).rejects.toThrow( "Limit must be a positive integer.", ); }); it("rejects execution when discovery mode is disabled", async () => { const tool = new SearchToolBm25Tool( createSession(discoverableTools, { isMCPDiscoveryEnabled: () => false, settings: Settings.isolated({ "mcp.discoveryMode": false }), }), ); await expect(tool.execute("call-disabled", { query: "github" })).rejects.toThrow("Tool discovery is disabled."); }); it("discovers built-in tools when using the new tools.discoveryMode=all setting", async () => { const builtinTools: DiscoverableTool[] = [ builtinTool("find", "Find files and directories matching a glob pattern"), builtinTool("search", "Search file contents using ripgrep"), ]; const allTools = [...discoverableTools, ...builtinTools]; const session = createSession(discoverableTools, { settings: Settings.isolated({ "tools.discoveryMode": "all" }), getDiscoverableTools: () => allTools, }); const tool = new SearchToolBm25Tool(session); const result = await tool.execute("call-builtin", { query: "find files" }); const names = result.details?.tools.map(t => t.name) ?? []; expect(names).toContain("find"); }); });