UNPKG

lamplighter-mcp

Version:

An intelligent context engine for AI-assisted software development

234 lines (201 loc) 8.37 kB
import { CodebaseAnalyzer } from '../src/modules/codebaseAnalyzer'; import * as fsPromises from 'fs/promises'; import * as fs from 'fs'; // Import base fs for types import * as path from 'path'; // Mock fs/promises module jest.mock('fs/promises'); const mockedFs = jest.mocked(fsPromises); // Helper to create Dirent objects for readdir mock const createDirent = (name: string, isDirectory: boolean): fs.Dirent => ({ name, isFile: () => !isDirectory, isDirectory: () => isDirectory, isBlockDevice: () => false, isCharacterDevice: () => false, isSymbolicLink: () => false, isFIFO: () => false, isSocket: () => false, } as fs.Dirent); // Use type assertion if necessary, or ensure all fields match // Helper to create Stats objects for stat mock const createStats = (isDirectory: boolean): fs.Stats => ({ isFile: () => !isDirectory, isDirectory: () => isDirectory, isBlockDevice: () => false, isCharacterDevice: () => false, isSymbolicLink: () => false, isFIFO: () => false, isSocket: () => false, dev: 0, ino: 0, mode: 0, nlink: 0, uid: 0, gid: 0, rdev: 0, size: 0, blksize: 0, blocks: 0, atimeMs: 0, mtimeMs: 0, ctimeMs: 0, birthtimeMs: 0, atime: new Date(), mtime: new Date(), ctime: new Date(), birthtime: new Date(), } as fs.Stats); // Use type assertion describe('CodebaseAnalyzer', () => { const defaultContextDir = './lamplighter_context'; const summaryFileName = 'codebase_summary.md'; const defaultSummaryPath = path.join(defaultContextDir, summaryFileName); beforeEach(() => { jest.clearAllMocks(); // Default mock implementations mockedFs.mkdir.mockResolvedValue(undefined); mockedFs.writeFile.mockResolvedValue(undefined); mockedFs.access.mockResolvedValue(undefined); // Assume files exist by default // Reset readdir and stat mocks too if they are complex mockedFs.readdir.mockReset(); mockedFs.stat.mockReset(); }); const setupMockFs = (structure: Record<string, any>) => { const getPathData = (targetPath: string): { item: any; isDir: boolean } | null => { const normalizedTargetPath = path.normalize(targetPath.toString()); // Function to traverse the mock structure const traverse = (structureObj: any, pathParts: string[]): any => { if (!pathParts.length) { return structureObj; // Reached the target } const currentPart = pathParts[0]; if (structureObj && typeof structureObj === 'object' && currentPart in structureObj) { return traverse(structureObj[currentPart], pathParts.slice(1)); } else { return null; // Part not found } }; // Find the root key in the structure (e.g., '/project') const rootKey = Object.keys(structure).find(key => normalizedTargetPath.startsWith(path.normalize(key))); if (!rootKey) { return null; // Root path not found in structure } const relativePath = path.relative(rootKey, normalizedTargetPath); const parts = relativePath.split(path.sep).filter(p => p && p !== '.'); let item; if (relativePath === '.' || relativePath === '') { item = structure[rootKey]; // Requesting the root itself } else { item = traverse(structure[rootKey], parts); } if (item !== null && item !== undefined) { return { item, isDir: typeof item === 'object' }; } return null; }; mockedFs.readdir.mockImplementation(async (dirPath): Promise<fs.Dirent[]> => { const data = getPathData(dirPath.toString()); if (!data || !data.isDir) { throw Object.assign(new Error(`ENOENT: no such file or directory, scandir '${dirPath}'`), { code: 'ENOENT' }); } return Object.keys(data.item).map(name => createDirent(name, typeof data.item[name] === 'object')); }); mockedFs.stat.mockImplementation(async (itemPath): Promise<fs.Stats> => { const data = getPathData(itemPath.toString()); if (data === null) { throw Object.assign(new Error(`ENOENT: no such file or directory, stat '${itemPath}'`), { code: 'ENOENT' }); } return createStats(data.isDir); }); mockedFs.access.mockImplementation(async (filePath) => { // Just rely on getPathData to see if the path resolves to anything const data = getPathData(filePath.toString()); if (data === null) { throw Object.assign(new Error(`ENOENT: no such file or directory, access '${filePath}'`), { code: 'ENOENT' }); } }); }; it('should analyze a simple project structure and detect package.json', async () => { const projectRoot = '/project'; const mockStructure = { [projectRoot]: { 'src': { 'server.ts': 'file_content' }, 'package.json': 'file_content' } }; setupMockFs(mockStructure); const analyzer = new CodebaseAnalyzer(); await analyzer.analyze(projectRoot); expect(mockedFs.writeFile).toHaveBeenCalledTimes(1); expect(mockedFs.writeFile).toHaveBeenCalledWith(defaultSummaryPath, expect.any(String), 'utf-8'); const summaryContent = mockedFs.writeFile.mock.calls[0][1]; expect(summaryContent).toContain('nodejs** (detected from: package.json)'); expect(summaryContent).toContain('📁 src/'); expect(summaryContent).toContain('📄 server.ts'); expect(summaryContent).toContain('📄 package.json'); }); it('should analyze nested directories and detect tsconfig.json', async () => { const projectRoot = '/project'; const mockStructure = { [projectRoot]: { 'src': { 'modules': { 'logger.ts': 'file_content' } }, 'tsconfig.json': 'file_content' } }; setupMockFs(mockStructure); const analyzer = new CodebaseAnalyzer(); await analyzer.analyze(projectRoot); const summaryContent = mockedFs.writeFile.mock.calls[0][1]; expect(summaryContent).toContain('typescript** (detected from: tsconfig.json)'); expect(summaryContent).toContain('📁 src/'); expect(summaryContent).toContain('📁 modules/'); expect(summaryContent).toContain('📄 logger.ts'); expect(summaryContent).toContain('📄 tsconfig.json'); expect(summaryContent).not.toContain('nodejs'); }); it('should handle an empty directory', async () => { const projectRoot = '/emptyProject'; const mockStructure = { [projectRoot]: {} }; setupMockFs(mockStructure); const analyzer = new CodebaseAnalyzer(); await analyzer.analyze(projectRoot); expect(mockedFs.writeFile).toHaveBeenCalledTimes(1); const summaryContent = mockedFs.writeFile.mock.calls[0][1]; expect(summaryContent).toContain('# Codebase Summary'); expect(summaryContent).toContain('## Tech Stack'); // Should still have headers expect(summaryContent).toContain('## Directory Structure'); }); it('should handle errors during file operations gracefully', async () => { const projectRoot = '/projectWithError'; const mockStructure = { [projectRoot]: { 'src': {} } }; setupMockFs(mockStructure); const readError = new Error('Permission denied'); mockedFs.readdir.mockImplementation(async (dirPath) => { const normalizedPath = path.normalize(dirPath.toString()); if (normalizedPath === projectRoot) { throw readError; } // Simulate findFilesByExtension also failing if it tries to read other dirs // Or just throw a generic ENOENT if unexpected path is read throw Object.assign(new Error(`ENOENT: unexpected readdir for ${dirPath}`), { code: 'ENOENT' }); }); const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); const analyzer = new CodebaseAnalyzer(); await expect(analyzer.analyze(projectRoot)).resolves.toBeUndefined(); // Check if writeFile was still called with a basic summary containing error info expect(mockedFs.writeFile).toHaveBeenCalledTimes(1); const summaryContent = mockedFs.writeFile.mock.calls[0][1]; expect(summaryContent).toContain('# Codebase Summary'); consoleErrorSpy.mockRestore(); }); });