UNPKG

@alvinveroy/codecompass

Version:

AI-powered MCP server for codebase navigation and LLM prompt optimization

353 lines (352 loc) 22.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); // 1. Mock 'child_process' and replace 'exec' with a vi.fn() created IN THE FACTORY. vitest_1.vi.mock('child_process', async (importOriginal) => { const _actualCp = await importOriginal(); return { ..._actualCp, exec: vitest_1.vi.fn(), // This vi.fn() is created when the factory runs. }; }); vitest_1.vi.mock('fs/promises', () => { const accessMock = vitest_1.vi.fn(); const readFileMock = vitest_1.vi.fn(); const readdirMock = vitest_1.vi.fn(); const statMock = vitest_1.vi.fn(); return { __esModule: true, default: { access: accessMock, readFile: readFileMock, readdir: readdirMock, stat: statMock }, access: accessMock, readFile: readFileMock, readdir: readdirMock, stat: statMock, }; }); // Mock 'isomorphic-git' // isomorphic-git exports named functions. We mock them directly. vitest_1.vi.mock('isomorphic-git', async (importOriginal) => { const _actual = await importOriginal(); return { resolveRef: vitest_1.vi.fn(), listFiles: vitest_1.vi.fn(), log: vitest_1.vi.fn(), readCommit: vitest_1.vi.fn(), walk: vitest_1.vi.fn(), readBlob: vitest_1.vi.fn(async ({ oid }) => { // Return some consistent mock data based on oid if needed for specific diffs, // otherwise, generic content is fine for `expect.any(String)`. return Promise.resolve({ blob: Buffer.from(`mock file content for ${oid || 'unknown'}`) }); }), TREE: vitest_1.vi.fn((args) => ({ // Simulate Walker object structure expected by SUT's git.TREE({ ref: treeOid }) // The mock TREE needs to return something that walk's `trees` parameter can use. // The important part for the mock is that `trees[0]._id` or similar can be accessed if the test relies on it. // If `args.ref` is passed (as in the new SUT code), use it for identification. _id: args?.ref || args?.oid || 'mock_tree_id_default', ...args })), }; }); vitest_1.vi.mock('util', async (importOriginal) => { const actualUtil = await importOriginal(); // This is the actual mock function that will be used for the promisified exec const internalMockedPromisifiedExec = vitest_1.vi.fn(); // Store it on a temporary global to retrieve it in the test file after imports. // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access globalThis.__test__mockedPromisifiedExec = internalMockedPromisifiedExec; return { __esModule: true, ...actualUtil, // eslint-disable-next-line @typescript-eslint/no-explicit-any promisify: (fnToPromisify) => { if (fnToPromisify && (typeof fnToPromisify.name === 'string' && fnToPromisify.name === 'exec' || fnToPromisify === child_process_1.exec)) { return internalMockedPromisifiedExec; } return actualUtil.promisify(fnToPromisify); }, }; }); // Mock other external dependencies vitest_1.vi.mock('../../lib/config-service', () => { const loggerInstance = { info: vitest_1.vi.fn(), warn: vitest_1.vi.fn(), error: vitest_1.vi.fn(), debug: vitest_1.vi.fn() }; return { __esModule: true, configService: { COLLECTION_NAME: 'test_collection', FILE_INDEXING_CHUNK_SIZE_CHARS: 100, FILE_INDEXING_CHUNK_OVERLAP_CHARS: 20, }, logger: loggerInstance, }; }); vitest_1.vi.mock('../../lib/ollama'); // Import exec AFTER mocking child_process. This 'exec' will be the vi.fn() from the factory. // We need a reference to this instance for the util.promisify mock. const child_process_1 = require("child_process"); // Import SUT and other necessary modules AFTER all vi.mock calls const repositoryFunctions = __importStar(require("../../lib/repository")); // Import all exports as a namespace const config_service_1 = require("../../lib/config-service"); // configService is mocked, only logger needed here // Import specific fs/promises methods directly // We will import the mocked versions of these functions const promises_1 = require("fs/promises"); // Retrieve the mock function via the globalThis workaround // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access const importedMockExecAsyncFn = globalThis.__test__mockedPromisifiedExec; // Import named mocks from isomorphic-git const git = __importStar(require("isomorphic-git")); // Import as namespace // import { QdrantClient } from '@qdrant/js-client-rest'; // _QdrantClient if used (0, vitest_1.describe)('Repository Utilities', () => { const repoPath = '/test/diff/repo'; // _execMock was unused and potentially causing a lint error, so it's removed. // Renamed for clarity, used in the inner beforeEach const setupGitLogWithTwoCommits = () => { const mockAuthor = { name: 'Test', email: 'test@example.com', timestamp: Date.now() / 1000, timezoneOffset: 0 }; vitest_1.vi.mocked(git.log).mockResolvedValue([ { oid: 'commit2_oid', commit: { message: 'Second', author: mockAuthor, committer: mockAuthor, parent: ['commit1_oid'], tree: 'tree2' } }, { oid: 'commit1_oid', commit: { message: 'First', author: mockAuthor, committer: mockAuthor, parent: [], tree: 'tree1' } } ]); }; // Renamed for clarity const setupGitLogWithSingleCommit = () => { const mockAuthor = { name: 'Test', email: 'test@example.com', timestamp: Date.now() / 1000, timezoneOffset: 0 }; vitest_1.vi.mocked(git.log).mockResolvedValue([ { oid: 'commit1_oid', commit: { message: 'First', author: mockAuthor, committer: mockAuthor, parent: [], tree: 'tree1' } } ]); }; (0, vitest_1.beforeEach)(() => { vitest_1.vi.clearAllMocks(); // execMock is vi.fn() from factory, clearAllMocks resets its state (calls, impls) // Reset the imported mock functions vitest_1.vi.mocked(promises_1.access).mockReset(); vitest_1.vi.mocked(promises_1.readFile).mockReset(); vitest_1.vi.mocked(promises_1.readdir).mockReset(); vitest_1.vi.mocked(promises_1.stat).mockReset(); // Reset all isomorphic-git mocks using named imports vitest_1.vi.mocked(git.resolveRef).mockReset(); vitest_1.vi.mocked(git.listFiles).mockReset(); vitest_1.vi.mocked(git.log).mockReset(); vitest_1.vi.mocked(git.readCommit).mockReset(); // vi.mocked(git.diffTrees)?.mockReset(); // diffTrees is no longer directly called by the SUT function being tested here vitest_1.vi.mocked(git.walk).mockReset(); if (git.TREE && typeof git.TREE.mockClear === 'function') { git.TREE.mockClear(); } config_service_1.logger.info.mockClear(); config_service_1.logger.warn.mockClear(); config_service_1.logger.error.mockClear(); config_service_1.logger.debug.mockClear(); }); (0, vitest_1.afterEach)(() => { vitest_1.vi.restoreAllMocks(); }); (0, vitest_1.describe)('validateGitRepository (direct tests)', () => { // These tests use the original implementation of validateGitRepository (0, vitest_1.it)('should return true for a valid repository', async () => { // Configure the specific mock function directly vitest_1.vi.mocked(promises_1.access).mockResolvedValue(undefined); vitest_1.vi.mocked(git.resolveRef).mockResolvedValue('refs/heads/main'); // Use mockedResolveRef const result = await repositoryFunctions.validateGitRepository(repoPath); (0, vitest_1.expect)(result).toBe(true); }); (0, vitest_1.it)('should return false if .git access is denied', async () => { // Configure the specific mock function directly vitest_1.vi.mocked(promises_1.access).mockRejectedValueOnce(new Error('Permission denied')); const result = await repositoryFunctions.validateGitRepository(repoPath); (0, vitest_1.expect)(result).toBe(false); }); (0, vitest_1.it)('should return false if HEAD cannot be resolved', async () => { // Configure the specific mock function directly vitest_1.vi.mocked(promises_1.access).mockResolvedValue(undefined); // fs.access passes vitest_1.vi.mocked(git.resolveRef).mockRejectedValueOnce(new Error('No HEAD')); // mockedResolveRef fails const result = await repositoryFunctions.validateGitRepository(repoPath); (0, vitest_1.expect)(result).toBe(false); }); }); (0, vitest_1.describe)('getRepositoryDiff', () => { let mockInjectedValidator; // Setup mocks specifically for tests that expect validateGitRepository to pass // This beforeEach establishes the common "happy path" for validateGitRepository and git.log (0, vitest_1.beforeEach)(() => { // This mock will be passed directly to getRepositoryDiff mockInjectedValidator = vitest_1.vi.fn(); vitest_1.vi.mocked(importedMockExecAsyncFn).mockReset(); // Reset our new async mock for execAsync mockInjectedValidator.mockResolvedValue(true); // Default to valid setupGitLogWithTwoCommits(); }); (0, vitest_1.it)('should call git diff command and return stdout', async () => { // Remove: execMock.mockImplementationOnce(...) as importedMockExecAsyncFn handles the async behavior now. vitest_1.vi.mocked(importedMockExecAsyncFn).mockResolvedValueOnce({ stdout: 'diff_content_stdout_explicit', stderr: '' }); const result = await repositoryFunctions.getRepositoryDiff(repoPath, mockInjectedValidator); (0, vitest_1.expect)(mockInjectedValidator).toHaveBeenCalledWith(repoPath); (0, vitest_1.expect)(importedMockExecAsyncFn).toHaveBeenCalledWith('git diff commit1_oid commit2_oid', { cwd: repoPath, maxBuffer: 1024 * 1024 * 5 }); (0, vitest_1.expect)(result).toBe('diff_content_stdout_explicit'); }); (0, vitest_1.it)('should truncate long diff output', async () => { const MAX_DIFF_LENGTH_FROM_SUT = 10000; const longDiff = 'a'.repeat(MAX_DIFF_LENGTH_FROM_SUT + 1); // Remove: execMock.mockImplementationOnce(...) vitest_1.vi.mocked(importedMockExecAsyncFn).mockResolvedValueOnce({ stdout: longDiff, stderr: '' }); const result = await repositoryFunctions.getRepositoryDiff(repoPath, mockInjectedValidator); (0, vitest_1.expect)(mockInjectedValidator).toHaveBeenCalledWith(repoPath); // Add assertion for importedMockExecAsyncFn call (0, vitest_1.expect)(importedMockExecAsyncFn).toHaveBeenCalledWith(vitest_1.expect.any(String), vitest_1.expect.objectContaining({ cwd: repoPath })); (0, vitest_1.expect)(result).toBe('a'.repeat(MAX_DIFF_LENGTH_FROM_SUT) + "\n... (diff truncated)"); (0, vitest_1.expect)(result).toContain('... (diff truncated)'); }); (0, vitest_1.it)('should handle errors from git diff command', async () => { const mockError = new Error('Git command failed'); const stderrText = 'stderr from exec callback'; mockError.code = 128; mockError.stderr = stderrText; // Remove: execMock.mockImplementationOnce(...) vitest_1.vi.mocked(importedMockExecAsyncFn).mockRejectedValueOnce(mockError); const result = await repositoryFunctions.getRepositoryDiff(repoPath, mockInjectedValidator); (0, vitest_1.expect)(mockInjectedValidator).toHaveBeenCalledWith(repoPath); // Add assertion for importedMockExecAsyncFn call (0, vitest_1.expect)(importedMockExecAsyncFn).toHaveBeenCalledWith(vitest_1.expect.any(String), vitest_1.expect.objectContaining({ cwd: repoPath })); (0, vitest_1.expect)(result).toBe(`Failed to retrieve diff for ${repoPath}: Git command failed`); (0, vitest_1.expect)(config_service_1.logger.error).toHaveBeenCalledWith(`Error retrieving git diff for ${repoPath}: Git command failed`, vitest_1.expect.objectContaining({ message: 'Git command failed', code: 128, stderr: stderrText, // Now this should match })); }); (0, vitest_1.it)('should return "No Git repository found" if validateGitRepository returns false', async () => { mockInjectedValidator.mockResolvedValue(false); // Control our specific mock const result = await repositoryFunctions.getRepositoryDiff(repoPath, mockInjectedValidator); (0, vitest_1.expect)(mockInjectedValidator).toHaveBeenCalledWith(repoPath); (0, vitest_1.expect)(result).toBe("No Git repository found"); (0, vitest_1.expect)(importedMockExecAsyncFn).not.toHaveBeenCalled(); // Check the async mock (0, vitest_1.expect)(vitest_1.vi.mocked(git.log)).not.toHaveBeenCalled(); // Use mockedGitLog }); (0, vitest_1.it)('should return "No previous commits to compare" if less than 2 commits (single commit)', async () => { // mockInjectedValidator is set to resolve true in beforeEach setupGitLogWithSingleCommit(); const result = await repositoryFunctions.getRepositoryDiff(repoPath, mockInjectedValidator); (0, vitest_1.expect)(mockInjectedValidator).toHaveBeenCalledWith(repoPath); (0, vitest_1.expect)(result).toBe("No previous commits to compare"); (0, vitest_1.expect)(importedMockExecAsyncFn).not.toHaveBeenCalled(); // Check the async mock }); }); (0, vitest_1.describe)('getCommitHistoryWithChanges', () => { (0, vitest_1.it)('should retrieve commit history with changed files', async () => { // No need to mock validateGitRepository here as it's not called by getCommitHistoryWithChanges const mockCommits = [ { oid: 'commit2', commit: { message: 'Feat: new feature', author: { name: 'Test Author', email: 'test@example.com', timestamp: 1672531200, timezoneOffset: 0 }, committer: { name: 'Test Committer', email: 'test@example.com', timestamp: 1672531200, timezoneOffset: 0 }, tree: 'tree2_oid', parent: ['commit1_oid'] } }, { oid: 'commit1', commit: { message: 'Initial commit', author: { name: 'Test Author', email: 'test@example.com', timestamp: 1672444800, timezoneOffset: 0 }, committer: { name: 'Test Committer', email: 'test@example.com', timestamp: 1672444800, timezoneOffset: 0 }, tree: 'tree1_oid', parent: [] } }, ]; vitest_1.vi.mocked(git.log).mockResolvedValue(mockCommits); vitest_1.vi.mocked(git.readCommit).mockImplementation(({ oid }) => { // Find the commit in our typed mockCommits array const foundCommit = mockCommits.find(c => c.oid === oid || (oid.endsWith('_oid') && c.commit.tree === oid)); // Handle tree oids if passed if (foundCommit) { return Promise.resolve({ oid: foundCommit.oid, commit: { ...foundCommit.commit, // Spread the well-typed commit data } }); } // Fallback for 'commit1_oid' if it's a parent OID not directly in mockCommits list by that OID if (oid === 'commit1_oid' && mockCommits[1]) { // Assuming commit1_oid refers to mockCommits[1]'s OID conceptually return Promise.resolve({ oid: mockCommits[1].oid, commit: { ...mockCommits[1].commit } }); } // Provide minimal valid structure for author/committer to avoid 'any' return Promise.resolve({ oid: 'unknown', commit: { tree: 'unknown_tree', parent: [], author: { name: 'Unknown' }, committer: { name: 'Unknown' }, message: 'Unknown' } }); }); // vi.mocked(git.diffTrees) no longer needed here as SUT uses git.walk for diffing. vitest_1.vi.mocked(git.walk).mockImplementation(async ({ fs: _nodeFsAlias, dir: _dir, gitdir: _gitdir, trees, map }) => { if (map && trees.length === 1 && trees[0]._id === 'mock_tree_id_default') { // Simulate initial commit walk: one file added // The SUT's initial commit logic uses `trees: [git.TREE()]`. Our `git.TREE` mock without args gives `_id: 'mock_tree_id_default'`. const mockEntry = { type: () => 'blob', oid: () => 'blob_oid_initial', mode: () => 0o100644 }; await map('initial.ts', [mockEntry]); } else if (map && trees.length === 2) { if (trees[0]._id === 'tree1_oid' && trees[1]._id === 'tree2_oid') { // Simulate one modified file const mockEntryBefore = { type: () => 'blob', oid: () => 'blob_before_oid', mode: () => 0o100644 }; const mockEntryAfter = { type: () => 'blob', oid: () => 'blob_after_oid', mode: () => 0o100644 }; await map('file.ts', [mockEntryBefore, mockEntryAfter]); // Simulate one added file const mockEntryAdded = { type: () => 'blob', oid: () => 'blob_added_oid', mode: () => 0o100644 }; await map('added_file.ts', [null, mockEntryAdded]); // Simulate one deleted file const mockEntryDeleted = { type: () => 'blob', oid: () => 'blob_deleted_oid', mode: () => 0o100644 }; await map('deleted_file.ts', [mockEntryDeleted, null]); } } return []; // Default return for walk }); const history = await repositoryFunctions.getCommitHistoryWithChanges(repoPath, { count: 2 }); (0, vitest_1.expect)(history).toHaveLength(2); // Check commit2 (non-initial commit, uses two-tree walk) (0, vitest_1.expect)(history[0].oid).toBe('commit2'); (0, vitest_1.expect)(history[0].changedFiles).toEqual(vitest_1.expect.arrayContaining([ vitest_1.expect.objectContaining({ path: 'file.ts', type: 'modify', oldOid: 'blob_before_oid', newOid: 'blob_after_oid' }), // diffText can be omitted if too complex or checked separately vitest_1.expect.objectContaining({ path: 'added_file.ts', type: 'add', oldOid: null, newOid: 'blob_added_oid' }), vitest_1.expect.objectContaining({ path: 'deleted_file.ts', type: 'delete', oldOid: 'blob_deleted_oid', newOid: null }) ])); // Optionally, check for diffText presence if crucial for this test history[0].changedFiles.forEach(file => { if (file.type !== 'typechange' && file.type !== 'equal') { // 'equal' type not used here (0, vitest_1.expect)(file.diffText).toEqual(vitest_1.expect.any(String)); } }); (0, vitest_1.expect)(history[0].changedFiles.length).toBe(3); // Check commit1 (initial commit, uses single-tree walk) (0, vitest_1.expect)(history[1].oid).toBe('commit1'); (0, vitest_1.expect)(history[1].changedFiles).toEqual(vitest_1.expect.arrayContaining([ vitest_1.expect.objectContaining({ path: 'initial.ts', type: 'add', oldOid: null, newOid: 'blob_oid_initial' }) ])); if (history[1].changedFiles[0]) { (0, vitest_1.expect)(history[1].changedFiles[0].diffText).toEqual(vitest_1.expect.any(String)); } (0, vitest_1.expect)(history[1].changedFiles.length).toBe(1); }); (0, vitest_1.it)('should handle errors from git.log', async () => { // Ensure validateGitRepository conditions are met if it's called by SUT, // though getCommitHistoryWithChanges doesn't call validateGitRepository directly. // However, it does use gitLog, which is what we're testing for failure here. vitest_1.vi.mocked(git.log).mockRejectedValue(new Error('Log failed')); // Use mockedGitLog await (0, vitest_1.expect)(repositoryFunctions.getCommitHistoryWithChanges(repoPath)).rejects.toThrow('Log failed'); }); }); });