@alvinveroy/codecompass
Version:
AI-powered MCP server for codebase navigation and LLM prompt optimization
283 lines (282 loc) • 17.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const vitest_1 = require("vitest");
const fs_1 = require("fs"); // Import Dirent directly from 'fs'
const zod_1 = require("zod"); // For the parameters_schema
// Near the top of the file, after imports but before the first describe block:
const createMockDirent = (name, isDir) => {
const dirent = new fs_1.Dirent();
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
const mockDirent = dirent;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.name = name;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isFile = () => !isDir;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isDirectory = () => isDir;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isBlockDevice = () => false;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isCharacterDevice = () => false;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isSymbolicLink = () => false;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isFIFO = () => false;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isSocket = () => false;
return dirent;
};
// 2. Mock external dependencies of agent.ts FIRST
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: {
MAX_DIFF_LENGTH_FOR_CONTEXT_TOOL: 3000,
MAX_SNIPPET_LENGTH_FOR_CONTEXT_NO_SUMMARY: 1500,
MAX_FILES_FOR_SUGGESTION_CONTEXT_NO_SUMMARY: 15,
AGENT_DEFAULT_MAX_STEPS: 2,
AGENT_ABSOLUTE_MAX_STEPS: 3,
REQUEST_ADDITIONAL_CONTEXT_MAX_SEARCH_RESULTS: 10,
COLLECTION_NAME: 'test-collection',
SUGGESTION_PROVIDER: 'ollama',
SUGGESTION_MODEL: 'test-model',
OLLAMA_HOST: 'http://localhost:11434',
AGENT_QUERY_TIMEOUT: 60000,
MAX_REFINEMENT_ITERATIONS: 3,
QDRANT_SEARCH_LIMIT_DEFAULT: 5,
},
logger: loggerInstance,
};
});
// Mock external dependencies (these are fine)
vitest_1.vi.mock('../lib/llm-provider');
vitest_1.vi.mock('../lib/state');
vitest_1.vi.mock('../lib/query-refinement');
vitest_1.vi.mock('../lib/repository');
vitest_1.vi.mock('isomorphic-git');
vitest_1.vi.mock('fs/promises', () => {
const readFileMock = vitest_1.vi.fn();
const readdirMock = vitest_1.vi.fn();
const accessMock = vitest_1.vi.fn();
const statMock = vitest_1.vi.fn();
// Define a mock Dirent structure that fsPromises.readdir would resolve with
const _mockDirent_fs_promises = (name, isDir, _basePath = '/test/repo/some/path') => {
const dirent = new fs_1.Dirent();
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
const mockDirent = dirent;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.name = name;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isFile = () => !isDir;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isDirectory = () => isDir;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isBlockDevice = () => false;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isCharacterDevice = () => false;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isSymbolicLink = () => false;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isFIFO = () => false;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mockDirent.isSocket = () => false;
// Standard fs.Dirent does not have 'path' or 'parentPath' properties.
return dirent;
};
const mockFsPromises = {
readFile: readFileMock,
readdir: readdirMock,
access: accessMock,
stat: statMock,
// If Dirent is used as a type like fsPromises.Dirent, it's not part of the value-level export
// Dirent is a type from the module, not a property of the fsPromises object.
};
return {
__esModule: true, // Important for CJS/ESM interop with mocks
...mockFsPromises, // Spread for named exports if any were used like `import { readFile } from 'fs/promises'`
default: mockFsPromises, // For `import fsPromises from 'fs/promises'`
// To make fsPromises.Dirent available as a type, it's usually handled by @types/node
// The mock itself doesn't need to provide the Dirent type, just objects conforming to it.
};
});
// SUT functions will be called via ActualAgentModule
// Import mocked dependencies
const llm_provider_1 = require("../lib/llm-provider");
const state_1 = require("../lib/state"); // Corrected: removed _getRelevantResults
const query_refinement_1 = require("../lib/query-refinement");
// import { logger, configService } from '../lib/config-service'; // Corrected: import logger, configService
const repository_1 = require("../lib/repository"); // Corrected: removed _getCommitHistoryWithChanges
const isomorphic_git_1 = __importDefault(require("isomorphic-git"));
const promises_1 = require("fs/promises"); // Corrected: import stat, access
// For testing the *original* parseToolCalls and executeToolCall:
let ActualAgentModule;
(0, vitest_1.beforeAll)(async () => {
ActualAgentModule = await vitest_1.vi.importActual('../lib/agent');
// runAgentLoopSUT will be assigned from ActualAgentModule in the relevant describe block
});
const mockLLMProviderInstance = {
generateText: vitest_1.vi.fn(),
checkConnection: vitest_1.vi.fn().mockResolvedValue(true),
};
const mockQdrantClientInstance = {
search: vitest_1.vi.fn(),
scroll: vitest_1.vi.fn(),
};
(0, vitest_1.describe)('Agent', () => {
(0, vitest_1.beforeEach)(async () => {
vitest_1.vi.clearAllMocks(); // Clear all mocks from previous tests
llm_provider_1.getLLMProvider.mockResolvedValue(mockLLMProviderInstance);
mockLLMProviderInstance.generateText.mockReset().mockResolvedValue("Default LLM response");
mockLLMProviderInstance.checkConnection.mockReset().mockResolvedValue(true);
// Clear logger mocks (assuming logger is imported from config-service which is mocked)
const { logger: agentLogger } = await vitest_1.vi.importActual('../lib/config-service');
if (agentLogger && typeof agentLogger.info.mockClear === 'function') { // Check if logger and its methods are mocks
Object.values(agentLogger).forEach(mockFn => mockFn.mockClear?.());
}
mockLLMProviderInstance.generateText.mockReset().mockResolvedValue("Default LLM response");
mockLLMProviderInstance.checkConnection.mockReset().mockResolvedValue(true);
// Clear logger mocks (assuming logger is imported from config-service which is mocked)
// const { logger: agentLogger } = await vi.importActual<typeof import('../lib/config-service')>('../lib/config-service'); // Duplicate removed
// if (agentLogger && typeof (agentLogger.info as Mock).mockClear === 'function') {
// (Object.values(agentLogger) as Mock[]).forEach(mockFn => mockFn.mockClear?.());
// }
vitest_1.vi.mocked(repository_1.validateGitRepository).mockReset().mockResolvedValue(true);
repository_1.getRepositoryDiff.mockReset().mockResolvedValue('Default diff content');
query_refinement_1.searchWithRefinement.mockReset().mockResolvedValue({ results: [], refinedQuery: 'refined query', relevanceScore: 0 });
vitest_1.vi.mocked(isomorphic_git_1.default.listFiles).mockReset().mockResolvedValue(['file1.ts', 'file2.js']); // Use vi.mocked for default exports
state_1.getOrCreateSession.mockReset().mockImplementation((sessionIdParam, _repoPath) => {
return { id: sessionIdParam || 'default-test-session', queries: [], suggestions: [], context: {} };
});
state_1.addQuery.mockReset();
state_1.addSuggestion.mockReset();
state_1.updateContext.mockReset();
state_1.getRecentQueries.mockReset().mockReturnValue([]);
vitest_1.vi.mocked(promises_1.readFile).mockReset().mockResolvedValue('Default file content from generic mock');
// Mock readdir to resolve with an array of these mock Dirent objects.
// The cast to `Dirent[]` should be sufficient if createMockDirent returns valid Dirent-like objects.
// Use 'as any' to resolve the stubborn TS2345 error for the mock.
// This is acceptable in tests where the precise generic of Dirent isn't crucial.
// Using the top-level createMockDirent
vitest_1.vi.mocked(promises_1.readdir).mockReset().mockResolvedValue([createMockDirent('entry1', false)]);
});
(0, vitest_1.afterEach)(() => { vitest_1.vi.restoreAllMocks(); });
(0, vitest_1.describe)('parseToolCalls (original)', () => {
(0, vitest_1.it)('should parse valid tool calls', () => {
const output = `TOOL_CALL: {"tool":"search_code","parameters":{"query":"authentication"}}`;
// Test the original function using ActualAgentModule
const result = ActualAgentModule.parseToolCalls(output);
(0, vitest_1.expect)(result).toHaveLength(1);
(0, vitest_1.expect)(result[0]).toEqual({ tool: 'search_code', parameters: { query: 'authentication' } });
});
});
(0, vitest_1.describe)('executeToolCall (original)', () => {
const repoPath = '/test/repo';
(0, vitest_1.it)('should throw if tool requires model and model is unavailable', async () => {
// Test the original function using ActualAgentModule
try {
await ActualAgentModule.executeToolCall({ tool: 'agent_query', parameters: { user_query: 'test' } }, // agent_query requires a model
mockQdrantClientInstance, repoPath, false // suggestionModelAvailable = false
);
// If it doesn't throw, fail the test
(0, vitest_1.expect)(true).toBe(false); // Should not reach here
}
catch (e) {
const errorResult = e instanceof Error ? e : new Error(String(e));
(0, vitest_1.expect)(errorResult.message).toContain('Tool agent_query requires the suggestion model which is not available');
}
});
});
(0, vitest_1.describe)('runAgentLoop', () => {
const mockQdrantClient = mockQdrantClientInstance;
const repoPath = '/test/repo';
// We will not spy on parseToolCalls or executeToolCall directly for runAgentLoop tests.
// We will control their behavior by mocking their dependencies or the LLM responses.
let runAgentLoopSUT_local; // SUT for this block
(0, vitest_1.beforeEach)(() => {
// Assign SUT for this block
runAgentLoopSUT_local = ActualAgentModule.runAgentLoop;
// General setup for LLM provider mock for this describe block. Tests can override.
mockLLMProviderInstance.generateText.mockReset().mockResolvedValue("LLM Verification OK");
// Ensure dependencies of executeToolCall are reset/mocked as needed for each test
const mockSearchResult = {
id: 'search-res-1',
score: 0.8,
payload: {
dataType: 'file_chunk',
filepath: 'file.ts',
file_content_chunk: 'mock snippet',
chunk_index: 0,
total_chunks: 1,
last_modified: '2023-01-01'
} // Explicit cast for clarity in test
// No vector or version needed as per DetailedQdrantSearchResult
};
vitest_1.vi.mocked(query_refinement_1.searchWithRefinement).mockClear().mockResolvedValue({ results: [mockSearchResult], refinedQuery: 'refined', relevanceScore: 0.8 });
});
(0, vitest_1.afterEach)(() => {
// vi.restoreAllMocks() in the outer afterEach will handle restoring these spies.
// vi.resetAllMocks(); // If outer afterEach doesn't cover vi.fn() instances, keep this.
// But vi.restoreAllMocks() should cover spies.
});
(0, vitest_1.it)('should execute a tool call and then provide final response', async () => {
// Correct sequence for mockLLMProviderInstance.generateText:
// 1. Verification call in runAgentLoop
// 2. Agent reasoning call (should return TOOL_CALL string)
// 3. Final response call (if loop ends or max steps reached)
// Mock setup for mockLLMProviderInstance.generateText
mockLLMProviderInstance.generateText
.mockReset()
.mockResolvedValueOnce("LLM Verification OK") // For currentProvider.generateText("Test message") in runAgentLoop
.mockResolvedValueOnce('TOOL_CALL: {"tool": "agent_query", "parameters": {"user_query": "query with tool", "session_id": "session2"}}') // For agentPrompt in runAgentLoop
.mockResolvedValueOnce('Final response from orchestrator.'); // For the first LLM call within runAgentQueryOrchestrator (assuming it gives a direct final answer for this test)
// The actual parseToolCalls will be used.
// The actual executeToolCall will be used. We need to ensure its dependencies are mocked.
// searchWithRefinement is already mocked in beforeEach.
// getRepositoryDiff is mocked in global beforeEach.
// validateGitRepository is mocked in global beforeEach.
await runAgentLoopSUT_local('query with tool', 'session2', mockQdrantClient, repoPath, true);
// Assertion for generateText call count
(0, vitest_1.expect)(mockLLMProviderInstance.generateText).toHaveBeenCalledTimes(3); // Verification, Outer Loop, Orchestrator's 1st call
// Assertions for what each call was made with
(0, vitest_1.expect)(mockLLMProviderInstance.generateText).toHaveBeenNthCalledWith(1, "Test message");
(0, vitest_1.expect)(mockLLMProviderInstance.generateText).toHaveBeenNthCalledWith(2, vitest_1.expect.stringContaining('User query: query with tool')); // Outer loop prompt
(0, vitest_1.expect)(mockLLMProviderInstance.generateText).toHaveBeenNthCalledWith(3, vitest_1.expect.stringContaining('Original User Query: query with tool')); // Orchestrator's prompt
// Verify that searchWithRefinement is NOT directly called by runAgentLoop or its immediate executeToolCall for agent_query.
// searchWithRefinement would be called by a capability *inside* the orchestrator.
// For this specific test, if we are mocking the orchestrator to give a direct final answer,
// then searchWithRefinement (or other capabilities) wouldn't be called.
// If the test intends to check a capability call, the orchestrator's LLM mock needs to output a capability call.
// For now, assuming the orchestrator's first LLM call yields the final answer:
(0, vitest_1.expect)(query_refinement_1.searchWithRefinement).not.toHaveBeenCalled();
const addSuggestionSpy = state_1.addSuggestion;
(0, vitest_1.expect)(addSuggestionSpy).toHaveBeenCalledWith('session2', 'query with tool', vitest_1.expect.stringContaining('Final response from orchestrator.'));
});
});
(0, vitest_1.describe)('createAgentState (original)', () => {
(0, vitest_1.it)('should create a new agent state with the correct structure', () => {
const result = ActualAgentModule.createAgentState('test_session', 'Find auth code');
(0, vitest_1.expect)(result).toEqual({ sessionId: 'test_session', query: 'Find auth code', steps: [], context: [], isComplete: false });
});
});
(0, vitest_1.describe)('generateAgentSystemPrompt (original)', () => {
(0, vitest_1.it)('should include descriptions of all available tools', () => {
const mockCapabilityDefinitionsForPrompt = [
{
name: 'capability_searchCodeSnippets', // Example
description: 'Searches for code.',
parameters_schema: zod_1.z.object({ query: zod_1.z.string().describe("The search query.") })
}
// Add more mock capabilities if the test needs to verify their presence in the prompt
];
const prompt = ActualAgentModule.generateAgentSystemPrompt(mockCapabilityDefinitionsForPrompt);
// Update assertions to match mockCapabilityDefinitionsForPrompt
(0, vitest_1.expect)(prompt).toContain("capability_searchCodeSnippets");
(0, vitest_1.expect)(prompt).toContain("Searches for code.");
(0, vitest_1.expect)(prompt).toContain(JSON.stringify(mockCapabilityDefinitionsForPrompt[0].parameters_schema._def || { description: mockCapabilityDefinitionsForPrompt[0].parameters_schema.description }, null, 2));
});
});
});