mirror of
https://github.com/yamadashy/repomix.git
synced 2026-05-30 11:18:53 +02:00
a94ce0f2ff
Vitest v4 changed how vi.fn() and vi.mock() work with class constructors. Arrow functions in mockImplementation no longer work as constructors when called with 'new' keyword. Changes: - Use regular function syntax instead of arrow functions for constructor mocks - Use vi.hoisted() to define class mocks that can be used in vi.mock() factories - Replace vi.fn().mockReturnValue() with vi.fn().mockImplementation() for class mocks - Update mock instance retrieval to use vi.mocked().mock.results[0].value
285 lines
10 KiB
TypeScript
285 lines
10 KiB
TypeScript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
import { getVersion } from '../../src/core/file/packageJsonParse.js';
|
|
import { createMcpServer, runMcpServer } from '../../src/mcp/mcpServer.js';
|
|
import { logger } from '../../src/shared/logger.js';
|
|
|
|
// Use vi.hoisted for class mocks that need to work as constructors
|
|
const { MockMcpServer, MockStdioServerTransport } = vi.hoisted(() => {
|
|
const createMockServerInstance = () => ({
|
|
tool: vi.fn().mockReturnThis(),
|
|
prompt: vi.fn().mockReturnThis(),
|
|
registerTool: vi.fn().mockReturnThis(),
|
|
registerPrompt: vi.fn().mockReturnThis(),
|
|
connect: vi.fn().mockResolvedValue(undefined),
|
|
close: vi.fn().mockResolvedValue(undefined),
|
|
// Add other required props
|
|
server: {},
|
|
_registeredResources: new Map(),
|
|
_registeredResourceTemplates: new Map(),
|
|
_registeredTools: new Map(),
|
|
_registeredPrompts: new Map(),
|
|
_registeredAgents: new Map(),
|
|
_registeredInstructions: new Map(),
|
|
_registeredRoutes: new Map(),
|
|
_registeredSessions: new Map(),
|
|
_registeredSockets: new Map(),
|
|
_registeredStreams: new Map(),
|
|
_registeredWebSockets: new Map(),
|
|
_registeredWorkers: new Map(),
|
|
_toolHandlersInitialized: false,
|
|
_completionHandlerInitialized: false,
|
|
setToolRequestHandlers: vi.fn(),
|
|
setCompletionRequestHandler: vi.fn(),
|
|
_messageHandler: vi.fn(),
|
|
_transport: {},
|
|
_connected: false,
|
|
_completionHandler: vi.fn(),
|
|
_toolHandlers: new Map(),
|
|
_validateHandlersInitialized: vi.fn(),
|
|
handlePromptCompletion: vi.fn(),
|
|
handleResourceCompletion: vi.fn(),
|
|
_resourceHandlersInitialized: false,
|
|
setResourceRequestHandlers: vi.fn(),
|
|
_resourceHandlers: new Map(),
|
|
});
|
|
|
|
// Wrap in vi.fn() to enable mockImplementation, using regular function for constructor
|
|
const MockMcpServer = vi.fn().mockImplementation(function (this: unknown) {
|
|
const instance = createMockServerInstance();
|
|
Object.assign(this as object, instance);
|
|
Object.assign(this as object, {
|
|
server: {},
|
|
_registeredResources: new Map(),
|
|
_registeredResourceTemplates: new Map(),
|
|
_registeredTools: new Map(),
|
|
_registeredPrompts: new Map(),
|
|
_registeredAgents: new Map(),
|
|
_registeredInstructions: new Map(),
|
|
_registeredRoutes: new Map(),
|
|
_registeredSessions: new Map(),
|
|
_registeredSockets: new Map(),
|
|
_registeredStreams: new Map(),
|
|
_registeredWebSockets: new Map(),
|
|
_registeredWorkers: new Map(),
|
|
_toolHandlersInitialized: false,
|
|
_completionHandlerInitialized: false,
|
|
setToolRequestHandlers: vi.fn(),
|
|
setCompletionRequestHandler: vi.fn(),
|
|
_messageHandler: vi.fn(),
|
|
_transport: {},
|
|
_connected: false,
|
|
_completionHandler: vi.fn(),
|
|
_toolHandlers: new Map(),
|
|
_validateHandlersInitialized: vi.fn(),
|
|
handlePromptCompletion: vi.fn(),
|
|
handleResourceCompletion: vi.fn(),
|
|
_resourceHandlersInitialized: false,
|
|
setResourceRequestHandlers: vi.fn(),
|
|
_resourceHandlers: new Map(),
|
|
});
|
|
return this;
|
|
});
|
|
|
|
const MockStdioServerTransport = vi.fn().mockImplementation(function (this: unknown) {
|
|
return this;
|
|
});
|
|
|
|
return { MockMcpServer, MockStdioServerTransport };
|
|
});
|
|
|
|
// Mock dependencies
|
|
vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
|
|
McpServer: MockMcpServer,
|
|
}));
|
|
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
|
|
StdioServerTransport: MockStdioServerTransport,
|
|
}));
|
|
vi.mock('../../src/core/file/packageJsonParse.js');
|
|
vi.mock('../../src/shared/logger.js');
|
|
|
|
describe('MCP Server', () => {
|
|
let mockExit: ReturnType<typeof vi.fn<(code?: number) => never>>;
|
|
const mockVersion = '1.0.0';
|
|
|
|
const mockLogger = {
|
|
trace: vi.fn(),
|
|
error: vi.fn(),
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
vi.resetAllMocks();
|
|
mockExit = vi.fn();
|
|
vi.mocked(getVersion).mockResolvedValue(mockVersion);
|
|
|
|
// ロガーのモックを設定
|
|
vi.mocked(logger.trace).mockImplementation(mockLogger.trace);
|
|
vi.mocked(logger.error).mockImplementation(mockLogger.error);
|
|
|
|
// Reset MockMcpServer to default implementation
|
|
vi.mocked(McpServer).mockImplementation(function (this: unknown) {
|
|
Object.assign(this as object, {
|
|
tool: vi.fn().mockReturnThis(),
|
|
prompt: vi.fn().mockReturnThis(),
|
|
registerTool: vi.fn().mockReturnThis(),
|
|
registerPrompt: vi.fn().mockReturnThis(),
|
|
connect: vi.fn().mockResolvedValue(undefined),
|
|
close: vi.fn().mockResolvedValue(undefined),
|
|
server: {},
|
|
_registeredResources: new Map(),
|
|
_registeredResourceTemplates: new Map(),
|
|
_registeredTools: new Map(),
|
|
_registeredPrompts: new Map(),
|
|
_registeredAgents: new Map(),
|
|
_registeredInstructions: new Map(),
|
|
_registeredRoutes: new Map(),
|
|
_registeredSessions: new Map(),
|
|
_registeredSockets: new Map(),
|
|
_registeredStreams: new Map(),
|
|
_registeredWebSockets: new Map(),
|
|
_registeredWorkers: new Map(),
|
|
_toolHandlersInitialized: false,
|
|
_completionHandlerInitialized: false,
|
|
setToolRequestHandlers: vi.fn(),
|
|
setCompletionRequestHandler: vi.fn(),
|
|
_messageHandler: vi.fn(),
|
|
_transport: {},
|
|
_connected: false,
|
|
_completionHandler: vi.fn(),
|
|
_toolHandlers: new Map(),
|
|
_validateHandlersInitialized: vi.fn(),
|
|
handlePromptCompletion: vi.fn(),
|
|
handleResourceCompletion: vi.fn(),
|
|
_resourceHandlersInitialized: false,
|
|
setResourceRequestHandlers: vi.fn(),
|
|
_resourceHandlers: new Map(),
|
|
});
|
|
return this;
|
|
});
|
|
|
|
// Reset MockStdioServerTransport to default implementation
|
|
vi.mocked(StdioServerTransport).mockImplementation(function (this: unknown) {
|
|
return this;
|
|
});
|
|
});
|
|
|
|
describe('createMcpServer', () => {
|
|
test('should create server with correct configuration', async () => {
|
|
const server = await createMcpServer();
|
|
|
|
expect(McpServer).toHaveBeenCalledWith(
|
|
{
|
|
name: 'repomix-mcp-server',
|
|
version: mockVersion,
|
|
},
|
|
{
|
|
instructions: expect.stringContaining('Repomix MCP Server provides AI-optimized codebase analysis tools'),
|
|
},
|
|
);
|
|
expect(server).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('runMcpServer', () => {
|
|
test('should connect server with stdio transport', async () => {
|
|
await expect(runMcpServer({ processExit: mockExit })).resolves.toBeUndefined();
|
|
|
|
expect(StdioServerTransport).toHaveBeenCalled();
|
|
// Get the mock server instance from the constructor call
|
|
const mockServer = vi.mocked(McpServer).mock.results[0]?.value;
|
|
expect(mockServer?.connect).toHaveBeenCalled();
|
|
expect(logger.trace).toHaveBeenCalledWith('Repomix MCP Server running on stdio');
|
|
});
|
|
|
|
test('should handle connection error', async () => {
|
|
const error = new Error('Connection failed');
|
|
|
|
// Pre-configure the mock to reject on connect
|
|
const mockServerWithError = {
|
|
tool: vi.fn().mockReturnThis(),
|
|
prompt: vi.fn().mockReturnThis(),
|
|
registerTool: vi.fn().mockReturnThis(),
|
|
registerPrompt: vi.fn().mockReturnThis(),
|
|
connect: vi.fn().mockRejectedValue(error),
|
|
close: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
|
|
// Override the constructor temporarily
|
|
vi.mocked(McpServer).mockImplementation(function (this: unknown) {
|
|
Object.assign(this as object, mockServerWithError);
|
|
return this as McpServer;
|
|
});
|
|
|
|
try {
|
|
await runMcpServer({ processExit: mockExit });
|
|
} catch (e) {
|
|
expect(e).toEqual(error);
|
|
}
|
|
|
|
expect(logger.error).toHaveBeenCalledWith('Failed to start MCP server:', error);
|
|
expect(mockExit).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
test('should handle SIGINT signal', async () => {
|
|
await expect(runMcpServer({ processExit: mockExit })).resolves.toBeUndefined();
|
|
// Get the mock server instance from the constructor call
|
|
const mockServer = vi.mocked(McpServer).mock.results[0]?.value;
|
|
mockLogger.trace.mockClear();
|
|
process.emit('SIGINT');
|
|
|
|
// 非同期処理の完了を待つ
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
expect(mockServer?.close).toHaveBeenCalled();
|
|
expect(logger.trace).toHaveBeenCalledWith('Repomix MCP Server shutdown complete');
|
|
expect(mockExit).toHaveBeenCalledWith(0);
|
|
});
|
|
|
|
test('should handle SIGTERM signal', async () => {
|
|
await expect(runMcpServer({ processExit: mockExit })).resolves.toBeUndefined();
|
|
// Get the mock server instance from the constructor call
|
|
const mockServer = vi.mocked(McpServer).mock.results[0]?.value;
|
|
mockLogger.trace.mockClear();
|
|
process.emit('SIGTERM');
|
|
|
|
// 非同期処理の完了を待つ
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
expect(mockServer?.close).toHaveBeenCalled();
|
|
expect(logger.trace).toHaveBeenCalledWith('Repomix MCP Server shutdown complete');
|
|
expect(mockExit).toHaveBeenCalledWith(0);
|
|
});
|
|
|
|
test('should handle shutdown error', async () => {
|
|
const error = new Error('Shutdown failed');
|
|
|
|
// Pre-configure the mock to reject on close
|
|
const mockServerWithCloseError = {
|
|
tool: vi.fn().mockReturnThis(),
|
|
prompt: vi.fn().mockReturnThis(),
|
|
registerTool: vi.fn().mockReturnThis(),
|
|
registerPrompt: vi.fn().mockReturnThis(),
|
|
connect: vi.fn().mockResolvedValue(undefined),
|
|
close: vi.fn().mockRejectedValue(error),
|
|
};
|
|
|
|
// Override the constructor temporarily
|
|
vi.mocked(McpServer).mockImplementation(function (this: unknown) {
|
|
Object.assign(this as object, mockServerWithCloseError);
|
|
return this as McpServer;
|
|
});
|
|
|
|
await expect(runMcpServer({ processExit: mockExit })).resolves.toBeUndefined();
|
|
mockLogger.error.mockClear();
|
|
process.emit('SIGINT');
|
|
|
|
// 非同期処理の完了を待つ
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
expect(logger.error).toHaveBeenCalledWith('Error during MCP server shutdown:', error);
|
|
expect(mockExit).toHaveBeenCalledWith(1);
|
|
});
|
|
});
|
|
});
|