UNPKG

mcp-continuity-tool

Version:

A tool for maintaining context and continuity in MCP-based development across multiple chat sessions

409 lines (351 loc) 19.7 kB
const http = require('http'); const { Readable } = require('stream'); // For mocking request body stream // Mock dependent modules jest.mock('../scripts/state_manager'); jest.mock('../scripts/auto_setup'); // Mock http.createServer const mockListen = jest.fn(); const mockClose = jest.fn(callback => { if (callback) callback(); }); // Mock server.close const mockServer = { listen: mockListen, close: mockClose, // Add close mock }; let requestHandler; // To store the handler function passed to createServer // We need to capture the request handler passed to http.createServer // The actual server module ('../index.js') will call createServer when it's required. jest.mock('http', () => ({ ...jest.requireActual('http'), // Import and retain default behavior if needed for other parts createServer: jest.fn(handler => { requestHandler = handler; // Capture the handler return mockServer; }), })); // Import functions from mocked modules to set up their behavior const { loadProjectState, updateProjectState, generateContinuityPrompt } = require('../scripts/state_manager'); const { initializeEnvironment } = require('../scripts/auto_setup'); describe('MCP Server (index.js)', () => { let mockReq; let mockRes; let originalArgv; let serverInstance; // To hold the server instance from index.js for closing beforeAll(() => { originalArgv = [...process.argv]; // Store original argv // Require the server module AFTER mocks are set up. // This will execute index.js, calling our mocked http.createServer. serverInstance = require('../index.js'); // serverInstance will be the module exports if any, or the server itself }); afterAll(() => { process.argv = originalArgv; // Restore original argv // Ensure server is closed if it was started by index.js if (serverInstance && serverInstance.close) { // index.js doesn't export server, http.mockServer handles it mockServer.close(); // Use the mockServer's close } }); beforeEach(() => { // Reset mocks for service functions and response methods loadProjectState.mockReset(); updateProjectState.mockReset(); generateContinuityPrompt.mockReset(); initializeEnvironment.mockReset(); mockListen.mockClear(); mockClose.mockClear(); // Clear close mock calls // Basic mock response object mockRes = { writeHead: jest.fn(), end: jest.fn(), on: jest.fn(), // For parseJsonBody if it uses req.on for errors }; // Reset process.argv to a default state for port parsing tests if needed process.argv = [...originalArgv.slice(0, 2)]; // node executable and script name }); // Test for server listening it('should start the server and listen on the configured PORT (default 3000 if no args/env)', () => { expect(http.createServer).toHaveBeenCalled(); expect(mockListen).toHaveBeenCalled(); // We can also check the port if needed, but it depends on how PORT is resolved in index.js // For now, just checking if listen was called is enough. // The first argument to the first call to listen const listenCallArgs = mockListen.mock.calls[0]; const usedPort = listenCallArgs[0]; expect(typeof usedPort).toBe('number'); // Should be a number }); describe('GET /', () => { it('should return a welcome message', () => { mockReq = { method: 'GET', url: '/' }; requestHandler(mockReq, mockRes); // Invoke the handler expect(mockRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ message: 'MCP Server is running. Use specific endpoints to interact.' })); }); }); describe('POST /initialize', () => { const mockRepoUrl = 'user/repo'; const mockRequestBody = { repositoryUrl: mockRepoUrl, workingDirectory: 'src' }; it('should call initializeEnvironment and return 200 on success', () => { const mockResult = { success: true, data: 'initialized' }; initializeEnvironment.mockReturnValue(mockResult); mockReq = { method: 'POST', url: '/initialize', on: (event, callback) => { if (event === 'data') callback(Buffer.from(JSON.stringify(mockRequestBody))); if (event === 'end') callback(); } }; requestHandler(mockReq, mockRes); expect(initializeEnvironment).toHaveBeenCalledWith(mockRepoUrl, 'src'); expect(mockRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify(mockResult)); }); it('should return 400 if repositoryUrl is missing', () => { mockReq = { method: 'POST', url: '/initialize', on: (event, callback) => { if (event === 'data') callback(Buffer.from(JSON.stringify({ workingDirectory: 'src' }))); // Missing repositoryUrl if (event === 'end') callback(); } }; requestHandler(mockReq, mockRes); expect(mockRes.writeHead).toHaveBeenCalledWith(400, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ error: 'Missing required field: repositoryUrl' })); }); it('should return 400 for invalid JSON in request body', () => { mockReq = { method: 'POST', url: '/initialize', on: (event, callback) => { if (event === 'data') callback(Buffer.from('this is not json')); if (event === 'end') callback(); } }; requestHandler(mockReq, mockRes); expect(mockRes.writeHead).toHaveBeenCalledWith(400, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(expect.stringContaining('Invalid JSON body:')); }); it('should return 500 if initializeEnvironment returns an error object', () => { const errorResult = { error: true, message: 'Initialization failed badly' }; initializeEnvironment.mockReturnValue(errorResult); mockReq = { method: 'POST', url: '/initialize', on: (event, callback) => { if (event === 'data') callback(Buffer.from(JSON.stringify(mockRequestBody))); if (event === 'end') callback(); } }; requestHandler(mockReq, mockRes); expect(initializeEnvironment).toHaveBeenCalledWith(mockRepoUrl, 'src'); expect(mockRes.writeHead).toHaveBeenCalledWith(500, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ error: 'Initialization failed badly' })); }); it('should return 500 if initializeEnvironment throws an exception', () => { initializeEnvironment.mockImplementation(() => { throw new Error('Critical failure'); }); mockReq = { method: 'POST', url: '/initialize', on: (event, callback) => { if (event === 'data') callback(Buffer.from(JSON.stringify(mockRequestBody))); if (event === 'end') callback(); } }; requestHandler(mockReq, mockRes); expect(initializeEnvironment).toHaveBeenCalledWith(mockRepoUrl, 'src'); expect(mockRes.writeHead).toHaveBeenCalledWith(500, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ error: 'Internal Server Error: Critical failure' })); }); }); describe('GET /state', () => { it('should call loadProjectState and return 200 with state', () => { const mockState = { project: 'test' }; const projectPath = 'custom/project-status.json'; loadProjectState.mockReturnValue(mockState); mockReq = { method: 'GET', url: `/state?projectPath=${encodeURIComponent(projectPath)}` }; requestHandler(mockReq, mockRes); expect(loadProjectState).toHaveBeenCalledWith(projectPath); expect(mockRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify(mockState)); }); it('should use default projectPath if not provided', () => { const mockState = { project: 'default' }; loadProjectState.mockReturnValue(mockState); mockReq = { method: 'GET', url: '/state' }; requestHandler(mockReq, mockRes); expect(loadProjectState).toHaveBeenCalledWith('project-status.json'); expect(mockRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify(mockState)); }); it('should return 500 if loadProjectState throws an error', () => { loadProjectState.mockImplementation(() => { throw new Error('Failed to load'); }); mockReq = { method: 'GET', url: '/state' }; requestHandler(mockReq, mockRes); expect(mockRes.writeHead).toHaveBeenCalledWith(500, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ error: 'Internal Server Error: Failed to load' })); }); }); describe('POST /state', () => { const mockUpdates = { newField: 'newValue' }; const projectPath = 'custom/project-status.json'; const mockRequestBody = { updates: mockUpdates, projectPath: projectPath }; it('should call updateProjectState and return 200 with updated state', () => { const mockUpdatedStateResult = { success: true, data: 'updated state' }; updateProjectState.mockReturnValue(mockUpdatedStateResult); mockReq = { method: 'POST', url: '/state', on: (event, callback) => { if (event === 'data') callback(Buffer.from(JSON.stringify(mockRequestBody))); if (event === 'end') callback(); } }; requestHandler(mockReq, mockRes); expect(updateProjectState).toHaveBeenCalledWith(mockUpdates, projectPath); expect(mockRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify(mockUpdatedStateResult)); }); it('should use default projectPath if not provided in body', () => { const mockUpdatedStateResult = { success: true, data: 'updated state' }; updateProjectState.mockReturnValue(mockUpdatedStateResult); const requestBodyNoPath = { updates: mockUpdates }; mockReq = { method: 'POST', url: '/state', on: (event, callback) => { if (event === 'data') callback(Buffer.from(JSON.stringify(requestBodyNoPath))); if (event === 'end') callback(); } }; requestHandler(mockReq, mockRes); expect(updateProjectState).toHaveBeenCalledWith(mockUpdates, 'project-status.json'); }); it('should return 400 if updates field is missing', () => { mockReq = { method: 'POST', url: '/state', on: (event, callback) => { if (event === 'data') callback(Buffer.from(JSON.stringify({ projectPath: projectPath }))); // Missing updates if (event === 'end') callback(); } }; requestHandler(mockReq, mockRes); expect(mockRes.writeHead).toHaveBeenCalledWith(400, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ error: 'Missing required field: updates' })); }); it('should return 400 for invalid JSON in request body', () => { mockReq = { method: 'POST', url: '/state', on: (event, callback) => { if (event === 'data') callback(Buffer.from('not valid json')); if (event === 'end') callback(); } }; requestHandler(mockReq, mockRes); expect(mockRes.writeHead).toHaveBeenCalledWith(400, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(expect.stringContaining('Invalid JSON body:')); }); it('should return 500 if updateProjectState returns an error object', () => { const errorResult = { success: false, error: 'Update failed badly' }; updateProjectState.mockReturnValue(errorResult); mockReq = { method: 'POST', url: '/state', on: (event, callback) => { if (event === 'data') callback(Buffer.from(JSON.stringify(mockRequestBody))); if (event === 'end') callback(); } }; requestHandler(mockReq, mockRes); expect(mockRes.writeHead).toHaveBeenCalledWith(500, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ error: 'Update failed badly' })); }); it('should return 500 if updateProjectState throws an exception', () => { updateProjectState.mockImplementation(() => { throw new Error('Critical update failure'); }); mockReq = { method: 'POST', url: '/state', on: (event, callback) => { if (event === 'data') callback(Buffer.from(JSON.stringify(mockRequestBody))); if (event === 'end') callback(); } }; requestHandler(mockReq, mockRes); expect(mockRes.writeHead).toHaveBeenCalledWith(500, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ error: 'Internal Server Error: Critical update failure' })); }); }); describe('GET /continuity-prompt', () => { const mockState = { projectInfo: { name: 'Test' } }; const mockPrompt = 'This is the continuity prompt.'; const projectPath = 'custom/path/status.json'; it('should load state, generate prompt, and return 200', () => { loadProjectState.mockReturnValue(mockState); generateContinuityPrompt.mockReturnValue(mockPrompt); mockReq = { method: 'GET', url: `/continuity-prompt?projectPath=${encodeURIComponent(projectPath)}` }; requestHandler(mockReq, mockRes); expect(loadProjectState).toHaveBeenCalledWith(projectPath); expect(generateContinuityPrompt).toHaveBeenCalledWith(mockState); expect(mockRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ prompt: mockPrompt })); }); it('should use default projectPath if not provided', () => { loadProjectState.mockReturnValue(mockState); generateContinuityPrompt.mockReturnValue(mockPrompt); mockReq = { method: 'GET', url: '/continuity-prompt' }; requestHandler(mockReq, mockRes); expect(loadProjectState).toHaveBeenCalledWith('project-status.json'); }); it('should return 500 if loadProjectState throws', () => { loadProjectState.mockImplementation(() => { throw new Error('Load failed for prompt'); }); mockReq = { method: 'GET', url: '/continuity-prompt' }; requestHandler(mockReq, mockRes); expect(mockRes.writeHead).toHaveBeenCalledWith(500, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ error: 'Internal Server Error: Load failed for prompt' })); expect(generateContinuityPrompt).not.toHaveBeenCalled(); }); it('should return 500 if generateContinuityPrompt throws', () => { loadProjectState.mockReturnValue(mockState); // Load succeeds generateContinuityPrompt.mockImplementation(() => { throw new Error('Prompt generation failed'); }); mockReq = { method: 'GET', url: '/continuity-prompt' }; requestHandler(mockReq, mockRes); expect(mockRes.writeHead).toHaveBeenCalledWith(500, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ error: 'Internal Server Error: Prompt generation failed' })); }); }); describe('Unknown route', () => { it('should return 404', () => { mockReq = { method: 'GET', url: '/unknown-route' }; requestHandler(mockReq, mockRes); expect(mockRes.writeHead).toHaveBeenCalledWith(404, { 'Content-Type': 'application/json' }); expect(mockRes.end).toHaveBeenCalledWith(JSON.stringify({ error: 'Not Found' })); }); }); // Test for port parsing (demonstrative - requires specific setup for index.js re-evaluation) describe('Port Configuration', () => { // This test is a bit more complex because index.js parses port on load. // We need to reset modules and re-require index.js with new process.argv // or inspect the arguments passed to mockListen. it('should use port from --port argument if provided', () => { jest.resetModules(); // Important to re-evaluate index.js process.argv = [...originalArgv.slice(0, 2), '--port', '4000']; // Re-mock http for this specific test context if needed, or ensure global mock handles it const localMockListen = jest.fn(); const localMockServer = { listen: localMockListen, close: jest.fn(cb => cb && cb()) }; jest.mock('http', () => ({ ...jest.requireActual('http'), createServer: jest.fn(() => localMockServer), })); require('../index.js'); // Re-require to parse new argv expect(localMockListen).toHaveBeenCalledWith(4000, expect.any(Function)); process.argv = originalArgv; // Restore argv jest.unmock('http'); // Unmock to not affect other tests, or restore original mock // Re-establish global mock for http for other tests if unmocked jest.mock('http', () => ({ ...jest.requireActual('http'), createServer: jest.fn(handler => { requestHandler = handler; return mockServer; // mockServer is the global one }), })); }); }); });