mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-12 08:59:02 +08:00
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 <amp@ampcode.com>
This commit is contained in:
parent
53528770f8
commit
229c57b951
130
src/main/mcpServers/hub/__tests__/generator.test.ts
Normal file
130
src/main/mcpServers/hub/__tests__/generator.test.ts
Normal file
@ -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<string>()
|
||||
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<string>(['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<string>()
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
144
src/main/mcpServers/hub/__tests__/runtime.test.ts
Normal file
144
src/main/mcpServers/hub/__tests__/runtime.test.ts
Normal file
@ -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>): 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
119
src/main/mcpServers/hub/__tests__/search.test.ts
Normal file
119
src/main/mcpServers/hub/__tests__/search.test.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { searchTools } from '../search'
|
||||
import type { GeneratedTool } from '../types'
|
||||
|
||||
const createMockTool = (partial: Partial<GeneratedTool>): 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(')
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user