From 229c57b9510a3745eb6434243995fde28fdab54a Mon Sep 17 00:00:00 2001 From: Vaayne Date: Wed, 24 Dec 2025 12:21:38 +0800 Subject: [PATCH] test(mcp): add unit tests for hub server - generator.test.ts: Test schema conversion and JSDoc generation - search.test.ts: Test keyword matching, ranking, and limits - runtime.test.ts: Test code execution, helpers, and error handling Amp-Thread-ID: https://ampcode.com/threads/T-019b4e7d-86a3-770d-82f8-9e646e7e597e Co-authored-by: Amp --- .../hub/__tests__/generator.test.ts | 130 ++++++++++++++++ .../mcpServers/hub/__tests__/runtime.test.ts | 144 ++++++++++++++++++ .../mcpServers/hub/__tests__/search.test.ts | 119 +++++++++++++++ 3 files changed, 393 insertions(+) create mode 100644 src/main/mcpServers/hub/__tests__/generator.test.ts create mode 100644 src/main/mcpServers/hub/__tests__/runtime.test.ts create mode 100644 src/main/mcpServers/hub/__tests__/search.test.ts diff --git a/src/main/mcpServers/hub/__tests__/generator.test.ts b/src/main/mcpServers/hub/__tests__/generator.test.ts new file mode 100644 index 0000000000..bb4e0549d4 --- /dev/null +++ b/src/main/mcpServers/hub/__tests__/generator.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, it } from 'vitest' + +import { generateToolFunction, generateToolsCode } from '../generator' +import type { GeneratedTool } from '../types' + +describe('generator', () => { + describe('generateToolFunction', () => { + it('generates a simple tool function', () => { + const tool = { + id: 'test-id', + name: 'search_repos', + description: 'Search for GitHub repositories', + serverId: 'github', + serverName: 'github-server', + inputSchema: { + type: 'object' as const, + properties: { + query: { type: 'string', description: 'Search query' }, + limit: { type: 'number', description: 'Max results' } + }, + required: ['query'] + }, + type: 'mcp' as const + } + + const server = { + id: 'github', + name: 'github-server', + isActive: true + } + + const existingNames = new Set() + const callTool = async () => ({ success: true }) + + const result = generateToolFunction(tool, server as any, existingNames, callTool) + + expect(result.toolId).toBe('github__search_repos') + expect(result.functionName).toBe('searchRepos') + expect(result.jsCode).toContain('async function searchRepos') + expect(result.jsCode).toContain('Search for GitHub repositories') + expect(result.jsCode).toContain('__callTool') + }) + + it('handles unique function names', () => { + const tool = { + id: 'test-id', + name: 'search', + serverId: 'server1', + serverName: 'server1', + inputSchema: { type: 'object' as const, properties: {} }, + type: 'mcp' as const + } + + const server = { id: 'server1', name: 'server1', isActive: true } + const existingNames = new Set(['search']) + const callTool = async () => ({}) + + const result = generateToolFunction(tool, server as any, existingNames, callTool) + + expect(result.functionName).toBe('search1') + }) + + it('handles enum types in schema', () => { + const tool = { + id: 'test-id', + name: 'launch_browser', + serverId: 'browser', + serverName: 'browser', + inputSchema: { + type: 'object' as const, + properties: { + browser: { + type: 'string', + enum: ['chromium', 'firefox', 'webkit'] + } + } + }, + type: 'mcp' as const + } + + const server = { id: 'browser', name: 'browser', isActive: true } + const existingNames = new Set() + const callTool = async () => ({}) + + const result = generateToolFunction(tool, server as any, existingNames, callTool) + + expect(result.jsCode).toContain('"chromium" | "firefox" | "webkit"') + }) + }) + + describe('generateToolsCode', () => { + it('generates code for multiple tools', () => { + const tools: GeneratedTool[] = [ + { + serverId: 's1', + serverName: 'server1', + toolName: 'tool1', + toolId: 's1__tool1', + functionName: 'tool1', + jsCode: 'async function tool1() {}', + fn: async () => ({}), + signature: '{}', + returns: 'unknown' + }, + { + serverId: 's2', + serverName: 'server2', + toolName: 'tool2', + toolId: 's2__tool2', + functionName: 'tool2', + jsCode: 'async function tool2() {}', + fn: async () => ({}), + signature: '{}', + returns: 'unknown' + } + ] + + const result = generateToolsCode(tools) + + expect(result).toContain('Found 2 tool(s)') + expect(result).toContain('async function tool1') + expect(result).toContain('async function tool2') + }) + + it('returns message for empty tools', () => { + const result = generateToolsCode([]) + expect(result).toBe('// No tools available') + }) + }) +}) diff --git a/src/main/mcpServers/hub/__tests__/runtime.test.ts b/src/main/mcpServers/hub/__tests__/runtime.test.ts new file mode 100644 index 0000000000..76366c5e0a --- /dev/null +++ b/src/main/mcpServers/hub/__tests__/runtime.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, it, vi } from 'vitest' + +import { Runtime } from '../runtime' +import type { GeneratedTool } from '../types' + +vi.mock('../mcp-bridge', () => ({ + callMcpTool: vi.fn(async (toolId: string, params: unknown) => { + if (toolId === 'server__failing_tool') { + throw new Error('Tool failed') + } + return { toolId, params, success: true } + }) +})) + +const createMockTool = (partial: Partial): GeneratedTool => ({ + serverId: 'server1', + serverName: 'server1', + toolName: 'tool', + toolId: 'server1__tool', + functionName: 'mockTool', + jsCode: 'async function mockTool() {}', + fn: async (params) => ({ result: params }), + signature: '{}', + returns: 'unknown', + ...partial +}) + +describe('Runtime', () => { + describe('execute', () => { + it('executes simple code and returns result', async () => { + const runtime = new Runtime() + const tools: GeneratedTool[] = [] + + const result = await runtime.execute('return 1 + 1', tools) + + expect(result.result).toBe(2) + expect(result.error).toBeUndefined() + }) + + it('executes async code', async () => { + const runtime = new Runtime() + const tools: GeneratedTool[] = [] + + const result = await runtime.execute('return await Promise.resolve(42)', tools) + + expect(result.result).toBe(42) + }) + + it('calls tool functions', async () => { + const runtime = new Runtime() + const tools = [ + createMockTool({ + functionName: 'searchRepos', + fn: async (params) => ({ repos: ['repo1', 'repo2'], query: params }) + }) + ] + + const result = await runtime.execute('return await searchRepos({ query: "test" })', tools) + + expect(result.result).toEqual({ repos: ['repo1', 'repo2'], query: { query: 'test' } }) + }) + + it('captures console logs', async () => { + const runtime = new Runtime() + const tools: GeneratedTool[] = [] + + const result = await runtime.execute( + ` + console.log("hello"); + console.warn("warning"); + return "done" + `, + tools + ) + + expect(result.result).toBe('done') + expect(result.logs).toContain('[log] hello') + expect(result.logs).toContain('[warn] warning') + }) + + it('handles errors gracefully', async () => { + const runtime = new Runtime() + const tools: GeneratedTool[] = [] + + const result = await runtime.execute('throw new Error("test error")', tools) + + expect(result.result).toBeUndefined() + expect(result.error).toBe('test error') + }) + + it('supports parallel helper', async () => { + const runtime = new Runtime() + const tools: GeneratedTool[] = [] + + const result = await runtime.execute( + ` + const results = await parallel( + Promise.resolve(1), + Promise.resolve(2), + Promise.resolve(3) + ); + return results + `, + tools + ) + + expect(result.result).toEqual([1, 2, 3]) + }) + + it('supports settle helper', async () => { + const runtime = new Runtime() + const tools: GeneratedTool[] = [] + + const result = await runtime.execute( + ` + const results = await settle( + Promise.resolve(1), + Promise.reject(new Error("fail")) + ); + return results.map(r => r.status) + `, + tools + ) + + expect(result.result).toEqual(['fulfilled', 'rejected']) + }) + + it('returns last expression when no explicit return', async () => { + const runtime = new Runtime() + const tools: GeneratedTool[] = [] + + const result = await runtime.execute( + ` + const x = 10; + const y = 20; + return x + y + `, + tools + ) + + expect(result.result).toBe(30) + }) + }) +}) diff --git a/src/main/mcpServers/hub/__tests__/search.test.ts b/src/main/mcpServers/hub/__tests__/search.test.ts new file mode 100644 index 0000000000..6d004797ef --- /dev/null +++ b/src/main/mcpServers/hub/__tests__/search.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from 'vitest' + +import { searchTools } from '../search' +import type { GeneratedTool } from '../types' + +const createMockTool = (partial: Partial): GeneratedTool => { + const functionName = partial.functionName || 'tool' + return { + serverId: 'server1', + serverName: 'server1', + toolName: partial.toolName || 'tool', + toolId: partial.toolId || 'server1__tool', + functionName, + jsCode: `async function ${functionName}() {}`, + fn: async () => ({}), + signature: '{}', + returns: 'unknown', + ...partial + } +} + +describe('search', () => { + describe('searchTools', () => { + it('returns all tools when query is empty', () => { + const tools = [ + createMockTool({ toolName: 'tool1', functionName: 'tool1' }), + createMockTool({ toolName: 'tool2', functionName: 'tool2' }) + ] + + const result = searchTools(tools, { query: '' }) + + expect(result.total).toBe(2) + expect(result.tools).toContain('tool1') + expect(result.tools).toContain('tool2') + }) + + it('filters tools by single keyword', () => { + const tools = [ + createMockTool({ toolName: 'search_repos', functionName: 'searchRepos' }), + createMockTool({ toolName: 'get_user', functionName: 'getUser' }), + createMockTool({ toolName: 'search_users', functionName: 'searchUsers' }) + ] + + const result = searchTools(tools, { query: 'search' }) + + expect(result.total).toBe(2) + expect(result.tools).toContain('searchRepos') + expect(result.tools).toContain('searchUsers') + expect(result.tools).not.toContain('getUser') + }) + + it('supports OR matching with comma-separated keywords', () => { + const tools = [ + createMockTool({ toolName: 'browser_open', functionName: 'browserOpen' }), + createMockTool({ toolName: 'chrome_launch', functionName: 'chromeLaunch' }), + createMockTool({ toolName: 'file_read', functionName: 'fileRead' }) + ] + + const result = searchTools(tools, { query: 'browser,chrome' }) + + expect(result.total).toBe(2) + expect(result.tools).toContain('browserOpen') + expect(result.tools).toContain('chromeLaunch') + expect(result.tools).not.toContain('fileRead') + }) + + it('matches against description', () => { + const tools = [ + createMockTool({ + toolName: 'launch', + functionName: 'launch', + description: 'Launch a browser instance' + }), + createMockTool({ + toolName: 'close', + functionName: 'close', + description: 'Close a window' + }) + ] + + const result = searchTools(tools, { query: 'browser' }) + + expect(result.total).toBe(1) + expect(result.tools).toContain('launch') + }) + + it('respects limit parameter', () => { + const tools = Array.from({ length: 20 }, (_, i) => + createMockTool({ toolName: `tool${i}`, functionName: `tool${i}`, toolId: `s__tool${i}` }) + ) + + const result = searchTools(tools, { query: 'tool', limit: 5 }) + + expect(result.total).toBe(20) + const matches = (result.tools.match(/async function tool\d+/g) || []).length + expect(matches).toBe(5) + }) + + it('is case insensitive', () => { + const tools = [createMockTool({ toolName: 'SearchRepos', functionName: 'searchRepos' })] + + const result = searchTools(tools, { query: 'SEARCH' }) + + expect(result.total).toBe(1) + }) + + it('ranks exact matches higher', () => { + const tools = [ + createMockTool({ toolName: 'searching', functionName: 'searching' }), + createMockTool({ toolName: 'search', functionName: 'search' }), + createMockTool({ toolName: 'search_more', functionName: 'searchMore' }) + ] + + const result = searchTools(tools, { query: 'search', limit: 1 }) + + expect(result.tools).toContain('function search(') + }) + }) +})