UNPKG

mcp-continuity-tool

Version:

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

322 lines (262 loc) 17.4 kB
const fs = require('fs'); const path = require('path'); const { setupProjectState, analyzeRepository, initializeEnvironment } = require('../scripts/auto_setup'); // Mock fs module jest.mock('fs'); // Mock state_manager module jest.mock('../scripts/state_manager', () => ({ loadProjectState: jest.fn(), saveProjectState: jest.fn(), // Though not directly called by auto_setup, it's part of the module generateContinuityPrompt: jest.fn(), })); // Import mocked functions from state_manager to use in tests if needed for assertions const { loadProjectState, saveProjectState: mockedSaveProjectState, generateContinuityPrompt } = require('../scripts/state_manager'); describe('Auto Setup', () => { const MOCK_REPO_URL = 'user/test-repo.git'; const MOCK_REPO_NAME = 'test-repo'; const DEFAULT_PROJECT_STATUS_FILENAME = 'project-status.json'; beforeEach(() => { // Reset all mocks before each test fs.readFileSync.mockReset(); fs.writeFileSync.mockReset(); fs.readdirSync.mockReset(); fs.statSync.mockReset(); fs.mkdirSync.mockReset(); loadProjectState.mockReset(); mockedSaveProjectState.mockReset(); // Reset our alias for the mocked saveProjectState generateContinuityPrompt.mockReset(); // Default behavior for statSync (exists and is a file) fs.statSync.mockImplementation((filePath) => { // console.log(`Mock fs.statSync called for: ${filePath}`); // Debugging // Default to file not found, specific tests can override const err = new Error(`ENOENT: no such file or directory, stat '${filePath}'`); err.code = 'ENOENT'; throw err; }); // Default behavior for readdirSync (empty directory) fs.readdirSync.mockReturnValue([]); }); describe('setupProjectState(repositoryUrl, workingDirectoryInput)', () => { it('should load existing state if project-status.json exists', () => { const mockExistingState = { projectInfo: { name: MOCK_REPO_NAME, repository: MOCK_REPO_URL } }; loadProjectState.mockReturnValue(mockExistingState); const workingDir = 'existing_project'; const resolvedWorkingDir = path.resolve(workingDir); const resolvedProjectStatusPath = path.join(resolvedWorkingDir, DEFAULT_PROJECT_STATUS_FILENAME); // Mock that workingDir exists fs.statSync.mockImplementation((p) => { if (p === resolvedWorkingDir) return { isDirectory: () => true }; const err = new Error(`ENOENT: no such file or directory, stat '${p}'`); err.code = 'ENOENT'; throw err; }); const state = setupProjectState(MOCK_REPO_URL, workingDir); expect(loadProjectState).toHaveBeenCalledWith(resolvedProjectStatusPath); expect(fs.writeFileSync).not.toHaveBeenCalled(); // Should not create a new file expect(state).toEqual(mockExistingState); }); it('should create new state if project-status.json does not exist', () => { loadProjectState.mockImplementation((p) => { // console.log(`loadProjectState mock called with ${p}, throwing ENOENT`); // Debug const err = new Error('File not found by loadProjectState mock'); err.code = 'ENOENT'; // Simulate file not found behavior for loadProjectState throw err; }); fs.readdirSync.mockReturnValue([ // Mock files in the directory { name: 'main.js', isFile: () => true, isDirectory: () => false }, { name: 'README.md', isFile: () => true, isDirectory: () => false }, ]); // Mock statSync for readdirSync's file check (if any, though not strictly needed by current auto_setup readdir logic) fs.statSync.mockImplementation((p) => { if (p.endsWith('main.js') || p.endsWith('README.md')) return { isDirectory: () => false }; const err = new Error(`ENOENT stat for ${p}`); err.code = 'ENOENT'; throw err; }); const workingDir = 'new_project'; const resolvedWorkingDir = path.resolve(workingDir); const resolvedProjectStatusPath = path.join(resolvedWorkingDir, DEFAULT_PROJECT_STATUS_FILENAME); // Mock that workingDir exists for the readdirSync call fs.statSync.mockImplementationOnce((p) => { // For the workingDir check itself if (p === resolvedWorkingDir) return { isDirectory: () => true }; const err = new Error(`ENOENT: no such file or directory, stat '${p}'`); err.code = 'ENOENT'; throw err; }); const state = setupProjectState(MOCK_REPO_URL, workingDir); expect(loadProjectState).toHaveBeenCalledWith(resolvedProjectStatusPath); expect(fs.writeFileSync).toHaveBeenCalledTimes(1); // For project-status.json const writtenPath = fs.writeFileSync.mock.calls[0][0]; const writtenData = JSON.parse(fs.writeFileSync.mock.calls[0][1]); expect(writtenPath).toBe(resolvedProjectStatusPath); expect(writtenData.projectInfo.name).toBe(MOCK_REPO_NAME); expect(writtenData.projectInfo.repository).toBe(MOCK_REPO_URL); expect(writtenData.projectInfo.workingDirectory).toBe(workingDir); expect(writtenData.development.currentFile).toBe('main.js'); // Based on readdirSync mock expect(state).toEqual(writtenData); }); it('should create workingDirectoryInput if it does not exist, then create project-status.json', () => { loadProjectState.mockImplementation(() => { const err = new Error('File not found by loadProjectState'); err.code = 'ENOENT'; throw err; }); fs.readdirSync.mockReturnValue([{ name: 'app.py', isFile: () => true, isDirectory: () => false }]); const newWorkingDir = 'non_existent_dir/project_src'; const resolvedNewWorkingDir = path.resolve(newWorkingDir); const resolvedProjectStatusPath = path.join(resolvedNewWorkingDir, DEFAULT_PROJECT_STATUS_FILENAME); const resolvedGitkeepPath = path.join(resolvedNewWorkingDir, '.gitkeep'); // Mock fs.statSync: first call for workingDir (not found), then for readdir (exists after creation) let statCallCount = 0; fs.statSync.mockImplementation((p) => { statCallCount++; if (statCallCount === 1 && p === resolvedNewWorkingDir) { // First call for the dir itself const err = new Error(`ENOENT: no such file or directory, stat '${p}'`); err.code = 'ENOENT'; throw err; } if (p === resolvedNewWorkingDir) { // Subsequent calls for the dir (e.g. by readdirSync) return { isDirectory: () => true }; } if (p.endsWith('app.py')) return {isDirectory: () => false}; const err = new Error(`ENOENT for path ${p} in statSync mock`); err.code = 'ENOENT'; throw err; }); const state = setupProjectState(MOCK_REPO_URL, newWorkingDir); expect(fs.mkdirSync).toHaveBeenCalledWith(resolvedNewWorkingDir, { recursive: true }); expect(fs.writeFileSync).toHaveBeenCalledWith(resolvedGitkeepPath, ''); // .gitkeep expect(fs.writeFileSync).toHaveBeenCalledWith(resolvedProjectStatusPath, expect.any(String)); // project-status.json const projectStatusWriteCall = fs.writeFileSync.mock.calls.find(call => call[0] === resolvedProjectStatusPath); const writtenData = JSON.parse(projectStatusWriteCall[1]); expect(writtenData.projectInfo.name).toBe(MOCK_REPO_NAME); expect(writtenData.projectInfo.workingDirectory).toBe(newWorkingDir); expect(writtenData.development.currentFile).toBe('app.py'); expect(state).toEqual(writtenData); }); it('should correctly extract repoName from various repositoryUrl formats', () => { loadProjectState.mockImplementation(() => { throw new Error('ENOENT'); }); // Ensure write path const urls = { 'user/my-repo.git': 'my-repo', 'https://github.com/user/another-repo.git': 'another-repo', 'git@gitlab.com:user/project-x.git': 'project-x', 'my-simple-repo': 'my-simple-repo' // No slashes }; for (const url in urls) { fs.writeFileSync.mockClear(); // Clear from previous iterations const expectedName = urls[url]; setupProjectState(url, ''); // No working dir, CWD is used for project-status.json // Check the name in the written project-status.json // The first argument to the first call to writeFileSync const writtenData = JSON.parse(fs.writeFileSync.mock.calls[0][1]); expect(writtenData.projectInfo.name).toBe(expectedName); } }); it('should use process.cwd() if workingDirectoryInput is empty or null', () => { loadProjectState.mockImplementation(() => { throw new Error('ENOENT'); }); const cwd = process.cwd(); const resolvedProjectStatusPath = path.join(cwd, DEFAULT_PROJECT_STATUS_FILENAME); setupProjectState(MOCK_REPO_URL, ''); // Empty working directory expect(fs.writeFileSync).toHaveBeenCalledWith(resolvedProjectStatusPath, expect.any(String)); fs.writeFileSync.mockClear(); setupProjectState(MOCK_REPO_URL, null); // Null working directory expect(fs.writeFileSync).toHaveBeenCalledWith(resolvedProjectStatusPath, expect.any(String)); }); }); describe('analyzeRepository(workingDirectoryInput)', () => { it('should correctly categorize files and directories', () => { const workingDir = 'project_root'; const resolvedWorkingDir = path.resolve(workingDir); fs.readdirSync.mockReturnValue([ { name: 'index.js', isFile: () => true, isDirectory: () => false }, { name: 'style.css', isFile: () => true, isDirectory: () => false }, { name: 'package.json', isFile: () => true, isDirectory: () => false }, { name: 'README.md', isFile: () => true, isDirectory: () => false }, { name: 'src', isFile: () => false, isDirectory: () => true }, { name: 'image.png', isFile: () => true, isDirectory: () => false }, // Not explicitly categorized { name: 'project-status.json', isFile: () => true, isDirectory: () => false }, // Should be ignored in config count ]); // Mock statSync for the workingDir itself if analyzeRepository calls it (it doesn't currently) // fs.statSync.mockReturnValueOnce({ isDirectory: () => true }); const analysis = analyzeRepository(workingDir); expect(fs.readdirSync).toHaveBeenCalledWith(resolvedWorkingDir, { withFileTypes: true }); expect(analysis.fileCount).toBe(5); // index.js, style.css, package.json, README.md, image.png (project-status.json is filtered) expect(analysis.categories.code).toEqual(['index.js']); expect(analysis.categories.web).toEqual(['style.css']); expect(analysis.categories.config).toEqual(['package.json']); // project-status.json is excluded expect(analysis.categories.docs).toEqual(['README.md']); expect(analysis.categories.dirs).toEqual(['src']); expect(analysis.analyzedDirectory).toBe(resolvedWorkingDir); }); it('should handle an empty directory', () => { fs.readdirSync.mockReturnValue([]); // Empty directory const analysis = analyzeRepository('empty_dir'); expect(analysis.fileCount).toBe(0); expect(analysis.categories.code).toEqual([]); expect(analysis.categories.dirs).toEqual([]); }); it('should return default structure on fs.readdirSync error', () => { fs.readdirSync.mockImplementationOnce(() => { throw new Error('Read dir failed'); }); const analysis = analyzeRepository('error_dir'); expect(analysis.fileCount).toBe(0); expect(Object.values(analysis.categories).every(arr => arr.length === 0)).toBe(true); expect(analysis.error).toBeUndefined(); // As per current implementation, it logs error but returns default }); }); describe('initializeEnvironment(repositoryUrl, workingDirectoryInput)', () => { const mockProjectState = { projectInfo: { name: MOCK_REPO_NAME }, development: {}, context: {} }; const mockRepoAnalysis = { fileCount: 5, categories: {} }; const mockContinuityPrompt = "This is a mock prompt."; beforeEach(() => { // Mock the functions called by initializeEnvironment // setupProjectState is part of the module under test, so we can let its actual logic run // but ensure its dependencies (loadProjectState, fs calls) are mocked appropriately. loadProjectState.mockImplementation(() => { throw new Error('ENOENT for setup in initialize'); }); // Default for setupProjectState to create new generateContinuityPrompt.mockReturnValue(mockContinuityPrompt); // Mock analyzeRepository's dependencies fs.readdirSync.mockImplementation((dirPath) => { // console.log(`readdirSync mock for analyzeRepository, path: ${dirPath}`); // Debug if (path.resolve(dirPath) === path.resolve('') || path.resolve(dirPath) === path.resolve('test_dir')) { return [{ name: 'mockFile.js', isFile: () => true, isDirectory: () => false }]; } return []; }); }); it('should call setupProjectState, analyzeRepository, and generateContinuityPrompt', () => { const result = initializeEnvironment(MOCK_REPO_URL, 'test_dir'); // Check if setupProjectState was effectively run (e.g. by its fs.writeFileSync call) expect(fs.writeFileSync).toHaveBeenCalledWith(expect.stringContaining(DEFAULT_PROJECT_STATUS_FILENAME), expect.any(String)); // Check analyzeRepository was effectively run expect(fs.readdirSync).toHaveBeenCalledWith(path.resolve('test_dir'), { withFileTypes: true }); // Check generateContinuityPrompt expect(generateContinuityPrompt).toHaveBeenCalled(); expect(result.projectState).toBeDefined(); expect(result.projectState.projectInfo.name).toBe(MOCK_REPO_NAME); // From the new state created by setupProjectState expect(result.repoAnalysis).toBeDefined(); expect(result.repoAnalysis.fileCount).toBe(1); // From readdirSync mock for analyzeRepository expect(result.continuityPrompt).toBe(mockContinuityPrompt); expect(result.error).toBeUndefined(); }); it('should return an error object if setupProjectState fails internally (e.g. fs.writeFileSync fails)', () => { // Make setupProjectState fail by making its fs.writeFileSync fail fs.writeFileSync.mockImplementationOnce(() => { throw new Error('Failed to write project-status.json'); }); const result = initializeEnvironment(MOCK_REPO_URL, 'fail_dir'); expect(result.error).toBe(true); expect(result.message).toContain('Failed to write project-status.json'); expect(result.projectState).toBeNull(); }); it('should return an error object if analyzeRepository fails', () => { // Mock setupProjectState to succeed (by ensuring its fs.writeFileSync doesn't throw) // And then make analyzeRepository fail fs.writeFileSync.mockImplementation((p,c) => { /* console.log(`Mock write OK for ${p}`); */ }); // Allow setupProjectState's write to "succeed" fs.readdirSync.mockImplementationOnce(() => { throw new Error('analyzeRepository failed'); }); const result = initializeEnvironment(MOCK_REPO_URL, 'fail_analysis_dir'); expect(result.error).toBe(true); expect(result.message).toContain('analyzeRepository failed'); expect(result.repoAnalysis).toBeNull(); // projectState might exist if setupProjectState completed before analyzeRepository failed. // This depends on exact internal error handling of initializeEnvironment. // Current initializeEnvironment catches and returns specific error object. }); }); });