@alvinveroy/codecompass
Version:
AI-powered MCP server for codebase navigation and LLM prompt optimization
353 lines (352 loc) • 22.1 kB
JavaScript
;
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');
});
});
});