292 lines
8.3 KiB
TypeScript
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:')
|
|
})
|
|
})
|
|
})
|