From be568bfdb0176ced63373493ee143adced329cf9 Mon Sep 17 00:00:00 2001 From: fullex <0xfullex@gmail.com> Date: Mon, 8 Jun 2026 05:31:40 -0700 Subject: [PATCH] test(mcp): consolidate mcp tests into the shared co-located suite src/main/__tests__/mcp.test.ts exercised src/shared/mcp.ts, sitting one layer away from the module it covers and duplicating the home of the existing src/shared/__tests__/mcp.test.ts. Merge its toCamelCase, buildMcpToolName, generateMcpToolFunctionName and buildFunctionCallToolName suites into the shared file and drop the misplaced copy. --- src/main/__tests__/mcp.test.ts | 240 ------------------------------ src/shared/__tests__/mcp.test.ts | 246 ++++++++++++++++++++++++++++++- 2 files changed, 245 insertions(+), 241 deletions(-) delete mode 100644 src/main/__tests__/mcp.test.ts diff --git a/src/main/__tests__/mcp.test.ts b/src/main/__tests__/mcp.test.ts deleted file mode 100644 index 809db3d665..0000000000 --- a/src/main/__tests__/mcp.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { buildFunctionCallToolName, buildMcpToolName, generateMcpToolFunctionName, toCamelCase } from '@shared/mcp' -import { describe, expect, it } from 'vitest' - -describe('toCamelCase', () => { - it('should convert hyphenated strings', () => { - expect(toCamelCase('my-server')).toBe('myServer') - expect(toCamelCase('my-tool-name')).toBe('myToolName') - }) - - it('should convert underscored strings', () => { - expect(toCamelCase('my_server')).toBe('myServer') - expect(toCamelCase('search_issues')).toBe('searchIssues') - }) - - it('should handle mixed delimiters', () => { - expect(toCamelCase('my-server_name')).toBe('myServerName') - }) - - it('should handle leading numbers by prefixing underscore', () => { - expect(toCamelCase('123server')).toBe('_123server') - }) - - it('should handle special characters', () => { - expect(toCamelCase('test@server!')).toBe('testServer') - expect(toCamelCase('tool#name$')).toBe('toolName') - }) - - it('should trim whitespace', () => { - expect(toCamelCase(' server ')).toBe('server') - }) - - it('should handle empty string', () => { - expect(toCamelCase('')).toBe('') - }) - - it('should handle uppercase snake case', () => { - expect(toCamelCase('MY_SERVER')).toBe('myServer') - expect(toCamelCase('SEARCH_ISSUES')).toBe('searchIssues') - }) - - it('should handle mixed case', () => { - expect(toCamelCase('MyServer')).toBe('myserver') - expect(toCamelCase('myTOOL')).toBe('mytool') - }) -}) - -describe('buildMcpToolName', () => { - it('should build basic name with defaults', () => { - expect(buildMcpToolName('github', 'search_issues')).toBe('github_searchIssues') - }) - - it('should handle undefined server name', () => { - expect(buildMcpToolName(undefined, 'search_issues')).toBe('searchIssues') - }) - - it('should apply custom prefix and delimiter', () => { - expect(buildMcpToolName('github', 'search', { prefix: 'mcp__', delimiter: '__' })).toBe('mcp__github__search') - }) - - it('should respect maxLength', () => { - const result = buildMcpToolName('veryLongServerName', 'veryLongToolName', { maxLength: 20 }) - expect(result.length).toBeLessThanOrEqual(20) - }) - - it('should handle collision with existingNames', () => { - const existingNames = new Set(['github_search']) - const result = buildMcpToolName('github', 'search', { existingNames }) - expect(result).toBe('github_search1') - expect(existingNames.has('github_search1')).toBe(true) - }) - - it('should respect maxLength when adding collision suffix', () => { - const existingNames = new Set(['a'.repeat(20)]) - const result = buildMcpToolName('a'.repeat(20), '', { maxLength: 20, existingNames }) - expect(result.length).toBeLessThanOrEqual(20) - expect(existingNames.has(result)).toBe(true) - }) - - it('should handle multiple collisions with maxLength', () => { - const existingNames = new Set(['abcd', 'abc1', 'abc2']) - const result = buildMcpToolName('abcd', '', { maxLength: 4, existingNames }) - expect(result).toBe('abc3') - expect(result.length).toBeLessThanOrEqual(4) - }) -}) - -describe('generateMcpToolFunctionName', () => { - it('should return format serverName_toolName in camelCase', () => { - expect(generateMcpToolFunctionName('github', 'search_issues')).toBe('github_searchIssues') - }) - - it('should handle hyphenated names', () => { - expect(generateMcpToolFunctionName('my-server', 'my-tool')).toBe('myServer_myTool') - }) - - it('should handle undefined server name', () => { - expect(generateMcpToolFunctionName(undefined, 'search_issues')).toBe('searchIssues') - }) - - it('should handle collision detection', () => { - const existingNames = new Set() - const first = generateMcpToolFunctionName('github', 'search', existingNames) - const second = generateMcpToolFunctionName('github', 'search', existingNames) - expect(first).toBe('github_search') - expect(second).toBe('github_search1') - }) -}) - -describe('buildFunctionCallToolName', () => { - describe('basic format', () => { - it('should return format mcp__{server}__{tool} in camelCase', () => { - const result = buildFunctionCallToolName('github', 'search_issues') - expect(result).toBe('mcp__github__searchIssues') - }) - - it('should handle simple server and tool names', () => { - expect(buildFunctionCallToolName('fetch', 'get_page')).toBe('mcp__fetch__getPage') - expect(buildFunctionCallToolName('database', 'query')).toBe('mcp__database__query') - }) - }) - - describe('valid JavaScript identifier', () => { - it('should always start with mcp__ prefix (valid JS identifier start)', () => { - const result = buildFunctionCallToolName('123server', '456tool') - expect(result).toMatch(/^mcp__/) - }) - - it('should handle hyphenated names with camelCase', () => { - const result = buildFunctionCallToolName('my-server', 'my-tool') - expect(result).toBe('mcp__myServer__myTool') - }) - - it('should be a valid JavaScript identifier', () => { - const testCases = [ - ['github', 'create_issue'], - ['my-server', 'fetch-data'], - ['test@server', 'tool#name'], - ['server.name', 'tool.action'] - ] - - for (const [server, tool] of testCases) { - const result = buildFunctionCallToolName(server, tool) - expect(result).toMatch(/^[a-zA-Z_][a-zA-Z0-9_]*$/) - } - }) - }) - - describe('character sanitization', () => { - it('should convert special characters to camelCase boundaries', () => { - expect(buildFunctionCallToolName('my-server', 'my-tool-name')).toBe('mcp__myServer__myToolName') - expect(buildFunctionCallToolName('test@server!', 'tool#name$')).toBe('mcp__testServer__toolName') - expect(buildFunctionCallToolName('server.name', 'tool.action')).toBe('mcp__serverName__toolAction') - }) - - it('should handle spaces', () => { - const result = buildFunctionCallToolName('my server', 'my tool') - expect(result).toBe('mcp__myServer__myTool') - }) - }) - - describe('length constraints', () => { - it('should not exceed 63 characters', () => { - const longServerName = 'a'.repeat(50) - const longToolName = 'b'.repeat(50) - const result = buildFunctionCallToolName(longServerName, longToolName) - expect(result.length).toBeLessThanOrEqual(63) - }) - - it('should not end with underscores after truncation', () => { - const longServerName = 'a'.repeat(30) - const longToolName = 'b'.repeat(30) - const result = buildFunctionCallToolName(longServerName, longToolName) - expect(result).not.toMatch(/_+$/) - expect(result.length).toBeLessThanOrEqual(63) - }) - }) - - describe('edge cases', () => { - it('should handle empty server name', () => { - const result = buildFunctionCallToolName('', 'tool') - expect(result).toBe('mcp__tool') - }) - - it('should handle empty tool name', () => { - const result = buildFunctionCallToolName('server', '') - expect(result).toBe('mcp__server__') - }) - - it('should trim whitespace from names', () => { - const result = buildFunctionCallToolName(' server ', ' tool ') - expect(result).toBe('mcp__server__tool') - }) - - it('should handle mixed case by normalizing to lowercase first', () => { - const result = buildFunctionCallToolName('MyServer', 'MyTool') - expect(result).toBe('mcp__myserver__mytool') - }) - - it('should handle uppercase snake case', () => { - const result = buildFunctionCallToolName('MY_SERVER', 'SEARCH_ISSUES') - expect(result).toBe('mcp__myServer__searchIssues') - }) - }) - - describe('deterministic output', () => { - it('should produce consistent results for same input', () => { - const result1 = buildFunctionCallToolName('github', 'search_repos') - const result2 = buildFunctionCallToolName('github', 'search_repos') - expect(result1).toBe(result2) - }) - - it('should produce different results for different inputs', () => { - const result1 = buildFunctionCallToolName('server1', 'tool') - const result2 = buildFunctionCallToolName('server2', 'tool') - expect(result1).not.toBe(result2) - }) - }) - - describe('real-world scenarios', () => { - it('should handle GitHub MCP server', () => { - expect(buildFunctionCallToolName('github', 'create_issue')).toBe('mcp__github__createIssue') - expect(buildFunctionCallToolName('github', 'search_repositories')).toBe('mcp__github__searchRepositories') - }) - - it('should handle filesystem MCP server', () => { - expect(buildFunctionCallToolName('filesystem', 'read_file')).toBe('mcp__filesystem__readFile') - expect(buildFunctionCallToolName('filesystem', 'write_file')).toBe('mcp__filesystem__writeFile') - }) - - it('should handle hyphenated server names (common in npm packages)', () => { - expect(buildFunctionCallToolName('cherry-fetch', 'get_page')).toBe('mcp__cherryFetch__getPage') - expect(buildFunctionCallToolName('mcp-server-github', 'search')).toBe('mcp__mcpServerGithub__search') - }) - - it('should handle scoped npm package style names', () => { - const result = buildFunctionCallToolName('@anthropic/mcp-server', 'chat') - expect(result).toBe('mcp__AnthropicMcpServer__chat') - }) - }) -}) diff --git a/src/shared/__tests__/mcp.test.ts b/src/shared/__tests__/mcp.test.ts index abc79c6c9c..4a32bbebf6 100644 --- a/src/shared/__tests__/mcp.test.ts +++ b/src/shared/__tests__/mcp.test.ts @@ -1,6 +1,12 @@ import { describe, expect, it } from 'vitest' -import { buildFunctionCallToolName, isFunctionCallToolNameForServer, toCamelCase } from '../mcp' +import { + buildFunctionCallToolName, + buildMcpToolName, + generateMcpToolFunctionName, + isFunctionCallToolNameForServer, + toCamelCase +} from '../mcp' describe('isFunctionCallToolNameForServer', () => { it('matches a normal minted id back to its server', () => { @@ -77,3 +83,241 @@ describe('isFunctionCallToolNameForServer', () => { expect(isFunctionCallToolNameForServer('github', 'web__search')).toBe(false) }) }) + +describe('toCamelCase', () => { + it('should convert hyphenated strings', () => { + expect(toCamelCase('my-server')).toBe('myServer') + expect(toCamelCase('my-tool-name')).toBe('myToolName') + }) + + it('should convert underscored strings', () => { + expect(toCamelCase('my_server')).toBe('myServer') + expect(toCamelCase('search_issues')).toBe('searchIssues') + }) + + it('should handle mixed delimiters', () => { + expect(toCamelCase('my-server_name')).toBe('myServerName') + }) + + it('should handle leading numbers by prefixing underscore', () => { + expect(toCamelCase('123server')).toBe('_123server') + }) + + it('should handle special characters', () => { + expect(toCamelCase('test@server!')).toBe('testServer') + expect(toCamelCase('tool#name$')).toBe('toolName') + }) + + it('should trim whitespace', () => { + expect(toCamelCase(' server ')).toBe('server') + }) + + it('should handle empty string', () => { + expect(toCamelCase('')).toBe('') + }) + + it('should handle uppercase snake case', () => { + expect(toCamelCase('MY_SERVER')).toBe('myServer') + expect(toCamelCase('SEARCH_ISSUES')).toBe('searchIssues') + }) + + it('should handle mixed case', () => { + expect(toCamelCase('MyServer')).toBe('myserver') + expect(toCamelCase('myTOOL')).toBe('mytool') + }) +}) + +describe('buildMcpToolName', () => { + it('should build basic name with defaults', () => { + expect(buildMcpToolName('github', 'search_issues')).toBe('github_searchIssues') + }) + + it('should handle undefined server name', () => { + expect(buildMcpToolName(undefined, 'search_issues')).toBe('searchIssues') + }) + + it('should apply custom prefix and delimiter', () => { + expect(buildMcpToolName('github', 'search', { prefix: 'mcp__', delimiter: '__' })).toBe('mcp__github__search') + }) + + it('should respect maxLength', () => { + const result = buildMcpToolName('veryLongServerName', 'veryLongToolName', { maxLength: 20 }) + expect(result.length).toBeLessThanOrEqual(20) + }) + + it('should handle collision with existingNames', () => { + const existingNames = new Set(['github_search']) + const result = buildMcpToolName('github', 'search', { existingNames }) + expect(result).toBe('github_search1') + expect(existingNames.has('github_search1')).toBe(true) + }) + + it('should respect maxLength when adding collision suffix', () => { + const existingNames = new Set(['a'.repeat(20)]) + const result = buildMcpToolName('a'.repeat(20), '', { maxLength: 20, existingNames }) + expect(result.length).toBeLessThanOrEqual(20) + expect(existingNames.has(result)).toBe(true) + }) + + it('should handle multiple collisions with maxLength', () => { + const existingNames = new Set(['abcd', 'abc1', 'abc2']) + const result = buildMcpToolName('abcd', '', { maxLength: 4, existingNames }) + expect(result).toBe('abc3') + expect(result.length).toBeLessThanOrEqual(4) + }) +}) + +describe('generateMcpToolFunctionName', () => { + it('should return format serverName_toolName in camelCase', () => { + expect(generateMcpToolFunctionName('github', 'search_issues')).toBe('github_searchIssues') + }) + + it('should handle hyphenated names', () => { + expect(generateMcpToolFunctionName('my-server', 'my-tool')).toBe('myServer_myTool') + }) + + it('should handle undefined server name', () => { + expect(generateMcpToolFunctionName(undefined, 'search_issues')).toBe('searchIssues') + }) + + it('should handle collision detection', () => { + const existingNames = new Set() + const first = generateMcpToolFunctionName('github', 'search', existingNames) + const second = generateMcpToolFunctionName('github', 'search', existingNames) + expect(first).toBe('github_search') + expect(second).toBe('github_search1') + }) +}) + +describe('buildFunctionCallToolName', () => { + describe('basic format', () => { + it('should return format mcp__{server}__{tool} in camelCase', () => { + const result = buildFunctionCallToolName('github', 'search_issues') + expect(result).toBe('mcp__github__searchIssues') + }) + + it('should handle simple server and tool names', () => { + expect(buildFunctionCallToolName('fetch', 'get_page')).toBe('mcp__fetch__getPage') + expect(buildFunctionCallToolName('database', 'query')).toBe('mcp__database__query') + }) + }) + + describe('valid JavaScript identifier', () => { + it('should always start with mcp__ prefix (valid JS identifier start)', () => { + const result = buildFunctionCallToolName('123server', '456tool') + expect(result).toMatch(/^mcp__/) + }) + + it('should handle hyphenated names with camelCase', () => { + const result = buildFunctionCallToolName('my-server', 'my-tool') + expect(result).toBe('mcp__myServer__myTool') + }) + + it('should be a valid JavaScript identifier', () => { + const testCases = [ + ['github', 'create_issue'], + ['my-server', 'fetch-data'], + ['test@server', 'tool#name'], + ['server.name', 'tool.action'] + ] + + for (const [server, tool] of testCases) { + const result = buildFunctionCallToolName(server, tool) + expect(result).toMatch(/^[a-zA-Z_][a-zA-Z0-9_]*$/) + } + }) + }) + + describe('character sanitization', () => { + it('should convert special characters to camelCase boundaries', () => { + expect(buildFunctionCallToolName('my-server', 'my-tool-name')).toBe('mcp__myServer__myToolName') + expect(buildFunctionCallToolName('test@server!', 'tool#name$')).toBe('mcp__testServer__toolName') + expect(buildFunctionCallToolName('server.name', 'tool.action')).toBe('mcp__serverName__toolAction') + }) + + it('should handle spaces', () => { + const result = buildFunctionCallToolName('my server', 'my tool') + expect(result).toBe('mcp__myServer__myTool') + }) + }) + + describe('length constraints', () => { + it('should not exceed 63 characters', () => { + const longServerName = 'a'.repeat(50) + const longToolName = 'b'.repeat(50) + const result = buildFunctionCallToolName(longServerName, longToolName) + expect(result.length).toBeLessThanOrEqual(63) + }) + + it('should not end with underscores after truncation', () => { + const longServerName = 'a'.repeat(30) + const longToolName = 'b'.repeat(30) + const result = buildFunctionCallToolName(longServerName, longToolName) + expect(result).not.toMatch(/_+$/) + expect(result.length).toBeLessThanOrEqual(63) + }) + }) + + describe('edge cases', () => { + it('should handle empty server name', () => { + const result = buildFunctionCallToolName('', 'tool') + expect(result).toBe('mcp__tool') + }) + + it('should handle empty tool name', () => { + const result = buildFunctionCallToolName('server', '') + expect(result).toBe('mcp__server__') + }) + + it('should trim whitespace from names', () => { + const result = buildFunctionCallToolName(' server ', ' tool ') + expect(result).toBe('mcp__server__tool') + }) + + it('should handle mixed case by normalizing to lowercase first', () => { + const result = buildFunctionCallToolName('MyServer', 'MyTool') + expect(result).toBe('mcp__myserver__mytool') + }) + + it('should handle uppercase snake case', () => { + const result = buildFunctionCallToolName('MY_SERVER', 'SEARCH_ISSUES') + expect(result).toBe('mcp__myServer__searchIssues') + }) + }) + + describe('deterministic output', () => { + it('should produce consistent results for same input', () => { + const result1 = buildFunctionCallToolName('github', 'search_repos') + const result2 = buildFunctionCallToolName('github', 'search_repos') + expect(result1).toBe(result2) + }) + + it('should produce different results for different inputs', () => { + const result1 = buildFunctionCallToolName('server1', 'tool') + const result2 = buildFunctionCallToolName('server2', 'tool') + expect(result1).not.toBe(result2) + }) + }) + + describe('real-world scenarios', () => { + it('should handle GitHub MCP server', () => { + expect(buildFunctionCallToolName('github', 'create_issue')).toBe('mcp__github__createIssue') + expect(buildFunctionCallToolName('github', 'search_repositories')).toBe('mcp__github__searchRepositories') + }) + + it('should handle filesystem MCP server', () => { + expect(buildFunctionCallToolName('filesystem', 'read_file')).toBe('mcp__filesystem__readFile') + expect(buildFunctionCallToolName('filesystem', 'write_file')).toBe('mcp__filesystem__writeFile') + }) + + it('should handle hyphenated server names (common in npm packages)', () => { + expect(buildFunctionCallToolName('cherry-fetch', 'get_page')).toBe('mcp__cherryFetch__getPage') + expect(buildFunctionCallToolName('mcp-server-github', 'search')).toBe('mcp__mcpServerGithub__search') + }) + + it('should handle scoped npm package style names', () => { + const result = buildFunctionCallToolName('@anthropic/mcp-server', 'chat') + expect(result).toBe('mcp__AnthropicMcpServer__chat') + }) + }) +})