Files
ai-inference/__tests__/mcp.test.ts
2025-07-16 02:19:49 +00:00

292 lines
8.3 KiB
TypeScript

/**
* Unit tests for the MCP module, src/mcp.ts
*/
import { jest } from '@jest/globals'
import * as core from '../__fixtures__/core.js'
// Mock MCP SDK
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockConnect = jest.fn() as jest.MockedFunction<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockListTools = jest.fn() as jest.MockedFunction<any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockCallTool = jest.fn() as jest.MockedFunction<any>
const mockClient = {
connect: mockConnect,
listTools: mockListTools,
callTool: mockCallTool
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any
jest.unstable_mockModule('@modelcontextprotocol/sdk/client/index.js', () => ({
Client: jest.fn(() => mockClient)
}))
jest.unstable_mockModule(
'@modelcontextprotocol/sdk/client/streamableHttp.js',
() => ({
StreamableHTTPClientTransport: jest.fn()
})
)
jest.unstable_mockModule('@actions/core', () => core)
// Import the module being tested
const { connectToGitHubMCP, executeToolCall, executeToolCalls } = await import(
'../src/mcp.js'
)
describe('mcp.ts', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('connectToGitHubMCP', () => {
it('successfully connects to MCP server and retrieves tools', async () => {
const token = 'test-token'
const mockTools = [
{
name: 'test-tool-1',
description: 'Test tool 1',
inputSchema: { type: 'object', properties: {} }
},
{
name: 'test-tool-2',
description: 'Test tool 2',
inputSchema: {
type: 'object',
properties: { param: { type: 'string' } }
}
}
]
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({ tools: mockTools })
const result = await connectToGitHubMCP(token)
expect(result).not.toBeNull()
expect(result?.client).toBe(mockClient)
expect(result?.tools).toHaveLength(2)
expect(result?.tools[0]).toEqual({
type: 'function',
function: {
name: 'test-tool-1',
description: 'Test tool 1',
parameters: { type: 'object', properties: {} }
}
})
expect(core.info).toHaveBeenCalledWith(
'Connecting to GitHub MCP server...'
)
expect(core.info).toHaveBeenCalledWith(
'Successfully connected to GitHub MCP server'
)
expect(core.info).toHaveBeenCalledWith(
'Retrieved 2 tools from GitHub MCP server'
)
expect(core.info).toHaveBeenCalledWith(
'Mapped 2 GitHub MCP tools for Azure AI Inference'
)
})
it('returns null when connection fails', async () => {
const token = 'test-token'
const connectionError = new Error('Connection failed')
mockConnect.mockRejectedValue(connectionError)
const result = await connectToGitHubMCP(token)
expect(result).toBeNull()
expect(core.warning).toHaveBeenCalledWith(
'Failed to connect to GitHub MCP server: Error: Connection failed'
)
})
it('handles empty tools list', async () => {
const token = 'test-token'
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({ tools: [] })
const result = await connectToGitHubMCP(token)
expect(result).not.toBeNull()
expect(result?.tools).toHaveLength(0)
expect(core.info).toHaveBeenCalledWith(
'Retrieved 0 tools from GitHub MCP server'
)
expect(core.info).toHaveBeenCalledWith(
'Mapped 0 GitHub MCP tools for Azure AI Inference'
)
})
it('handles undefined tools list', async () => {
const token = 'test-token'
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({})
const result = await connectToGitHubMCP(token)
expect(result).not.toBeNull()
expect(result?.tools).toHaveLength(0)
expect(core.info).toHaveBeenCalledWith(
'Retrieved 0 tools from GitHub MCP server'
)
})
})
describe('executeToolCall', () => {
it('successfully executes a tool call', async () => {
const toolCall = {
id: 'call-123',
type: 'function',
function: {
name: 'test-tool',
arguments: '{"param": "value"}'
}
}
const toolResult = {
content: [{ type: 'text', text: 'Tool execution result' }]
}
mockCallTool.mockResolvedValue(toolResult)
const result = await executeToolCall(mockClient, toolCall)
expect(mockCallTool).toHaveBeenCalledWith({
name: 'test-tool',
arguments: { param: 'value' }
})
expect(result).toEqual({
tool_call_id: 'call-123',
role: 'tool',
name: 'test-tool',
content: JSON.stringify(toolResult.content)
})
expect(core.info).toHaveBeenCalledWith(
'Executing GitHub MCP tool: test-tool with args: {"param": "value"}'
)
expect(core.info).toHaveBeenCalledWith(
'GitHub MCP tool test-tool executed successfully'
)
})
it('handles tool execution errors gracefully', async () => {
const toolCall = {
id: 'call-456',
type: 'function',
function: {
name: 'failing-tool',
arguments: '{"param": "value"}'
}
}
const toolError = new Error('Tool execution failed')
mockCallTool.mockRejectedValue(toolError)
const result = await executeToolCall(mockClient, toolCall)
expect(result).toEqual({
tool_call_id: 'call-456',
role: 'tool',
name: 'failing-tool',
content: 'Error: Error: Tool execution failed'
})
expect(core.warning).toHaveBeenCalledWith(
'Failed to execute GitHub MCP tool failing-tool: Error: Tool execution failed'
)
})
it('handles invalid JSON arguments', async () => {
const toolCall = {
id: 'call-789',
type: 'function',
function: {
name: 'test-tool',
arguments: 'invalid-json'
}
}
const result = await executeToolCall(mockClient, toolCall)
expect(result.tool_call_id).toBe('call-789')
expect(result.role).toBe('tool')
expect(result.name).toBe('test-tool')
expect(result.content).toContain('Error:')
expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining('Failed to execute GitHub MCP tool test-tool:')
)
})
})
describe('executeToolCalls', () => {
it('executes multiple tool calls successfully', async () => {
const toolCalls = [
{
id: 'call-1',
type: 'function',
function: { name: 'tool-1', arguments: '{}' }
},
{
id: 'call-2',
type: 'function',
function: { name: 'tool-2', arguments: '{"param": "value"}' }
}
]
mockCallTool
.mockResolvedValueOnce({
content: [{ type: 'text', text: 'Result 1' }]
})
.mockResolvedValueOnce({
content: [{ type: 'text', text: 'Result 2' }]
})
const results = await executeToolCalls(mockClient, toolCalls)
expect(results).toHaveLength(2)
expect(results[0].tool_call_id).toBe('call-1')
expect(results[1].tool_call_id).toBe('call-2')
expect(mockCallTool).toHaveBeenCalledTimes(2)
})
it('handles empty tool calls array', async () => {
const results = await executeToolCalls(mockClient, [])
expect(results).toHaveLength(0)
expect(mockCallTool).not.toHaveBeenCalled()
})
it('continues execution even if some tools fail', async () => {
const toolCalls = [
{
id: 'call-1',
type: 'function',
function: { name: 'tool-1', arguments: '{}' }
},
{
id: 'call-2',
type: 'function',
function: { name: 'tool-2', arguments: '{}' }
}
]
mockCallTool
.mockResolvedValueOnce({
content: [{ type: 'text', text: 'Result 1' }]
})
.mockRejectedValueOnce(new Error('Tool 2 failed'))
const results = await executeToolCalls(mockClient, toolCalls)
expect(results).toHaveLength(2)
expect(results[0].content).toContain('Result 1')
expect(results[1].content).toContain('Error:')
})
})
})