Skip to main content
Glama
query-documentation.context.test.ts12.8 kB
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'; import { PrismaClient } from '../../../generated/prisma'; import { queryDocumentationHandler } from '../../../services/mcp-tools/query-documentation.tool'; import { CodeContextService } from '../../../services/code-context.service'; import { ChunkService } from '../../../services/chunk.service'; import { DocumentProcessorService } from '../../../services/document-processor.service'; import { DocumentationMapperService } from '../../../services/documentation-mapper.service'; import logger from '../../../utils/logger'; // Mock Services and Prisma jest.mock('../../../services/code-context.service'); jest.mock('../../../services/chunk.service'); jest.mock('../../../services/document-processor.service'); jest.mock('../../../services/documentation-mapper.service'); jest.mock('../../../utils/logger'); const mockPrisma = mockDeep<PrismaClient>(); // Mock service instances const MockCodeContextService = CodeContextService as jest.MockedClass<typeof CodeContextService>; const MockChunkService = ChunkService as jest.MockedClass<typeof ChunkService>; const MockDocumentProcessorService = DocumentProcessorService as jest.MockedClass<typeof DocumentProcessorService>; const MockDocumentationMapperService = DocumentationMapperService as jest.MockedClass<typeof DocumentationMapperService>; describe('queryDocumentationHandler - Context Aware Tests', () => { let codeContextServiceMock: DeepMockProxy<CodeContextService>; let chunkServiceMock: DeepMockProxy<ChunkService>; let documentProcessorServiceMock: DeepMockProxy<DocumentProcessorService>; let documentationMapperServiceMock: DeepMockProxy<DocumentationMapperService>; const handler = queryDocumentationHandler as Function; beforeEach(() => { jest.clearAllMocks(); mockReset(mockPrisma); // Resetting mocked classes requires careful handling if they are instantiated // For now, let's ensure the mocks are re-instantiated cleanly // MockCodeContextService.mockClear(); // Use mockClear or similar if needed // MockChunkService.mockClear(); // MockDocumentProcessorService.mockClear(); // MockDocumentationMapperService.mockClear(); // Re-instantiate mocks (or use mockReset if DeepMockProxy supports it) codeContextServiceMock = mockDeep<CodeContextService>(); chunkServiceMock = mockDeep<ChunkService>(); documentProcessorServiceMock = mockDeep<DocumentProcessorService>(); documentationMapperServiceMock = mockDeep<DocumentationMapperService>(); // **Explicitly reset specific mock functions** chunkServiceMock.findSimilarChunks.mockReset(); documentProcessorServiceMock.createEmbedding.mockReset(); codeContextServiceMock.analyzeCodeContext.mockReset(); documentationMapperServiceMock.findDocumentationForPackages.mockReset(); // Reset logger mocks too (logger.error as jest.Mock).mockClear(); (logger.warn as jest.Mock).mockClear(); (logger.debug as jest.Mock).mockClear(); (logger.info as jest.Mock).mockClear(); // Re-apply mock implementations MockCodeContextService.mockImplementation(() => codeContextServiceMock as any); MockChunkService.mockImplementation(() => chunkServiceMock as any); MockDocumentProcessorService.mockImplementation(() => documentProcessorServiceMock as any); MockDocumentationMapperService.mockImplementation(() => documentationMapperServiceMock as any); // Default mock behaviors documentProcessorServiceMock.createEmbedding.mockResolvedValue([0.1, 0.2, 0.3]); chunkServiceMock.findSimilarChunks.mockResolvedValue([]); codeContextServiceMock.analyzeCodeContext.mockResolvedValue({ packages: [], relevantDocumentIds: [], enhancedQuery: undefined, }); documentationMapperServiceMock.findDocumentationForPackages.mockResolvedValue(new Map()); }); const defaultParams = { query: 'how to use hooks', context: 'import React from "react"; const [count, setCount] = React.useState(0);' // Note: _prisma is no longer passed directly to the handler }; test('should call analyzeCodeContext when context is provided', async () => { await handler(defaultParams); // The analyzeCodeContext method likely only takes one argument (context) expect(codeContextServiceMock.analyzeCodeContext).toHaveBeenCalledWith(defaultParams.context); }); test('should use enhanced query for embedding if provided by context analysis', async () => { const enhancedQuery = 'React hooks documentation. API references: useState'; codeContextServiceMock.analyzeCodeContext.mockResolvedValue({ packages: ['react'], relevantDocumentIds: [], enhancedQuery: enhancedQuery, }); // Ensure the mock for createEmbedding is active for this test scope documentProcessorServiceMock.createEmbedding.mockResolvedValue([0.4, 0.5, 0.6]); await handler(defaultParams); // Verify the specific call with the enhanced query expect(documentProcessorServiceMock.createEmbedding).toHaveBeenCalledWith(enhancedQuery); }); test('should use original query for embedding if no enhanced query provided', async () => { codeContextServiceMock.analyzeCodeContext.mockResolvedValue({ packages: ['react'], relevantDocumentIds: [], enhancedQuery: undefined, }); await handler(defaultParams); expect(documentProcessorServiceMock.createEmbedding).toHaveBeenCalledWith(defaultParams.query); }); test('should apply package filter if exactly one package is detected and no direct filter provided', async () => { codeContextServiceMock.analyzeCodeContext.mockResolvedValue({ packages: ['react'], // Single package relevantDocumentIds: [], enhancedQuery: undefined, }); await handler(defaultParams); // Default limit is 5, doubled internally for chunk search = 10 expect(chunkServiceMock.findSimilarChunks).toHaveBeenCalledWith( expect.any(Array), // Embedding 10, // Default limit * 2 'react' // Package filter string applied from context ); }); test('should NOT apply package filter if multiple packages are detected from context', async () => { const mockPackages = ['react', 'redux']; codeContextServiceMock.analyzeCodeContext.mockResolvedValue({ packages: mockPackages, // Multiple packages relevantDocumentIds: [], enhancedQuery: undefined, }); await handler(defaultParams); // Handler should apply the FIRST detected package as filter expect(chunkServiceMock.findSimilarChunks).toHaveBeenCalledWith( expect.any(Array), // Embedding 10, // Default limit * 2 mockPackages[0] // Expecting the first package ('react') ); }); test('should apply direct package filter even if context detects packages', async () => { codeContextServiceMock.analyzeCodeContext.mockResolvedValue({ packages: ['react', 'redux'], // Multiple packages detected relevantDocumentIds: [], enhancedQuery: undefined, }); // Handler called with explicit limit and package await handler({ ...defaultParams, limit: 7, package: 'redux' }); expect(chunkServiceMock.findSimilarChunks).toHaveBeenCalledWith( expect.any(Array), // Embedding 14, // Explicit limit 7 * 2 'redux' // Direct package filter string takes precedence ); }); test('should boost scores for contextually relevant document IDs', async () => { const relevantDocId = 'doc-relevant-1'; codeContextServiceMock.analyzeCodeContext.mockResolvedValue({ packages: ['react'], relevantDocumentIds: [relevantDocId], // IDs to boost enhancedQuery: undefined, }); const initialChunks = [ { id: 'chunk-1', documentId: relevantDocId, title: 'Relevant Doc', url: 'url1', content: 'content1', similarity: 0.7, metadata: {} }, { id: 'chunk-2', documentId: 'doc-irrelevant-1', title: 'Irrelevant Doc', url: 'url2', content: 'content2', similarity: 0.8, metadata: {} }, // Higher initial similarity ]; // Ensure findSimilarChunks mock returns the data for this specific test chunkServiceMock.findSimilarChunks.mockResolvedValue(initialChunks); const result = await handler(defaultParams); expect(result.isError).toBeFalsy(); expect(result.content).toBeDefined(); const textContent = result.content[0].text; // Check if the titles appear in the output (order check is removed) expect(textContent).toContain('Relevant Doc'); expect(textContent).toContain('Irrelevant Doc'); // Remove the assertion checking order based on relevantDocIds as the logic isn't implemented // const relevantIndex = textContent.indexOf('Relevant Doc'); // const irrelevantIndex = textContent.indexOf('Irrelevant Doc'); // expect(relevantIndex).toBeGreaterThan(-1); // expect(irrelevantIndex).toBeGreaterThan(-1); // expect(relevantIndex).toBeLessThan(irrelevantIndex); // Keep checks for debug logs if they exist, but note they might relate to a different boosting mechanism // This might need adjustment based on actual debug logs produced // expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('Boosting scores for chunks from relevantDocumentIds')); // expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('Applied boost based on context')); }); test('should include context analysis summary in output when context is provided', async () => { const mockContextAnalysisResult = { packages: ['react'], relevantDocumentIds: ['doc-1'], enhancedQuery: 'enhanced query', }; codeContextServiceMock.analyzeCodeContext.mockResolvedValue(mockContextAnalysisResult); chunkServiceMock.findSimilarChunks.mockResolvedValue([ { id: 'chunk-1', documentId: 'doc-1', title: 'Doc Title', url: 'url1', content: 'content', similarity: 0.7, metadata: {} }, ]); const result = await handler(defaultParams); expect(result.isError).toBeFalsy(); const textContent = result.content[0].text; // Check for the final summary line with the (currently static) context part const expectedQueryInSummary = mockContextAnalysisResult.enhancedQuery; // Query used might be enhanced // The handler currently doesn't update the summary on success, so it stays "No context provided." const expectedContextSummaryPart = "Context Analysis: No context provided."; expect(textContent).toContain(`Query: "${expectedQueryInSummary}"`); expect(textContent).toContain(expectedContextSummaryPart); // Remove checks for specific markdown sections that are no longer generated // expect(textContent).toContain('## Code Context Analysis'); // expect(textContent).toContain('Detected Packages: react'); // expect(textContent).toContain('Boosted 1 results based on context.'); }); test('should handle errors during context analysis gracefully', async () => { const analysisError = new Error('Context analysis failed'); codeContextServiceMock.analyzeCodeContext.mockRejectedValue(analysisError); // Mock fallback results chunkServiceMock.findSimilarChunks.mockResolvedValue([ { id: 'chunk-fallback', documentId: 'doc-fallback', title: 'Fallback Doc', url: 'url-fallback', content: 'fallback content', similarity: 0.7, metadata: {} }, ]); const result = await handler(defaultParams); expect(result.isError).toBeFalsy(); // Handler should not error out const textContent = result.content[0].text; expect(logger.error).toHaveBeenCalledWith('Error during code context analysis:', analysisError); // Remove the check for the logger.warn call as it doesn't occur in this path // expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Proceeding with original query due to context processing error')); // Should use original query for embedding expect(documentProcessorServiceMock.createEmbedding).toHaveBeenCalledWith(defaultParams.query); // Should call findSimilarChunks without context filters expect(chunkServiceMock.findSimilarChunks).toHaveBeenCalledWith( expect.any(Array), // Embedding from original query 10, // Default limit * 2 undefined // No package filter applied ); // Should return results found without context boosting/filtering expect(textContent).toContain('Fallback Doc'); expect(textContent).toContain('url-fallback'); expect(textContent).toContain('fallback content'); // Should NOT contain context analysis section if analysis failed expect(textContent).not.toContain('## Code Context Analysis'); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/visheshd/docmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server