Skip to main content
Glama
search-integration.test.ts29.5 kB
/** * Search Integration Tests (Mocked) * * Tests the interaction between FullTextSearchEngine, MetadataFilterEngine, * and MemoryRepository using mocks for external dependencies (database). * * This is an integration test that verifies internal module interactions work correctly, * NOT a test of external service integration. * * Requirements: 12.2, 12.3, 12.4, 12.6 */ import type { PoolClient } from "pg"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { FilterResult, FullTextSearchQuery, FullTextSearchResponse, MetadataFilters, } from "../../search/types"; // Create mock database client function createMockClient(): PoolClient { const mockClient = { query: vi.fn(), release: vi.fn(), } as unknown as PoolClient; return mockClient; } // Create mock database manager function createMockDbManager() { const mockClient = createMockClient(); return { client: mockClient, manager: { getConnection: vi.fn().mockResolvedValue(mockClient), releaseConnection: vi.fn(), connect: vi.fn().mockResolvedValue(undefined), disconnect: vi.fn().mockResolvedValue(undefined), }, }; } // Create mock full-text search engine function createMockFullTextSearchEngine() { return { search: vi.fn(), clearCache: vi.fn(), getCacheStats: vi.fn().mockReturnValue({ hits: 0, misses: 0, size: 0, hitRate: 0, }), }; } // Create mock metadata filter engine function createMockMetadataFilterEngine() { return { filter: vi.fn(), }; } // Create mock memory repository function createMockMemoryRepository() { return { create: vi.fn(), retrieve: vi.fn(), update: vi.fn(), delete: vi.fn(), search: vi.fn(), }; } describe("Search Integration (Mocked)", () => { // Database and memory repo are created for context but not directly used in assertions let mockFullTextSearch: ReturnType<typeof createMockFullTextSearchEngine>; let mockMetadataFilter: ReturnType<typeof createMockMetadataFilterEngine>; beforeEach(() => { vi.clearAllMocks(); createMockDbManager(); // Create for context mockFullTextSearch = createMockFullTextSearchEngine(); mockMetadataFilter = createMockMetadataFilterEngine(); createMockMemoryRepository(); // Create for context }); afterEach(() => { vi.restoreAllMocks(); }); describe("Full-Text Search + Memory Repository Integration", () => { it("should coordinate full-text search with memory retrieval", async () => { const mockUserId = "test-user-1"; const mockMemoryId = "memory-123"; const mockTimestamp = new Date("2024-12-11T10:00:00Z"); // Setup mock search response const searchResponse: FullTextSearchResponse = { results: [ { memoryId: mockMemoryId, content: "Machine learning algorithms for data analysis", headline: "<b>Machine</b> <b>learning</b> algorithms for data analysis", rank: 0.85, matchedTerms: ["machine", "learning"], createdAt: mockTimestamp, salience: 0.7, strength: 0.9, }, ], statistics: { totalResults: 1, searchTime: 15, indexUsed: true, }, }; mockFullTextSearch.search.mockResolvedValue(searchResponse); // Execute search const query: FullTextSearchQuery = { query: "machine & learning", userId: mockUserId, }; const result = await mockFullTextSearch.search(query); expect(mockFullTextSearch.search).toHaveBeenCalledWith(query); expect(result.results).toHaveLength(1); expect(result.results[0].memoryId).toBe(mockMemoryId); expect(result.results[0].rank).toBe(0.85); expect(result.statistics.indexUsed).toBe(true); }); it("should handle search with boolean operators", async () => { const mockUserId = "test-user-1"; // Setup mock for AND operator search mockFullTextSearch.search.mockResolvedValue({ results: [ { memoryId: "memory-1", content: "Deep learning neural networks", headline: "<b>Deep</b> <b>learning</b> neural networks", rank: 0.9, matchedTerms: ["deep", "learning"], createdAt: new Date(), salience: 0.8, strength: 1.0, }, ], statistics: { totalResults: 1, searchTime: 10, indexUsed: true }, }); const result = await mockFullTextSearch.search({ query: "deep & learning", userId: mockUserId, }); expect(result.results).toHaveLength(1); expect(result.results[0].content).toContain("Deep learning"); }); it("should handle search with NOT operator", async () => { const mockUserId = "test-user-1"; // Setup mock for NOT operator search mockFullTextSearch.search.mockResolvedValue({ results: [ { memoryId: "memory-2", content: "Machine learning basics", headline: "<b>Machine</b> <b>learning</b> basics", rank: 0.75, matchedTerms: ["machine", "learning"], createdAt: new Date(), salience: 0.6, strength: 0.9, }, ], statistics: { totalResults: 1, searchTime: 12, indexUsed: true }, }); const result = await mockFullTextSearch.search({ query: "learning & !deep", userId: mockUserId, }); expect(result.results).toHaveLength(1); expect(result.results[0].content).not.toContain("deep"); }); it("should handle phrase search with quotes", async () => { const mockUserId = "test-user-1"; mockFullTextSearch.search.mockResolvedValue({ results: [ { memoryId: "memory-3", content: "neural networks are powerful", headline: "<b>neural networks</b> are powerful", rank: 0.95, matchedTerms: ["neural networks"], createdAt: new Date(), salience: 0.7, strength: 1.0, }, ], statistics: { totalResults: 1, searchTime: 8, indexUsed: true }, }); const result = await mockFullTextSearch.search({ query: '"neural networks"', userId: mockUserId, }); expect(result.results).toHaveLength(1); expect(result.results[0].content).toContain("neural networks"); }); }); describe("Metadata Filter + Memory Repository Integration", () => { it("should coordinate metadata filtering with memory retrieval", async () => { const mockUserId = "test-user-1"; // Setup mock filter response const filterResult: FilterResult = { memoryIds: ["memory-1", "memory-2", "memory-3"], count: 3, executionTimeMs: 5, }; mockMetadataFilter.filter.mockResolvedValue(filterResult); // Execute filter const filters: MetadataFilters = { keywords: ["machine", "learning"], keywordOperator: "AND", userId: mockUserId, }; const result = await mockMetadataFilter.filter(filters); expect(mockMetadataFilter.filter).toHaveBeenCalledWith(filters); expect(result.memoryIds).toHaveLength(3); expect(result.count).toBe(3); expect(result.executionTimeMs).toBeLessThan(50); }); it("should filter by tags with OR logic", async () => { mockMetadataFilter.filter.mockResolvedValue({ memoryIds: ["memory-1", "memory-2"], count: 2, executionTimeMs: 3, }); const result = await mockMetadataFilter.filter({ tags: ["ai", "research"], tagOperator: "OR", }); expect(result.memoryIds).toHaveLength(2); }); it("should filter by category", async () => { mockMetadataFilter.filter.mockResolvedValue({ memoryIds: ["memory-1"], count: 1, executionTimeMs: 2, }); const result = await mockMetadataFilter.filter({ categories: ["technology"], userId: "test-user-1", }); expect(result.memoryIds).toHaveLength(1); }); it("should filter by importance range", async () => { mockMetadataFilter.filter.mockResolvedValue({ memoryIds: ["memory-high-1", "memory-high-2"], count: 2, executionTimeMs: 4, }); const result = await mockMetadataFilter.filter({ importanceMin: 0.7, importanceMax: 1.0, }); expect(result.count).toBe(2); }); it("should filter by date range", async () => { const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); mockMetadataFilter.filter.mockResolvedValue({ memoryIds: ["recent-memory-1", "recent-memory-2"], count: 2, executionTimeMs: 3, }); const result = await mockMetadataFilter.filter({ createdAfter: oneDayAgo, }); expect(result.count).toBe(2); }); }); describe("Combined Search + Filter Integration", () => { it("should combine full-text search with metadata filtering", async () => { const mockUserId = "test-user-1"; // First, perform full-text search mockFullTextSearch.search.mockResolvedValue({ results: [ { memoryId: "memory-1", content: "AI research paper", headline: "<b>AI</b> research paper", rank: 0.9, matchedTerms: ["ai"], createdAt: new Date(), salience: 0.8, strength: 1.0, }, { memoryId: "memory-2", content: "AI tutorial for beginners", headline: "<b>AI</b> tutorial for beginners", rank: 0.7, matchedTerms: ["ai"], createdAt: new Date(), salience: 0.6, strength: 0.9, }, ], statistics: { totalResults: 2, searchTime: 10, indexUsed: true }, }); // Then, filter by metadata mockMetadataFilter.filter.mockResolvedValue({ memoryIds: ["memory-1"], // Only research papers count: 1, executionTimeMs: 3, }); // Execute combined search const searchResults = await mockFullTextSearch.search({ query: "AI", userId: mockUserId, }); const filterResults = await mockMetadataFilter.filter({ tags: ["research"], userId: mockUserId, }); // Combine results - intersection const combinedIds = searchResults.results .map((r: { memoryId: string }) => r.memoryId) .filter((id: string) => filterResults.memoryIds.includes(id)); expect(combinedIds).toHaveLength(1); expect(combinedIds[0]).toBe("memory-1"); }); it("should handle complex query with all filters", async () => { const mockUserId = "test-user-1"; const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); // Setup complex filter mockMetadataFilter.filter.mockResolvedValue({ memoryIds: ["memory-complex-1"], count: 1, executionTimeMs: 8, }); const result = await mockMetadataFilter.filter({ keywords: ["ai", "learning"], keywordOperator: "OR", tags: ["research"], tagOperator: "AND", categories: ["technology", "science"], importanceMin: 0.7, createdAfter: oneDayAgo, userId: mockUserId, }); expect(result.count).toBe(1); expect(result.executionTimeMs).toBeLessThan(50); }); }); describe("Search Result Ranking Integration", () => { it("should rank results by relevance score", async () => { mockFullTextSearch.search.mockResolvedValue({ results: [ { memoryId: "memory-high", content: "machine learning machine learning machine learning", headline: "<b>machine</b> <b>learning</b>...", rank: 0.95, matchedTerms: ["machine", "learning"], createdAt: new Date(), salience: 0.8, strength: 1.0, }, { memoryId: "memory-medium", content: "machine learning algorithms", headline: "<b>machine</b> <b>learning</b> algorithms", rank: 0.75, matchedTerms: ["machine", "learning"], createdAt: new Date(), salience: 0.6, strength: 0.9, }, { memoryId: "memory-low", content: "learning about machines", headline: "<b>learning</b> about machines", rank: 0.5, matchedTerms: ["learning"], createdAt: new Date(), salience: 0.5, strength: 0.8, }, ], statistics: { totalResults: 3, searchTime: 12, indexUsed: true }, }); const result = await mockFullTextSearch.search({ query: "machine & learning", userId: "test-user-1", }); // Verify results are sorted by rank descending expect(result.results[0].rank).toBeGreaterThan(result.results[1].rank); expect(result.results[1].rank).toBeGreaterThan(result.results[2].rank); }); it("should maintain ranking consistency across searches", async () => { const searchResponse = { results: [ { memoryId: "memory-1", content: "test content", headline: "<b>test</b> content", rank: 0.8, matchedTerms: ["test"], createdAt: new Date(), salience: 0.7, strength: 1.0, }, ], statistics: { totalResults: 1, searchTime: 5, indexUsed: true }, }; mockFullTextSearch.search.mockResolvedValue(searchResponse); const result1 = await mockFullTextSearch.search({ query: "test", userId: "test-user-1", }); const result2 = await mockFullTextSearch.search({ query: "test", userId: "test-user-1", }); expect(result1.results[0].memoryId).toBe(result2.results[0].memoryId); expect(result1.results[0].rank).toBe(result2.results[0].rank); }); }); describe("Search Pagination Integration", () => { it("should support pagination with limit and offset", async () => { // Page 1 mockFullTextSearch.search.mockResolvedValueOnce({ results: [ { memoryId: "memory-1", content: "Page 1 item 1", headline: "", rank: 0.9, matchedTerms: [], createdAt: new Date(), salience: 0.5, strength: 1.0, }, { memoryId: "memory-2", content: "Page 1 item 2", headline: "", rank: 0.85, matchedTerms: [], createdAt: new Date(), salience: 0.5, strength: 1.0, }, ], statistics: { totalResults: 4, searchTime: 10, indexUsed: true }, }); // Page 2 mockFullTextSearch.search.mockResolvedValueOnce({ results: [ { memoryId: "memory-3", content: "Page 2 item 1", headline: "", rank: 0.8, matchedTerms: [], createdAt: new Date(), salience: 0.5, strength: 1.0, }, { memoryId: "memory-4", content: "Page 2 item 2", headline: "", rank: 0.75, matchedTerms: [], createdAt: new Date(), salience: 0.5, strength: 1.0, }, ], statistics: { totalResults: 4, searchTime: 8, indexUsed: true }, }); const page1 = await mockFullTextSearch.search({ query: "test", userId: "test-user-1", maxResults: 2, offset: 0, }); const page2 = await mockFullTextSearch.search({ query: "test", userId: "test-user-1", maxResults: 2, offset: 2, }); expect(page1.results).toHaveLength(2); expect(page2.results).toHaveLength(2); // Pages should not overlap const page1Ids = new Set(page1.results.map((r: { memoryId: string }) => r.memoryId)); const page2Ids = new Set(page2.results.map((r: { memoryId: string }) => r.memoryId)); const intersection = [...page1Ids].filter((id) => page2Ids.has(id)); expect(intersection).toHaveLength(0); }); it("should support metadata filter pagination", async () => { mockMetadataFilter.filter .mockResolvedValueOnce({ memoryIds: ["memory-1", "memory-2"], count: 4, executionTimeMs: 3, }) .mockResolvedValueOnce({ memoryIds: ["memory-3", "memory-4"], count: 4, executionTimeMs: 3, }); const page1 = await mockMetadataFilter.filter({ keywords: ["test"], limit: 2, offset: 0, }); const page2 = await mockMetadataFilter.filter({ keywords: ["test"], limit: 2, offset: 2, }); expect(page1.memoryIds).toHaveLength(2); expect(page2.memoryIds).toHaveLength(2); }); }); describe("Search Caching Integration", () => { it("should cache search results", async () => { const searchResponse = { results: [ { memoryId: "memory-1", content: "Cached content", headline: "<b>Cached</b> content", rank: 0.8, matchedTerms: ["cached"], createdAt: new Date(), salience: 0.7, strength: 1.0, }, ], statistics: { totalResults: 1, searchTime: 15, indexUsed: true }, }; // First call - not cached mockFullTextSearch.search.mockResolvedValueOnce(searchResponse); // Second call - cached (searchTime = 0) mockFullTextSearch.search.mockResolvedValueOnce({ ...searchResponse, statistics: { ...searchResponse.statistics, searchTime: 0 }, }); const result1 = await mockFullTextSearch.search({ query: "cached", userId: "test-user-1", }); const result2 = await mockFullTextSearch.search({ query: "cached", userId: "test-user-1", }); expect(result1.results[0].memoryId).toBe(result2.results[0].memoryId); expect(result2.statistics.searchTime).toBe(0); // Cached }); it("should provide cache statistics", async () => { mockFullTextSearch.getCacheStats.mockReturnValue({ hits: 5, misses: 10, size: 15, hitRate: 0.33, }); const stats = mockFullTextSearch.getCacheStats(); expect(stats.hits).toBe(5); expect(stats.misses).toBe(10); expect(stats.hitRate).toBeGreaterThan(0); }); it("should clear cache on demand", async () => { mockFullTextSearch.clearCache.mockImplementation(() => { mockFullTextSearch.getCacheStats.mockReturnValue({ hits: 0, misses: 0, size: 0, hitRate: 0, }); }); mockFullTextSearch.clearCache(); const stats = mockFullTextSearch.getCacheStats(); expect(stats.size).toBe(0); }); }); describe("Search Validation Integration", () => { it("should reject empty query", async () => { mockFullTextSearch.search.mockRejectedValue(new Error("Query cannot be empty")); await expect( mockFullTextSearch.search({ query: "", userId: "test-user-1", }) ).rejects.toThrow("Query cannot be empty"); }); it("should reject query exceeding max length", async () => { const longQuery = "a".repeat(1001); mockFullTextSearch.search.mockRejectedValue(new Error("Query exceeds maximum length")); await expect( mockFullTextSearch.search({ query: longQuery, userId: "test-user-1", }) ).rejects.toThrow("Query exceeds maximum length"); }); it("should reject invalid importance range in filters", async () => { mockMetadataFilter.filter.mockRejectedValue( new Error("importanceMin must be between 0 and 1") ); await expect( mockMetadataFilter.filter({ importanceMin: -0.1, }) ).rejects.toThrow("importanceMin must be between 0 and 1"); }); it("should reject importanceMin > importanceMax", async () => { mockMetadataFilter.filter.mockRejectedValue( new Error("importanceMin cannot be greater than importanceMax") ); await expect( mockMetadataFilter.filter({ importanceMin: 0.8, importanceMax: 0.3, }) ).rejects.toThrow("importanceMin cannot be greater than importanceMax"); }); it("should reject invalid date range", async () => { mockMetadataFilter.filter.mockRejectedValue( new Error("createdAfter cannot be after createdBefore") ); await expect( mockMetadataFilter.filter({ createdAfter: new Date("2024-12-01"), createdBefore: new Date("2024-11-01"), }) ).rejects.toThrow("createdAfter cannot be after createdBefore"); }); it("should reject negative offset", async () => { mockMetadataFilter.filter.mockRejectedValue(new Error("offset must be non-negative")); await expect( mockMetadataFilter.filter({ offset: -5, }) ).rejects.toThrow("offset must be non-negative"); }); }); describe("Search User Isolation Integration", () => { it("should isolate search results by userId", async () => { // User A search mockFullTextSearch.search.mockResolvedValueOnce({ results: [ { memoryId: "user-a-memory", content: "User A memory", headline: "User A memory", rank: 0.8, matchedTerms: ["memory"], createdAt: new Date(), salience: 0.7, strength: 1.0, }, ], statistics: { totalResults: 1, searchTime: 10, indexUsed: true }, }); // User B search mockFullTextSearch.search.mockResolvedValueOnce({ results: [ { memoryId: "user-b-memory", content: "User B memory", headline: "User B memory", rank: 0.8, matchedTerms: ["memory"], createdAt: new Date(), salience: 0.7, strength: 1.0, }, ], statistics: { totalResults: 1, searchTime: 10, indexUsed: true }, }); const resultA = await mockFullTextSearch.search({ query: "memory", userId: "user-a", }); const resultB = await mockFullTextSearch.search({ query: "memory", userId: "user-b", }); // User A should not see User B's memories expect(resultA.results[0].content).not.toContain("User B"); expect(resultB.results[0].content).not.toContain("User A"); }); it("should isolate metadata filter results by userId", async () => { mockMetadataFilter.filter .mockResolvedValueOnce({ memoryIds: ["user-a-memory-1", "user-a-memory-2"], count: 2, executionTimeMs: 3, }) .mockResolvedValueOnce({ memoryIds: ["user-b-memory-1"], count: 1, executionTimeMs: 3, }); const resultA = await mockMetadataFilter.filter({ keywords: ["test"], userId: "user-a", }); const resultB = await mockMetadataFilter.filter({ keywords: ["test"], userId: "user-b", }); // Results should be different for different users expect(resultA.memoryIds).not.toEqual(resultB.memoryIds); }); }); describe("Search Error Handling Integration", () => { it("should handle database connection failure gracefully", async () => { mockFullTextSearch.search.mockRejectedValue(new Error("Database connection failed")); await expect( mockFullTextSearch.search({ query: "test", userId: "test-user-1", }) ).rejects.toThrow("Database connection failed"); }); it("should handle search timeout gracefully", async () => { mockFullTextSearch.search.mockRejectedValue(new Error("Search timeout exceeded")); await expect( mockFullTextSearch.search({ query: "complex query", userId: "test-user-1", }) ).rejects.toThrow("Search timeout exceeded"); }); it("should handle filter execution failure", async () => { mockMetadataFilter.filter.mockRejectedValue(new Error("Filter execution failed")); await expect( mockMetadataFilter.filter({ keywords: ["test"], }) ).rejects.toThrow("Filter execution failed"); }); it("should return empty results for no matches", async () => { mockFullTextSearch.search.mockResolvedValue({ results: [], statistics: { totalResults: 0, searchTime: 5, indexUsed: true }, }); const result = await mockFullTextSearch.search({ query: "nonexistentterm12345", userId: "test-user-1", }); expect(result.results).toHaveLength(0); expect(result.statistics.totalResults).toBe(0); }); it("should return empty filter results for no matches", async () => { mockMetadataFilter.filter.mockResolvedValue({ memoryIds: [], count: 0, executionTimeMs: 2, }); const result = await mockMetadataFilter.filter({ keywords: ["nonexistent"], }); expect(result.memoryIds).toHaveLength(0); expect(result.count).toBe(0); }); }); describe("Search Highlighting Integration", () => { it("should generate highlighted headlines for matched terms", async () => { mockFullTextSearch.search.mockResolvedValue({ results: [ { memoryId: "memory-1", content: "Machine learning is a subset of artificial intelligence", headline: "<b>Machine</b> <b>learning</b> is a subset of artificial intelligence", rank: 0.85, matchedTerms: ["machine", "learning"], createdAt: new Date(), salience: 0.7, strength: 1.0, }, ], statistics: { totalResults: 1, searchTime: 10, indexUsed: true }, }); const result = await mockFullTextSearch.search({ query: "machine & learning", userId: "test-user-1", }); expect(result.results[0].headline).toContain("<b>"); expect(result.results[0].headline).toContain("</b>"); }); it("should highlight multiple matches in headline", async () => { mockFullTextSearch.search.mockResolvedValue({ results: [ { memoryId: "memory-1", content: "Deep learning neural networks", headline: "<b>Deep</b> <b>learning</b> neural networks", rank: 0.9, matchedTerms: ["deep", "learning"], createdAt: new Date(), salience: 0.8, strength: 1.0, }, ], statistics: { totalResults: 1, searchTime: 8, indexUsed: true }, }); const result = await mockFullTextSearch.search({ query: "deep & learning", userId: "test-user-1", }); const headline = result.results[0].headline; const matches = (headline.match(/<b>/g) || []).length; expect(matches).toBeGreaterThanOrEqual(2); }); }); describe("Search Strength and Salience Filtering Integration", () => { it("should filter by minimum strength", async () => { mockFullTextSearch.search.mockResolvedValue({ results: [ { memoryId: "high-strength-memory", content: "High strength memory", headline: "High strength memory", rank: 0.8, matchedTerms: ["strength"], createdAt: new Date(), salience: 0.7, strength: 0.9, }, ], statistics: { totalResults: 1, searchTime: 10, indexUsed: true }, }); const result = await mockFullTextSearch.search({ query: "strength", userId: "test-user-1", minStrength: 0.8, }); expect(result.results).toHaveLength(1); expect(result.results[0].strength).toBeGreaterThanOrEqual(0.8); }); it("should filter by minimum salience", async () => { mockFullTextSearch.search.mockResolvedValue({ results: [ { memoryId: "high-salience-memory", content: "High salience memory", headline: "High salience memory", rank: 0.8, matchedTerms: ["salience"], createdAt: new Date(), salience: 0.9, strength: 1.0, }, ], statistics: { totalResults: 1, searchTime: 10, indexUsed: true }, }); const result = await mockFullTextSearch.search({ query: "salience", userId: "test-user-1", minSalience: 0.8, }); expect(result.results).toHaveLength(1); expect(result.results[0].salience).toBeGreaterThanOrEqual(0.8); }); }); });

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/keyurgolani/ThoughtMcp'

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