Skip to main content
Glama
TaskSearchStrategy.test.tsβ€’9.85 kB
/** * Unit tests for TaskSearchStrategy * Issue #598: Add strategy-specific unit tests */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { TaskSearchStrategy } from '../../../src/services/search-strategies/TaskSearchStrategy.js'; import { SearchType, MatchType, SortType, UniversalResourceType, } from '../../../src/handlers/tool-configs/universal/types.js'; import { AttioRecord, AttioTask } from '../../../src/types/attio.js'; import { StrategyDependencies } from '../../../src/services/search-strategies/interfaces.js'; import { CachingService } from '../../../src/services/CachingService.js'; import { UniversalUtilityService } from '../../../src/services/UniversalUtilityService.js'; import { SearchUtilities } from '../../../src/services/search-utilities/SearchUtilities.js'; import { enhancedPerformanceTracker } from '../../../src/middleware/performance-enhanced.js'; // Mock dependencies vi.mock('../../../src/middleware/performance-enhanced.js', () => ({ enhancedPerformanceTracker: { markTiming: vi.fn(), }, })); vi.mock('../../../src/services/CachingService.js', () => ({ CachingService: { getOrLoadTasks: vi.fn(), }, })); vi.mock('../../../src/services/UniversalUtilityService.js', () => ({ UniversalUtilityService: { convertTaskToRecord: vi.fn(), }, })); vi.mock('../../../src/services/search-utilities/SearchUtilities.js', () => ({ SearchUtilities: { getTaskFieldValue: vi.fn(), rankByRelevance: vi.fn(), }, })); describe('TaskSearchStrategy', () => { let strategy: TaskSearchStrategy; let mockDependencies: StrategyDependencies; let mockTaskFunction: ReturnType<typeof vi.fn>; let mockTask: AttioTask; let mockTaskRecord: AttioRecord; beforeEach(() => { mockTaskFunction = vi.fn(); mockDependencies = { taskFunction: mockTaskFunction, }; mockTask = { id: { value: 'task-123' }, content: 'Test task content', title: 'Test Task', status: 'open', assignees: [], created_at: '2023-01-01T00:00:00Z', } as AttioTask; mockTaskRecord = { id: { value: 'task-123' }, content: 'Test task content', title: 'Test Task', status: 'open', content_plaintext: 'Test task content', } as AttioRecord; strategy = new TaskSearchStrategy(mockDependencies); // Mock SearchUtilities methods vi.mocked(SearchUtilities.getTaskFieldValue).mockImplementation( (record: AttioRecord, field: string) => { const value = (record as any)[field]; return typeof value === 'string' ? value : ''; } ); vi.mocked(SearchUtilities.rankByRelevance).mockImplementation( (results: AttioRecord[]) => results ); vi.mocked(UniversalUtilityService.convertTaskToRecord).mockImplementation( (task: AttioTask) => mockTaskRecord ); vi.mocked(CachingService.getOrLoadTasks).mockResolvedValue({ data: [mockTaskRecord], fromCache: false, }); }); afterEach(() => { vi.clearAllMocks(); }); describe('interface compliance', () => { it('should return correct resource type', () => { expect(strategy.getResourceType()).toBe(UniversalResourceType.TASKS); }); it('should indicate no advanced filtering support', () => { expect(strategy.supportsAdvancedFiltering()).toBe(false); }); it('should indicate query search support', () => { // This was the documentation fix from High Priority Item 1 expect(strategy.supportsQuerySearch()).toBe(true); }); }); describe('basic search', () => { it('should perform basic task search without query', async () => { const results = await strategy.search({ limit: 10, offset: 0, }); expect(results).toEqual([mockTaskRecord]); expect(CachingService.getOrLoadTasks).toHaveBeenCalled(); }); it('should handle empty task list', async () => { vi.mocked(CachingService.getOrLoadTasks).mockResolvedValue({ data: [], fromCache: false, }); const results = await strategy.search({}); expect(results).toEqual([]); }); it('should handle missing taskFunction gracefully', async () => { const strategyWithoutTask = new TaskSearchStrategy({}); vi.mocked(CachingService.getOrLoadTasks).mockImplementation( async (loadFunction) => { const data = await loadFunction(); return { data, fromCache: false }; } ); const results = await strategyWithoutTask.search({}); expect(results).toEqual([]); }); }); describe('content search', () => { beforeEach(() => { vi.mocked(SearchUtilities.getTaskFieldValue).mockImplementation( (record: AttioRecord, field: string) => { switch (field) { case 'content': return 'Test task content'; case 'title': return 'Test Task'; case 'content_plaintext': return 'Test task content'; default: return ''; } } ); }); it('should perform content search with query', async () => { const results = await strategy.search({ query: 'test', search_type: SearchType.CONTENT, limit: 10, offset: 0, }); expect(results).toEqual([mockTaskRecord]); }); it('should filter out non-matching content', async () => { vi.mocked(SearchUtilities.getTaskFieldValue).mockReturnValue( 'Different content' ); const results = await strategy.search({ query: 'test', search_type: SearchType.CONTENT, }); expect(results).toEqual([]); }); it('should support exact match type', async () => { const results = await strategy.search({ query: 'Test task content', search_type: SearchType.CONTENT, match_type: MatchType.EXACT, }); expect(results).toEqual([mockTaskRecord]); }); it('should support custom fields for content search', async () => { const results = await strategy.search({ query: 'test', search_type: SearchType.CONTENT, fields: ['title'], }); expect(results).toEqual([mockTaskRecord]); expect(SearchUtilities.getTaskFieldValue).toHaveBeenCalledWith( mockTaskRecord, 'title' ); }); it('should support relevance sorting', async () => { const results = await strategy.search({ query: 'test', search_type: SearchType.CONTENT, sort: SortType.RELEVANCE, }); expect(results).toEqual([mockTaskRecord]); expect(SearchUtilities.rankByRelevance).toHaveBeenCalledWith( [mockTaskRecord], 'test', ['content', 'title', 'content_plaintext'] ); }); }); describe('pagination', () => { beforeEach(() => { const multipleRecords = [ { ...mockTaskRecord, id: { value: 'task-1' } }, { ...mockTaskRecord, id: { value: 'task-2' } }, { ...mockTaskRecord, id: { value: 'task-3' } }, ]; vi.mocked(CachingService.getOrLoadTasks).mockResolvedValue({ data: multipleRecords, fromCache: false, }); }); it('should handle pagination correctly', async () => { const results = await strategy.search({ limit: 2, offset: 1, }); expect(results).toHaveLength(2); expect(results[0].id.value).toBe('task-2'); expect(results[1].id.value).toBe('task-3'); }); it('should handle offset beyond dataset size', async () => { const results = await strategy.search({ offset: 10, }); expect(results).toEqual([]); }); it('should handle limit that exceeds remaining items', async () => { const results = await strategy.search({ limit: 10, offset: 2, }); expect(results).toHaveLength(1); expect(results[0].id.value).toBe('task-3'); }); }); describe('performance optimization', () => { it('should use cached data when available', async () => { vi.mocked(CachingService.getOrLoadTasks).mockResolvedValue({ data: [mockTaskRecord], fromCache: true, }); const results = await strategy.search({}); expect(results).toEqual([mockTaskRecord]); expect(enhancedPerformanceTracker.markTiming).toHaveBeenCalledWith( 'tasks_search', 'other', 1 ); }); it('should track API performance when not using cache', async () => { vi.mocked(CachingService.getOrLoadTasks).mockResolvedValue({ data: [mockTaskRecord], fromCache: false, }); await strategy.search({}); expect(enhancedPerformanceTracker.markTiming).toHaveBeenCalledWith( 'tasks_search', 'attioApi', expect.any(Number) ); }); }); describe('error handling', () => { it('should handle API errors gracefully', async () => { vi.mocked(CachingService.getOrLoadTasks).mockImplementation( async (loadFunction) => { const data = await loadFunction(); return { data, fromCache: false }; } ); mockTaskFunction.mockRejectedValue(new Error('API Error')); const results = await strategy.search({}); expect(results).toEqual([]); }); it('should handle non-array task response', async () => { vi.mocked(CachingService.getOrLoadTasks).mockImplementation( async (loadFunction) => { const data = await loadFunction(); return { data, fromCache: false }; } ); mockTaskFunction.mockResolvedValue('invalid response' as any); const results = await strategy.search({}); expect(results).toEqual([]); }); }); });

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/kesslerio/attio-mcp-server'

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