Skip to main content
Glama
content-search.test.ts24 kB
/** * Unit tests for content search functionality * Tests the implementation of Issue #474: Content Search */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { UniversalSearchService } from '../../src/services/UniversalSearchService.js'; import { UniversalResourceType, SearchType, MatchType, SortType, } from '../../src/handlers/tool-configs/universal/types.js'; import { AttioRecord, AttioTask, AttioList } from '../../src/types/attio.js'; // Mock the imported modules vi.mock('../../src/objects/companies/index.js', () => ({ advancedSearchCompanies: vi.fn(), })); vi.mock('../../src/objects/people/index.js', () => ({ advancedSearchPeople: vi.fn(), })); vi.mock('../../src/objects/tasks.js', () => ({ listTasks: vi.fn(), })); vi.mock('../../src/objects/lists.js', () => ({ searchLists: vi.fn(), })); vi.mock('../../src/services/CachingService.js', () => ({ CachingService: { getOrLoadTasks: vi.fn().mockImplementation(async (loadFn) => { const data = await loadFn(); return { data, fromCache: false }; }), }, })); vi.mock('../../src/services/UniversalUtilityService.js', () => ({ UniversalUtilityService: { convertTaskToRecord: vi.fn().mockImplementation((task) => ({ id: { record_id: task.id.task_id, task_id: task.id.task_id }, values: { content: task.content, title: task.title || task.content?.substring(0, 50) + '...', content_plaintext: task.content_plaintext || task.content, status: task.status, created_at: task.created_at, updated_at: task.updated_at, }, })), }, })); vi.mock('../../src/services/create/index.js', () => ({ shouldUseMockData: vi.fn(() => false), })); vi.mock('../../src/api/attio-client.js', () => ({ getAttioClient: vi.fn(() => ({ post: vi.fn(), })), })); vi.mock('../../src/middleware/performance-enhanced.js', () => ({ enhancedPerformanceTracker: { startOperation: vi.fn(() => 'test-perf-id'), markApiStart: vi.fn(() => Date.now()), markApiEnd: vi.fn(), markTiming: vi.fn(), endOperation: vi.fn(), }, })); vi.mock('../../src/api/operations/search.js', () => ({ searchObject: vi.fn(), })); import { advancedSearchCompanies } from '../../src/objects/companies/index.js'; import { advancedSearchPeople } from '../../src/objects/people/index.js'; import { listTasks } from '../../src/objects/tasks.js'; import { searchLists } from '../../src/objects/lists.js'; import { searchObject } from '../../src/api/operations/search.js'; describe('Content Search Functionality', () => { beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.clearAllMocks(); }); describe('UniversalSearchService.searchRecords', () => { it('should default to basic search when search_type is not specified', async () => { const mockResults = [ { id: { record_id: '1' }, values: { name: 'Test Company' }, } as AttioRecord, ]; // Basic queries (no search_type) now route through searchObject() vi.mocked(searchObject).mockResolvedValue(mockResults); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: 'test', }); expect(searchObject).toHaveBeenCalled(); expect(result).toEqual(mockResults); }); it('should use content search when search_type is CONTENT', async () => { const mockResults = [ { id: { record_id: '1' }, values: { name: 'Alpha Technologies', description: 'AI solutions for healthcare', }, } as AttioRecord, ]; vi.mocked(advancedSearchCompanies).mockResolvedValue(mockResults); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: 'AI', search_type: SearchType.CONTENT, }); expect(advancedSearchCompanies).toHaveBeenCalledWith( expect.objectContaining({ filters: expect.arrayContaining([ expect.objectContaining({ attribute: { slug: 'name' }, condition: 'contains', value: 'AI', }), expect.objectContaining({ attribute: { slug: 'description' }, condition: 'contains', value: 'AI', }), expect.objectContaining({ attribute: { slug: 'notes' }, condition: 'contains', value: 'AI', }), ]), matchAny: true, }), undefined, undefined ); expect(result).toEqual(mockResults); }); it('should use custom fields when specified', async () => { const mockResults = [] as AttioRecord[]; vi.mocked(advancedSearchCompanies).mockResolvedValue(mockResults); await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: 'test', search_type: SearchType.CONTENT, fields: ['name', 'website'], }); expect(advancedSearchCompanies).toHaveBeenCalledWith( expect.objectContaining({ filters: expect.arrayContaining([ expect.objectContaining({ attribute: { slug: 'name' }, condition: 'contains', value: 'test', }), expect.objectContaining({ attribute: { slug: 'website' }, condition: 'contains', value: 'test', }), ]), matchAny: true, }), undefined, undefined ); }); it('should use exact match when match_type is EXACT', async () => { const mockResults = [] as AttioRecord[]; vi.mocked(advancedSearchCompanies).mockResolvedValue(mockResults); await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: 'Exact Company Name', search_type: SearchType.CONTENT, match_type: MatchType.EXACT, }); expect(advancedSearchCompanies).toHaveBeenCalledWith( expect.objectContaining({ filters: expect.arrayContaining([ expect.objectContaining({ attribute: { slug: 'name' }, condition: 'equals', value: 'Exact Company Name', }), ]), }), undefined, undefined ); }); it('should apply relevance ranking when sort is RELEVANCE', async () => { const mockResults = [ { id: { record_id: '1' }, values: { name: 'AI Company', description: 'Specializes in AI and AI technologies', }, } as AttioRecord, { id: { record_id: '2' }, values: { name: 'Tech Corp', description: 'General technology with some AI', }, } as AttioRecord, ]; vi.mocked(advancedSearchCompanies).mockResolvedValue(mockResults); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: 'AI', search_type: SearchType.CONTENT, sort: SortType.RELEVANCE, }); // First result should have more AI mentions and rank higher expect(result[0].id).toEqual({ record_id: '1' }); }); }); describe('Content Search for People', () => { it('should search across default people fields', async () => { const mockResults = { results: [ { id: { record_id: '1' }, values: { name: 'Alice Smith', notes: 'Expert in machine learning', }, } as AttioRecord, ], pagination: { totalCount: 1, currentPage: 1, pageSize: 10, totalPages: 1, hasMore: false, nextPageUrl: undefined, prevPageUrl: undefined, }, }; vi.mocked(advancedSearchPeople).mockResolvedValue(mockResults); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.PEOPLE, query: 'machine learning', search_type: SearchType.CONTENT, }); expect(advancedSearchPeople).toHaveBeenCalledWith( expect.objectContaining({ filters: expect.arrayContaining([ expect.objectContaining({ attribute: { slug: 'name' }, condition: 'contains', value: 'machine learning', }), expect.objectContaining({ attribute: { slug: 'job_title' }, condition: 'contains', value: 'machine learning', }), expect.objectContaining({ attribute: { slug: 'notes' }, condition: 'contains', value: 'machine learning', }), expect.objectContaining({ attribute: { slug: 'email_addresses' }, condition: 'contains', value: 'machine learning', }), ]), matchAny: true, }), { limit: undefined, offset: undefined } ); expect(result).toEqual(mockResults.results); }); it('should handle people search with custom fields', async () => { const mockResults = { results: [] as AttioRecord[], pagination: { totalCount: 0, currentPage: 1, pageSize: 10, totalPages: 0, hasMore: false, nextPageUrl: undefined, prevPageUrl: undefined, }, }; vi.mocked(advancedSearchPeople).mockResolvedValue(mockResults); await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.PEOPLE, query: 'test', search_type: SearchType.CONTENT, fields: ['name', 'notes'], }); expect(advancedSearchPeople).toHaveBeenCalledWith( expect.objectContaining({ filters: expect.arrayContaining([ expect.objectContaining({ attribute: { slug: 'name' }, condition: 'contains', value: 'test', }), expect.objectContaining({ attribute: { slug: 'notes' }, condition: 'contains', value: 'test', }), ]), matchAny: true, }), { limit: undefined, offset: undefined } ); }); }); describe('Relevance Ranking Algorithm', () => { it('should rank exact matches highest', async () => { const mockResults = [ { id: { record_id: '1' }, values: { name: 'Other Company' }, } as AttioRecord, { id: { record_id: '2' }, values: { name: 'AI' }, } as AttioRecord, { id: { record_id: '3' }, values: { name: 'AI Technologies' }, } as AttioRecord, ]; vi.mocked(advancedSearchCompanies).mockResolvedValue(mockResults); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: 'AI', search_type: SearchType.CONTENT, sort: SortType.RELEVANCE, }); // Exact match 'AI' should rank first expect(result[0].id).toEqual({ record_id: '2' }); // Starts with 'AI' should rank second expect(result[1].id).toEqual({ record_id: '3' }); }); it('should rank by number of occurrences', async () => { const mockResults = [ { id: { record_id: '1' }, values: { name: 'Tech Company', description: 'AI is mentioned once', }, } as AttioRecord, { id: { record_id: '2' }, values: { name: 'AI Corp', description: 'AI solutions using AI technology for AI applications', }, } as AttioRecord, ]; vi.mocked(advancedSearchCompanies).mockResolvedValue(mockResults); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: 'AI', search_type: SearchType.CONTENT, sort: SortType.RELEVANCE, }); // Company with more AI mentions should rank first expect(result[0].id).toEqual({ record_id: '2' }); }); it('should handle partial word matches', async () => { const mockResults = [ { id: { record_id: '1' }, values: { name: 'Company', description: 'automation systems', }, } as AttioRecord, ]; vi.mocked(advancedSearchCompanies).mockResolvedValue(mockResults); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: 'automat', search_type: SearchType.CONTENT, match_type: MatchType.PARTIAL, }); expect(result).toHaveLength(1); expect(result[0].id).toEqual({ record_id: '1' }); }); }); describe('Edge Cases', () => { it('should handle empty query gracefully', async () => { const mockResults = [] as AttioRecord[]; vi.mocked(advancedSearchCompanies).mockResolvedValue(mockResults); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: '', search_type: SearchType.CONTENT, }); expect(result).toEqual([]); }); it('should handle null/undefined fields gracefully', async () => { const mockResults = [ { id: { record_id: '1' }, values: { name: 'Test Company', description: null, notes: undefined, }, } as unknown as AttioRecord, ]; vi.mocked(advancedSearchCompanies).mockResolvedValue(mockResults); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: 'test', search_type: SearchType.CONTENT, sort: SortType.RELEVANCE, }); expect(result).toHaveLength(1); expect(result[0].id).toEqual({ record_id: '1' }); }); it('should handle special characters in search query', async () => { const mockResults = [] as AttioRecord[]; vi.mocked(advancedSearchCompanies).mockResolvedValue(mockResults); await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: 'AI & Machine Learning (ML)', search_type: SearchType.CONTENT, }); expect(advancedSearchCompanies).toHaveBeenCalledWith( expect.objectContaining({ filters: expect.arrayContaining([ expect.objectContaining({ value: 'AI & Machine Learning (ML)', }), ]), }), undefined, undefined ); }); }); describe('Pagination with Content Search', () => { it('should pass pagination parameters correctly', async () => { const mockResults = [] as AttioRecord[]; vi.mocked(advancedSearchCompanies).mockResolvedValue(mockResults); await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.COMPANIES, query: 'test', search_type: SearchType.CONTENT, limit: 20, offset: 10, }); expect(advancedSearchCompanies).toHaveBeenCalledWith( expect.any(Object), 20, 10 ); }); }); describe('Content Search for Tasks', () => { it('should search across default task fields', async () => { const mockTasks = [ { id: { task_id: '1' }, content: 'Complete project alpha testing', title: 'Alpha Project', content_plaintext: 'Complete project alpha testing', status: 'open', }, { id: { task_id: '2' }, content: 'Review beta release', title: 'Beta Review', content_plaintext: 'Review beta release', status: 'open', }, ]; vi.mocked(listTasks).mockResolvedValue( mockTasks as unknown as AttioTask[] ); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.TASKS, query: 'project', search_type: SearchType.CONTENT, }); expect(listTasks).toHaveBeenCalled(); expect(result).toHaveLength(1); expect(result[0].values?.content).toContain('project alpha testing'); }); it('should search across custom task fields', async () => { const mockTasks = [ { id: { task_id: '1' }, content: 'Task content here', title: 'Important task title', content_plaintext: 'Task content here', status: 'open', }, ]; vi.mocked(listTasks).mockResolvedValue( mockTasks as unknown as AttioTask[] ); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.TASKS, query: 'important', search_type: SearchType.CONTENT, fields: ['title'], }); expect(result).toHaveLength(1); expect(result[0].values?.title).toContain('Important task title'); }); it('should handle exact match for tasks', async () => { const mockTasks = [ { id: { task_id: '1' }, content: 'test', title: 'Test Task', content_plaintext: 'test', status: 'open', }, { id: { task_id: '2' }, content: 'testing something', title: 'Testing', content_plaintext: 'testing something', status: 'open', }, ]; vi.mocked(listTasks).mockResolvedValue( mockTasks as unknown as AttioTask[] ); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.TASKS, query: 'test', search_type: SearchType.CONTENT, match_type: MatchType.EXACT, }); expect(result).toHaveLength(1); expect(result[0].values?.content).toBe('test'); }); it('should apply relevance ranking for tasks', async () => { const mockTasks = [ { id: { task_id: '1' }, content: 'Some task with AI mention', title: 'Regular Task', content_plaintext: 'Some task with AI mention', status: 'open', }, { id: { task_id: '2' }, content: 'AI development task', title: 'AI Task', content_plaintext: 'AI development task', status: 'open', }, ]; vi.mocked(listTasks).mockResolvedValue( mockTasks as unknown as AttioTask[] ); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.TASKS, query: 'AI', search_type: SearchType.CONTENT, sort: SortType.RELEVANCE, }); expect(result).toHaveLength(2); // Task with AI in both title and content should rank higher expect(result[0].id?.task_id).toBe('2'); }); }); describe('Content Search for Lists', () => { it('should search across default list fields', async () => { const mockLists = [ { id: { list_id: '1' }, name: 'Customer Prospects', title: 'Customer Prospects', description: 'List of potential customers', api_slug: 'customers', }, { id: { list_id: '2' }, name: 'Employee List', title: 'Employee List', description: 'All company employees', api_slug: 'employees', }, ]; vi.mocked(searchLists).mockResolvedValue( mockLists as unknown as AttioList[] ); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.LISTS, query: 'customer', search_type: SearchType.CONTENT, }); expect(searchLists).toHaveBeenCalledWith('', 100, 0); expect(result).toHaveLength(1); expect(result[0].values?.name).toContain('Customer'); }); it('should search across custom list fields', async () => { const mockLists = [ { id: { list_id: '1' }, name: 'Test List', title: 'Test List', description: 'Important test data here', api_slug: 'test', }, ]; vi.mocked(searchLists).mockResolvedValue( mockLists as unknown as AttioList[] ); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.LISTS, query: 'important', search_type: SearchType.CONTENT, fields: ['description'], }); expect(result).toHaveLength(1); expect(result[0].values?.description).toContain('Important test data'); }); it('should handle exact match for lists', async () => { const mockLists = [ { id: { list_id: '1' }, name: 'test', title: 'test', description: 'A test list', api_slug: 'test', }, { id: { list_id: '2' }, name: 'testing', title: 'testing', description: 'A testing list', api_slug: 'testing', }, ]; vi.mocked(searchLists).mockResolvedValue( mockLists as unknown as AttioList[] ); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.LISTS, query: 'test', search_type: SearchType.CONTENT, match_type: MatchType.EXACT, }); expect(result).toHaveLength(1); expect(result[0].values?.name).toBe('test'); }); it('should apply relevance ranking for lists', async () => { const mockLists = [ { id: { list_id: '1' }, name: 'Customer List', title: 'Customer List', description: 'Some customer data', api_slug: 'customers', }, { id: { list_id: '2' }, name: 'Customer Prospects', title: 'Customer Prospects', description: 'Customer prospect tracking', api_slug: 'customer-prospects', }, ]; vi.mocked(searchLists).mockResolvedValue( mockLists as unknown as AttioList[] ); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.LISTS, query: 'customer', search_type: SearchType.CONTENT, sort: SortType.RELEVANCE, }); expect(result).toHaveLength(2); // List with more customer mentions should rank higher expect(result[0].id?.list_id).toBe('2'); }); it('should handle pagination for filtered lists', async () => { const mockLists = Array.from({ length: 15 }, (_, i) => ({ id: { list_id: `${i + 1}` }, name: `Customer List ${i + 1}`, title: `Customer List ${i + 1}`, description: `Customer data ${i + 1}`, api_slug: `customers-${i + 1}`, })); vi.mocked(searchLists).mockResolvedValue( mockLists as unknown as AttioList[] ); const result = await UniversalSearchService.searchRecords({ resource_type: UniversalResourceType.LISTS, query: 'customer', search_type: SearchType.CONTENT, limit: 5, offset: 0, }); expect(result).toHaveLength(5); expect(result[0].values?.name).toBe('Customer List 1'); }); }); });

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