UNPKG

@alvinveroy/codecompass

Version:

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

283 lines (282 loc) 17.9 kB
"use strict"; 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)); }); }); });