Skip to main content
Glama
query-tool.test.ts16.2 kB
import { queryHandler } from '../../../mcp/services/handlers/unified/query-handler'; import { ToolHandlerContext } from '../../../mcp/types/sdk-custom'; import { ContextService } from '../../../services/domain/context.service'; import { GraphQueryService } from '../../../services/domain/graph-query.service'; import { MemoryService } from '../../../services/memory.service'; // Define discriminated union types for query results type QueryResult = | { type: 'context'; contexts: any[] } | { type: 'entities'; label: string; entities: any[]; limit: number; offset: number; totalCount: number; } | { type: 'relationships'; startItemId: string; relatedItems: any[]; relationshipFilter?: string; depth?: number; } | { type: 'dependencies'; componentId: string; direction: 'dependencies' | 'dependents'; components: any[]; } | { type: 'governance'; componentId: string; decisions: any[]; rules: any[] } | { type: 'history'; itemId: string; itemType: string; contextHistory: any[] } | { type: 'tags'; tagId: string; items: any[] }; // Test file for query tool - consolidates 7 query types describe('Query Tool Tests', () => { let mockMemoryService: jest.Mocked<MemoryService>; let mockContextService: jest.Mocked<ContextService>; let mockGraphQueryService: jest.Mocked<GraphQueryService>; let mockContext: jest.Mocked<ToolHandlerContext>; beforeEach(() => { mockContextService = { getLatestContexts: jest.fn(), } as any; mockGraphQueryService = { listNodesByLabel: jest.fn(), getRelatedItems: jest.fn(), getComponentDependencies: jest.fn(), getComponentDependents: jest.fn(), getGoverningItemsForComponent: jest.fn(), getItemContextualHistory: jest.fn(), findItemsByTag: jest.fn(), } as any; mockMemoryService = { context: mockContextService, graphQuery: mockGraphQueryService, services: { context: mockContextService, graphQuery: mockGraphQueryService, }, } as any; // Mock context with session mockContext = { logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), }, session: { clientProjectRoot: '/test/project', }, sendProgress: jest.fn(), request: {} as any, meta: {} as any, signal: {} as any, } as any; }); describe('Context Query', () => { it('should retrieve contexts with default parameters', async () => { const mockContexts = [ { id: 'ctx-1', name: 'ctx-1', iso_date: '2024-12-09', agent: 'test-agent', summary: 'Test summary', observation: null, repository: 'test-repo', branch: 'main', created_at: new Date('2024-12-09T10:00:00Z'), updated_at: new Date('2024-12-09T10:00:00Z'), }, ]; mockContextService.getLatestContexts.mockResolvedValue(mockContexts); const result = (await queryHandler( { type: 'context', repository: 'test-repo', branch: 'main', }, mockContext, mockMemoryService, )) as QueryResult; if (result.type === 'context') { expect(result.type).toBe('context'); expect(result.contexts).toHaveLength(1); expect(result.contexts[0].id).toBe('ctx-1'); } else { fail('Expected result type to be context'); } expect(mockContextService.getLatestContexts).toHaveBeenCalledWith( mockContext, '/test/project', 'test-repo', 'main', undefined, ); }); it('should retrieve contexts with limit', async () => { mockContextService.getLatestContexts.mockResolvedValue([]); await queryHandler( { type: 'context', repository: 'test-repo', limit: 5, }, mockContext, mockMemoryService, ); expect(mockContextService.getLatestContexts).toHaveBeenCalledWith( mockContext, '/test/project', 'test-repo', 'main', 5, ); }); }); describe('Entities Query', () => { it('should list entities by label', async () => { const mockResult = { type: 'entities' as const, label: 'Component', entities: [ { id: 'comp-1', name: 'Component 1' }, { id: 'comp-2', name: 'Component 2' }, ], limit: 100, offset: 0, totalCount: 2, }; mockGraphQueryService.listNodesByLabel.mockResolvedValue(mockResult); const result = (await queryHandler( { type: 'entities', repository: 'test-repo', label: 'Component', }, mockContext, mockMemoryService, )) as QueryResult; if (result.type === 'entities') { expect(result.type).toBe('entities'); expect(result.label).toBe('Component'); expect(result.entities).toHaveLength(2); expect(result.totalCount).toBe(2); } else { fail('Expected result type to be entities'); } }); it('should throw error if label is missing', async () => { await expect( queryHandler( { type: 'entities', repository: 'test-repo', }, mockContext, mockMemoryService, ), ).rejects.toThrow('Required fields missing for the specified query type'); }); }); describe('Relationships Query', () => { it('should find related items', async () => { const mockResult = { startItemId: 'comp-1', relatedItems: [ { id: 'comp-2', type: 'Component' }, { id: 'rule-1', type: 'Rule' }, ], }; mockGraphQueryService.getRelatedItems.mockResolvedValue(mockResult as any); const result = (await queryHandler( { type: 'relationships', repository: 'test-repo', startItemId: 'comp-1', depth: 2, relationshipFilter: 'DEPENDS_ON', }, mockContext, mockMemoryService, )) as QueryResult; if (result.type === 'relationships') { expect(result.type).toBe('relationships'); expect(result.startItemId).toBe('comp-1'); expect(result.relatedItems).toHaveLength(2); expect(result.depth).toBe(2); } else { fail('Expected result type to be relationships'); } expect(mockGraphQueryService.getRelatedItems).toHaveBeenCalledWith( mockContext, '/test/project', 'test-repo', 'main', 'comp-1', { depth: 2, relationshipFilter: 'DEPENDS_ON', targetNodeTypeFilter: undefined, }, ); }); it('should throw error if startItemId is missing', async () => { await expect( queryHandler( { type: 'relationships', repository: 'test-repo', }, mockContext, mockMemoryService, ), ).rejects.toThrow('Required fields missing for the specified query type'); }); }); describe('Dependencies Query', () => { it('should get component dependencies', async () => { const mockResult = { componentId: 'comp-1', dependencies: [{ id: 'comp-2', name: 'Component 2', type: 'Component' }], }; mockGraphQueryService.getComponentDependencies.mockResolvedValue(mockResult as any); const result = (await queryHandler( { type: 'dependencies', repository: 'test-repo', componentId: 'comp-1', direction: 'dependencies', }, mockContext, mockMemoryService, )) as QueryResult; if (result.type === 'dependencies') { expect(result.type).toBe('dependencies'); expect(result.componentId).toBe('comp-1'); expect(result.direction).toBe('dependencies'); expect(result.components).toHaveLength(1); } else { fail('Expected result type to be dependencies'); } expect(mockGraphQueryService.getComponentDependencies).toHaveBeenCalled(); }); it('should get component dependents', async () => { const mockResult = { componentId: 'comp-1', dependents: [{ id: 'comp-3', name: 'Component 3', type: 'Component' }], }; mockGraphQueryService.getComponentDependents.mockResolvedValue(mockResult as any); const result = (await queryHandler( { type: 'dependencies', repository: 'test-repo', componentId: 'comp-1', direction: 'dependents', }, mockContext, mockMemoryService, )) as QueryResult; if (result.type === 'dependencies') { expect(result.type).toBe('dependencies'); expect(result.direction).toBe('dependents'); expect(result.components).toHaveLength(1); } else { fail('Expected result type to be dependencies'); } expect(mockGraphQueryService.getComponentDependents).toHaveBeenCalled(); }); it('should throw error if componentId or direction is missing', async () => { await expect( queryHandler( { type: 'dependencies', repository: 'test-repo', componentId: 'comp-1', }, mockContext, mockMemoryService, ), ).rejects.toThrow(); // Zod validation error for missing direction }); }); describe('Governance Query', () => { it('should get governing items for component', async () => { const mockResult = { componentId: 'comp-1', decisions: [{ id: 'dec-1', name: 'Decision 1' }], rules: [{ id: 'rule-1', name: 'Rule 1' }], }; mockGraphQueryService.getGoverningItemsForComponent.mockResolvedValue(mockResult as any); const result = (await queryHandler( { type: 'governance', repository: 'test-repo', componentId: 'comp-1', }, mockContext, mockMemoryService, )) as QueryResult; if (result.type === 'governance') { expect(result.type).toBe('governance'); expect(result.componentId).toBe('comp-1'); expect(result.decisions).toHaveLength(1); expect(result.rules).toHaveLength(1); } else { fail('Expected result type to be governance'); } }); it('should throw error if componentId is missing', async () => { await expect( queryHandler( { type: 'governance', repository: 'test-repo', }, mockContext, mockMemoryService, ), ).rejects.toThrow('Required fields missing for the specified query type'); }); }); describe('History Query', () => { it('should get item contextual history', async () => { const mockResult = { itemId: 'comp-1', itemType: 'Component', contextHistory: [ { id: 'ctx-1', summary: 'Created component' }, { id: 'ctx-2', summary: 'Updated component' }, ], }; mockGraphQueryService.getItemContextualHistory.mockResolvedValue(mockResult as any); const result = (await queryHandler( { type: 'history', repository: 'test-repo', itemId: 'comp-1', itemType: 'Component', }, mockContext, mockMemoryService, )) as QueryResult; if (result.type === 'history') { expect(result.type).toBe('history'); expect(result.itemId).toBe('comp-1'); expect(result.itemType).toBe('Component'); expect(result.contextHistory).toHaveLength(2); } else { fail('Expected result type to be history'); } }); it('should throw error if itemId or itemType is missing', async () => { await expect( queryHandler( { type: 'history', repository: 'test-repo', itemId: 'comp-1', }, mockContext, mockMemoryService, ), ).rejects.toThrow('Required fields missing for the specified query type'); }); }); describe('Tags Query', () => { it('should find items by tag', async () => { const mockResult = { type: 'tags' as const, tagId: 'tag-security', items: [ { id: 'comp-1', type: 'Component' }, { id: 'rule-1', type: 'Rule' }, ], }; mockGraphQueryService.findItemsByTag.mockResolvedValue(mockResult); const result = (await queryHandler( { type: 'tags', repository: 'test-repo', tagId: 'tag-security', }, mockContext, mockMemoryService, )) as QueryResult; if (result.type === 'tags') { expect(result.type).toBe('tags'); expect(result.tagId).toBe('tag-security'); expect(result.items).toHaveLength(2); expect(result.items[0].type).toBe('Component'); } else { fail('Expected result type to be tags'); } }); it('should find items by tag with entity type filter', async () => { const mockResult = { type: 'tags' as const, tagId: 'tag-security', items: [{ id: 'comp-1', type: 'Component' }], }; mockGraphQueryService.findItemsByTag.mockResolvedValue(mockResult); await queryHandler( { type: 'tags', repository: 'test-repo', tagId: 'tag-security', entityType: 'Component', }, mockContext, mockMemoryService, ); expect(mockGraphQueryService.findItemsByTag).toHaveBeenCalledWith( mockContext, '/test/project', 'test-repo', 'main', 'tag-security', 'Component', ); }); it('should throw error if tagId is missing', async () => { await expect( queryHandler( { type: 'tags', repository: 'test-repo', }, mockContext, mockMemoryService, ), ).rejects.toThrow('Required fields missing for the specified query type'); }); }); describe('Session Validation', () => { it('should throw error if no active session', async () => { const contextNoSession = { logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), }, session: {}, sendProgress: jest.fn(), request: {} as any, meta: {} as any, signal: {} as any, } as any; await expect( queryHandler( { type: 'context', repository: 'test-repo', }, contextNoSession, mockMemoryService, ), ).rejects.toThrow('No active session'); }); }); describe('Error Handling', () => { it('should handle and rethrow service errors', async () => { const error = new Error('Service error'); mockContextService.getLatestContexts.mockRejectedValue(error); await expect( queryHandler( { type: 'context', repository: 'test-repo', }, mockContext, mockMemoryService, ), ).rejects.toThrow('Service error'); expect(mockContext.sendProgress).toHaveBeenCalledWith({ status: 'error', message: 'Failed to execute context query: Service error', percent: 100, isFinal: true, }); }); it('should throw error for unknown query type', async () => { await expect( queryHandler( { type: 'unknown' as any, repository: 'test-repo', }, mockContext, mockMemoryService, ), ).rejects.toThrow( "Invalid enum value. Expected 'context' | 'entities' | 'relationships' | 'dependencies' | 'governance' | 'history' | 'tags', received 'unknown'", ); }); }); });

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/Jakedismo/KuzuMem-MCP'

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