import crypto from 'node:crypto'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; // Type guard for structured content with result property function hasResult(obj: unknown): obj is { result: string } { return ( typeof obj === 'object' && obj !== null && 'result' in obj && typeof (obj as Record).result === 'string' ); } import { buildMcpToolErrorResponse, buildMcpToolSuccessResponse, createToolWorkspace, formatPackToolResponse, generateOutputId, getOutputFilePath, registerOutputFile, } from '../../../src/mcp/tools/mcpToolRuntime.js'; vi.mock('node:fs/promises'); vi.mock('node:path'); vi.mock('node:os'); vi.mock('node:crypto'); describe('mcpToolRuntime', () => { beforeEach(() => { vi.resetAllMocks(); }); describe('registerOutputFile and getOutputFilePath', () => { it('should register and retrieve output file paths', () => { const id = 'test-id'; const filePath = '/path/to/file.txt'; registerOutputFile(id, filePath); const retrievedPath = getOutputFilePath(id); expect(retrievedPath).toBe(filePath); }); it('should return undefined for non-existent output ID', () => { const retrievedPath = getOutputFilePath('non-existent-id'); expect(retrievedPath).toBeUndefined(); }); }); describe('createToolWorkspace', () => { beforeEach(() => { vi.mocked(os.tmpdir).mockReturnValue('/tmp'); vi.mocked(path.join).mockImplementation((...args) => args.join('/')); vi.mocked(fs.mkdir).mockResolvedValue(undefined); vi.mocked(fs.mkdtemp).mockResolvedValue('/tmp/repomix/mcp-outputs/temp-dir'); }); it('should create a temporary directory for tool operations', async () => { const tempDir = await createToolWorkspace(); expect(os.tmpdir).toHaveBeenCalled(); // path.join is now invoked twice: once inside shared/tmpDir.getRepomixTmpDir // to build the umbrella, then again here to append the mcp-outputs subdir. expect(path.join).toHaveBeenCalledWith('/tmp', 'repomix'); expect(path.join).toHaveBeenCalledWith('/tmp/repomix', 'mcp-outputs'); expect(fs.mkdir).toHaveBeenCalledWith('/tmp/repomix/mcp-outputs', { recursive: true }); expect(fs.mkdtemp).toHaveBeenCalledWith('/tmp/repomix/mcp-outputs/'); expect(tempDir).toBe('/tmp/repomix/mcp-outputs/temp-dir'); }); it('should throw an error if directory creation fails', async () => { const error = new Error('Failed to create directory'); vi.mocked(fs.mkdir).mockRejectedValue(error); await expect(createToolWorkspace()).rejects.toThrow( 'Failed to create temporary directory: Failed to create directory', ); }); }); describe('generateOutputId', () => { it('should generate a unique output ID', () => { vi.mocked(crypto.randomBytes).mockImplementation(() => ({ toString: () => 'abcdef1234567890', })); const outputId = generateOutputId(); expect(crypto.randomBytes).toHaveBeenCalledWith(8); expect(outputId).toBe('abcdef1234567890'); }); }); describe('formatToolResponse', () => { beforeEach(() => { vi.mocked(crypto.randomBytes).mockImplementation(() => ({ toString: () => 'abcdef1234567890', })); vi.mocked(fs.readFile).mockResolvedValue('Line 1\nLine 2\nLine 3\nLine 4\nLine 5'); }); it('should format a tool response with directory context', async () => { const context = { directory: '/path/to/dir' }; const metrics = { totalFiles: 10, totalCharacters: 1000, totalTokens: 200, totalLines: 0, // Will be calculated in formatToolResponse fileCharCounts: { 'file1.js': 500, 'file2.js': 300, 'file3.js': 200, }, fileTokenCounts: { 'file1.js': 100, 'file2.js': 60, 'file3.js': 40, }, processedFiles: [], safeFilePaths: [], }; const outputFilePath = '/path/to/output.xml'; const response = await formatPackToolResponse(context, metrics, outputFilePath); expect(response).toHaveProperty('content'); expect(response.content).toHaveLength(1); expect(response.content[0].type).toBe('text'); // Check that the structured content contains the expected result JSON const structuredContent = response.structuredContent; expect(structuredContent).toHaveProperty('result'); if (!hasResult(structuredContent)) { throw new Error('Expected structuredContent to have a result property of type string'); } const resultJson = JSON.parse(structuredContent.result); expect(resultJson.directory).toBe('/path/to/dir'); expect(resultJson.outputId).toBe('abcdef1234567890'); expect(resultJson.metrics.totalFiles).toBe(10); expect(resultJson.metrics.totalLines).toBe(5); expect(response).toHaveProperty('structuredContent'); expect(response.structuredContent).toHaveProperty('result'); expect(response.structuredContent).toHaveProperty('description'); expect(response.structuredContent).toHaveProperty('directoryStructure'); }); it('should format a tool response with repository context', async () => { const context = { repository: 'user/repo' }; const metrics = { totalFiles: 5, totalCharacters: 500, totalTokens: 100, totalLines: 0, // Will be calculated in formatToolResponse fileCharCounts: { 'file1.js': 300, 'file2.js': 200, }, fileTokenCounts: { 'file1.js': 60, 'file2.js': 40, }, processedFiles: [], safeFilePaths: [], }; const outputFilePath = '/path/to/output.xml'; const response = await formatPackToolResponse(context, metrics, outputFilePath); // Check that the structured content contains the expected result JSON const structuredContent = response.structuredContent; expect(structuredContent).toHaveProperty('result'); if (!hasResult(structuredContent)) { throw new Error('Expected structuredContent to have a result property of type string'); } const resultJson = JSON.parse(structuredContent.result); expect(resultJson.repository).toBe('user/repo'); expect(resultJson.directory).toBeUndefined(); expect(resultJson.metrics.totalLines).toBe(5); }); it('should limit the number of top files based on the parameter', async () => { const context = {}; const metrics = { totalFiles: 10, totalCharacters: 1000, totalTokens: 200, totalLines: 0, // Will be calculated in formatToolResponse fileCharCounts: { 'file1.js': 500, 'file2.js': 300, 'file3.js': 200, 'file4.js': 100, 'file5.js': 50, 'file6.js': 25, }, fileTokenCounts: { 'file1.js': 100, 'file2.js': 60, 'file3.js': 40, 'file4.js': 20, 'file5.js': 10, 'file6.js': 5, }, processedFiles: [], safeFilePaths: [], }; const outputFilePath = '/path/to/output.xml'; const topFilesLen = 3; const response = await formatPackToolResponse(context, metrics, outputFilePath, topFilesLen); // Check that the structured content contains the expected result JSON const structuredContent = response.structuredContent; expect(structuredContent).toHaveProperty('result'); if (!hasResult(structuredContent)) { throw new Error('Expected structuredContent to have a result property of type string'); } const result = JSON.parse(structuredContent.result); expect(result.metrics.topFiles).toHaveLength(3); expect(result.metrics.topFiles[0].path).toBe('file1.js'); expect(result.metrics.topFiles[1].path).toBe('file2.js'); expect(result.metrics.topFiles[2].path).toBe('file3.js'); expect(result.metrics.totalLines).toBe(5); }); }); describe('buildMcpToolSuccessResponse', () => { it('should create a successful response with structured content', () => { const structuredContent = { description: 'Operation completed successfully' }; const response = buildMcpToolSuccessResponse(structuredContent); expect(response).toEqual({ content: [ { type: 'text', text: JSON.stringify(structuredContent, null, 2), }, ], structuredContent: structuredContent, }); expect(response.isError).toBeUndefined(); }); it('should create a successful response with complex structured content', () => { const structuredContent = { description: 'Operation completed successfully', results: ['First result', 'Second result', 'Third result'], count: 3, }; const response = buildMcpToolSuccessResponse(structuredContent); expect(response).toEqual({ content: [ { type: 'text', text: JSON.stringify(structuredContent, null, 2), }, ], structuredContent: structuredContent, }); expect(response.isError).toBeUndefined(); }); it('should create a successful response with undefined structured content', () => { const structuredContent = undefined; const response = buildMcpToolSuccessResponse(structuredContent); expect(response).toEqual({ content: [ { type: 'text', text: 'null', }, ], structuredContent: structuredContent, }); expect(response.isError).toBeUndefined(); }); }); describe('buildMcpToolErrorResponse', () => { it('should create an error response with structured content', () => { const errorContent = { message: 'Something went wrong' }; const response = buildMcpToolErrorResponse(errorContent); expect(response).toEqual({ isError: true, content: [ { type: 'text', text: JSON.stringify(errorContent, null, 2), }, ], }); }); it('should create an error response with complex structured content', () => { const errorContent = { message: 'Multiple errors occurred', errors: ['Error 1', 'Error 2', 'Error 3'], code: 'MULTIPLE_ERRORS', }; const response = buildMcpToolErrorResponse(errorContent); expect(response).toEqual({ isError: true, content: [ { type: 'text', text: JSON.stringify(errorContent, null, 2), }, ], }); }); it('should create an error response with undefined structured content', () => { const errorContent = undefined; const response = buildMcpToolErrorResponse(errorContent); expect(response).toEqual({ isError: true, content: [ { type: 'text', text: 'null', }, ], structuredContent: errorContent, }); }); }); });