Files
ai-inference/__tests__/mcp.test.ts
T

296 lines
9.1 KiB
TypeScript
Raw Normal View History

2025-07-24 19:11:15 +10:00
import {vi, type MockedFunction, describe, it, expect, beforeEach} from 'vitest'
2025-07-16 00:12:41 +00:00
import * as core from '../__fixtures__/core.js'
// Mock MCP SDK
2025-07-16 02:19:49 +00:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2025-07-24 18:08:26 +10:00
const mockConnect = vi.fn() as MockedFunction<any>
2025-07-16 02:19:49 +00:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2025-07-24 18:08:26 +10:00
const mockListTools = vi.fn() as MockedFunction<any>
2025-07-16 02:19:49 +00:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2025-07-24 18:08:26 +10:00
const mockCallTool = vi.fn() as MockedFunction<any>
2025-07-16 00:12:41 +00:00
const mockClient = {
connect: mockConnect,
listTools: mockListTools,
2025-07-24 19:11:15 +10:00
callTool: mockCallTool,
2025-07-16 02:19:49 +00:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2025-07-16 00:12:41 +00:00
} as any
2025-07-24 18:08:26 +10:00
vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
2025-07-24 19:11:15 +10:00
Client: vi.fn(() => mockClient),
2025-07-16 00:12:41 +00:00
}))
2025-07-24 18:08:26 +10:00
vi.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({
2025-07-24 19:11:15 +10:00
StreamableHTTPClientTransport: vi.fn(),
2025-07-24 18:08:26 +10:00
}))
2025-07-16 00:12:41 +00:00
2025-07-24 18:08:26 +10:00
vi.mock('@actions/core', () => core)
2025-07-16 00:12:41 +00:00
// Import the module being tested
2025-07-24 19:11:15 +10:00
const {connectToGitHubMCP, executeToolCall, executeToolCalls} = await import('../src/mcp.js')
2025-07-16 00:12:41 +00:00
describe('mcp.ts', () => {
beforeEach(() => {
2025-07-24 18:08:26 +10:00
vi.clearAllMocks()
2025-07-16 00:12:41 +00:00
})
2025-07-16 02:19:49 +00:00
describe('connectToGitHubMCP', () => {
2025-07-16 00:12:41 +00:00
it('successfully connects to MCP server and retrieves tools', async () => {
const token = 'test-token'
const mockTools = [
{
name: 'test-tool-1',
description: 'Test tool 1',
2025-07-24 19:11:15 +10:00
inputSchema: {type: 'object', properties: {}},
2025-07-16 00:12:41 +00:00
},
{
name: 'test-tool-2',
description: 'Test tool 2',
inputSchema: {
type: 'object',
2025-07-24 19:11:15 +10:00
properties: {param: {type: 'string'}},
},
},
2025-07-16 00:12:41 +00:00
]
mockConnect.mockResolvedValue(undefined)
2025-07-24 19:11:15 +10:00
mockListTools.mockResolvedValue({tools: mockTools})
2025-07-16 00:12:41 +00:00
2025-07-16 02:19:49 +00:00
const result = await connectToGitHubMCP(token)
2025-07-16 00:12:41 +00:00
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',
2025-07-24 19:11:15 +10:00
parameters: {type: 'object', properties: {}},
},
2025-07-16 00:12:41 +00:00
})
2025-07-24 19:11:15 +10:00
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')
2025-07-16 00:12:41 +00:00
})
it('returns null when connection fails', async () => {
const token = 'test-token'
const connectionError = new Error('Connection failed')
mockConnect.mockRejectedValue(connectionError)
2025-07-16 02:19:49 +00:00
const result = await connectToGitHubMCP(token)
2025-07-16 00:12:41 +00:00
expect(result).toBeNull()
2025-07-24 19:11:15 +10:00
expect(core.warning).toHaveBeenCalledWith('Failed to connect to GitHub MCP server: Error: Connection failed')
2025-07-16 00:12:41 +00:00
})
it('handles empty tools list', async () => {
const token = 'test-token'
mockConnect.mockResolvedValue(undefined)
2025-07-24 19:11:15 +10:00
mockListTools.mockResolvedValue({tools: []})
2025-07-16 00:12:41 +00:00
2025-07-16 02:19:49 +00:00
const result = await connectToGitHubMCP(token)
2025-07-16 00:12:41 +00:00
expect(result).not.toBeNull()
expect(result?.tools).toHaveLength(0)
2025-07-24 19:11:15 +10:00
expect(core.info).toHaveBeenCalledWith('Retrieved 0 tools from GitHub MCP server')
expect(core.info).toHaveBeenCalledWith('Mapped 0 GitHub MCP tools for Azure AI Inference')
2025-07-16 00:12:41 +00:00
})
it('handles undefined tools list', async () => {
const token = 'test-token'
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({})
2025-07-16 02:19:49 +00:00
const result = await connectToGitHubMCP(token)
2025-07-16 00:12:41 +00:00
expect(result).not.toBeNull()
expect(result?.tools).toHaveLength(0)
2025-07-24 19:11:15 +10:00
expect(core.info).toHaveBeenCalledWith('Retrieved 0 tools from GitHub MCP server')
2025-07-16 00:12:41 +00:00
})
2025-11-02 23:20:24 +01:00
it('uses default toolsets when toolsets parameter is not provided', async () => {
const token = 'test-token'
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({tools: []})
await connectToGitHubMCP(token)
expect(core.info).toHaveBeenCalledWith('Using default GitHub MCP toolsets')
})
it('uses custom toolsets when toolsets parameter is provided', async () => {
const token = 'test-token'
const toolsets = 'repos,issues,pull_requests,actions'
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({tools: []})
await connectToGitHubMCP(token, toolsets)
expect(core.info).toHaveBeenCalledWith('Using GitHub MCP toolsets: repos,issues,pull_requests,actions')
})
it('ignores empty toolsets parameter', async () => {
const token = 'test-token'
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({tools: []})
await connectToGitHubMCP(token, ' ')
expect(core.info).toHaveBeenCalledWith('Using default GitHub MCP toolsets')
})
2025-07-16 00:12:41 +00:00
})
describe('executeToolCall', () => {
it('successfully executes a tool call', async () => {
const toolCall = {
id: 'call-123',
2025-07-16 02:19:49 +00:00
type: 'function',
2025-07-16 00:12:41 +00:00
function: {
name: 'test-tool',
2025-07-24 19:11:15 +10:00
arguments: '{"param": "value"}',
},
2025-07-16 00:12:41 +00:00
}
const toolResult = {
2025-07-24 19:11:15 +10:00
content: [{type: 'text', text: 'Tool execution result'}],
2025-07-16 00:12:41 +00:00
}
mockCallTool.mockResolvedValue(toolResult)
const result = await executeToolCall(mockClient, toolCall)
expect(mockCallTool).toHaveBeenCalledWith({
name: 'test-tool',
2025-07-24 19:11:15 +10:00
arguments: {param: 'value'},
2025-07-16 00:12:41 +00:00
})
expect(result).toEqual({
tool_call_id: 'call-123',
role: 'tool',
name: 'test-tool',
2025-07-24 19:11:15 +10:00
content: JSON.stringify(toolResult.content),
2025-07-16 00:12:41 +00:00
})
expect(core.debug).toHaveBeenCalledWith('Executing GitHub MCP tool: test-tool with args: {"param": "value"}')
expect(core.debug).toHaveBeenCalledWith('GitHub MCP tool test-tool executed successfully')
2025-07-16 00:12:41 +00:00
})
it('handles tool execution errors gracefully', async () => {
const toolCall = {
id: 'call-456',
2025-07-16 02:19:49 +00:00
type: 'function',
2025-07-16 00:12:41 +00:00
function: {
name: 'failing-tool',
2025-07-24 19:11:15 +10:00
arguments: '{"param": "value"}',
},
2025-07-16 00:12:41 +00:00
}
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',
2025-07-24 19:11:15 +10:00
content: 'Error: Error: Tool execution failed',
2025-07-16 00:12:41 +00:00
})
expect(core.warning).toHaveBeenCalledWith(
2025-07-24 19:11:15 +10:00
'Failed to execute GitHub MCP tool failing-tool: Error: Tool execution failed',
2025-07-16 00:12:41 +00:00
)
})
it('handles invalid JSON arguments', async () => {
const toolCall = {
id: 'call-789',
2025-07-16 02:19:49 +00:00
type: 'function',
2025-07-16 00:12:41 +00:00
function: {
name: 'test-tool',
2025-07-24 19:11:15 +10:00
arguments: 'invalid-json',
},
2025-07-16 00:12:41 +00:00
}
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:')
2025-07-24 19:11:15 +10:00
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining('Failed to execute GitHub MCP tool test-tool:'))
2025-07-16 00:12:41 +00:00
})
})
describe('executeToolCalls', () => {
it('executes multiple tool calls successfully', async () => {
const toolCalls = [
{
id: 'call-1',
2025-07-16 02:19:49 +00:00
type: 'function',
2025-07-24 19:11:15 +10:00
function: {name: 'tool-1', arguments: '{}'},
2025-07-16 00:12:41 +00:00
},
{
id: 'call-2',
2025-07-16 02:19:49 +00:00
type: 'function',
2025-07-24 19:11:15 +10:00
function: {name: 'tool-2', arguments: '{"param": "value"}'},
},
2025-07-16 00:12:41 +00:00
]
mockCallTool
.mockResolvedValueOnce({
2025-07-24 19:11:15 +10:00
content: [{type: 'text', text: 'Result 1'}],
2025-07-16 00:12:41 +00:00
})
.mockResolvedValueOnce({
2025-07-24 19:11:15 +10:00
content: [{type: 'text', text: 'Result 2'}],
2025-07-16 00:12:41 +00:00
})
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',
2025-07-16 02:19:49 +00:00
type: 'function',
2025-07-24 19:11:15 +10:00
function: {name: 'tool-1', arguments: '{}'},
2025-07-16 00:12:41 +00:00
},
{
id: 'call-2',
2025-07-16 02:19:49 +00:00
type: 'function',
2025-07-24 19:11:15 +10:00
function: {name: 'tool-2', arguments: '{}'},
},
2025-07-16 00:12:41 +00:00
]
mockCallTool
.mockResolvedValueOnce({
2025-07-24 19:11:15 +10:00
content: [{type: 'text', text: 'Result 1'}],
2025-07-16 00:12:41 +00:00
})
.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:')
})
})
})