Skip to main content
Glama
search-integration.test.ts57.2 kB
/** * Tests for MemorySearchEngine - Integrated Search Orchestrator * * Phase 3 Task 3.4.1: Write tests for search integration * Requirements: 4.1, 4.2, 4.3, 4.4, 4.5 * * Tests cover: * - Multi-strategy search execution * - Composite result ranking * - Query caching with TTL * - Pagination and result limiting * - Search analytics tracking * - Validation and error handling */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { MemorySearchEngine } from "../../../search/memory-search-engine"; import type { IntegratedSearchQuery } from "../../../search/types"; describe("MemorySearchEngine - Integrated Search", () => { let mockDb: any; let mockEmbeddingStorage: any; let searchEngine: MemorySearchEngine; let mockClient: any; beforeEach(() => { // Mock database client mockClient = { query: vi.fn(), }; // Mock database connection manager mockDb = { pool: { query: vi.fn(), }, getConnection: vi.fn().mockResolvedValue(mockClient), releaseConnection: vi.fn(), }; // Mock embedding storage mockEmbeddingStorage = { vectorSimilaritySearch: vi.fn(), }; // Create search engine with default config searchEngine = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined); }); afterEach(() => { vi.clearAllMocks(); }); describe("Query Validation", () => { it("should reject query with no search criteria", async () => { const query: IntegratedSearchQuery = { userId: "user-1", }; await expect(searchEngine.search(query)).rejects.toMatchObject({ name: "IntegratedSearchValidationError", message: "At least one search criterion must be provided", code: "VALIDATION_ERROR", }); }); it("should reject query with negative limit", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", limit: -1, }; await expect(searchEngine.search(query)).rejects.toMatchObject({ name: "IntegratedSearchValidationError", message: "Limit must be non-negative", code: "VALIDATION_ERROR", field: "limit", }); }); it("should reject query with limit exceeding maximum", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", limit: 10000, // Exceeds default max of 1000 }; await expect(searchEngine.search(query)).rejects.toMatchObject({ name: "IntegratedSearchValidationError", message: "Limit cannot exceed 1000", code: "VALIDATION_ERROR", field: "limit", }); }); it("should reject query with negative offset", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", offset: -1, }; await expect(searchEngine.search(query)).rejects.toMatchObject({ name: "IntegratedSearchValidationError", message: "Offset must be non-negative", code: "VALIDATION_ERROR", field: "offset", }); }); it("should reject query with minStrength < 0", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", minStrength: -0.1, }; await expect(searchEngine.search(query)).rejects.toMatchObject({ name: "IntegratedSearchValidationError", message: "minStrength must be between 0 and 1", code: "VALIDATION_ERROR", field: "minStrength", }); }); it("should reject query with minStrength > 1", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", minStrength: 1.5, }; await expect(searchEngine.search(query)).rejects.toMatchObject({ name: "IntegratedSearchValidationError", message: "minStrength must be between 0 and 1", code: "VALIDATION_ERROR", field: "minStrength", }); }); it("should reject query with minSalience < 0", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", minSalience: -0.1, }; await expect(searchEngine.search(query)).rejects.toMatchObject({ name: "IntegratedSearchValidationError", message: "minSalience must be between 0 and 1", code: "VALIDATION_ERROR", field: "minSalience", }); }); it("should reject query with minSalience > 1", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", minSalience: 1.5, }; await expect(searchEngine.search(query)).rejects.toMatchObject({ name: "IntegratedSearchValidationError", message: "minSalience must be between 0 and 1", code: "VALIDATION_ERROR", field: "minSalience", }); }); it("should accept valid query with text", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test query", limit: 10, offset: 0, }; // Mock database response mockDb.pool.query.mockResolvedValue({ rows: [] }); await expect(searchEngine.search(query)).resolves.toBeDefined(); }); }); describe("Cache Management", () => { it("should provide cache statistics", () => { const stats = searchEngine.getCacheStats(); expect(stats).toHaveProperty("hits"); expect(stats).toHaveProperty("misses"); expect(stats).toHaveProperty("size"); expect(stats).toHaveProperty("maxSize"); expect(stats).toHaveProperty("hitRate"); expect(stats.maxSize).toBe(1000); }); it("should clear cache", () => { searchEngine.clearCache(); const stats = searchEngine.getCacheStats(); expect(stats.size).toBe(0); expect(stats.hits).toBe(0); expect(stats.misses).toBe(0); }); it("should track cache hits and misses", () => { const initialStats = searchEngine.getCacheStats(); expect(initialStats.hits).toBeGreaterThanOrEqual(0); expect(initialStats.misses).toBeGreaterThanOrEqual(0); }); it("should calculate hit rate correctly", () => { const stats = searchEngine.getCacheStats(); if (stats.hits + stats.misses > 0) { expect(stats.hitRate).toBeGreaterThanOrEqual(0); expect(stats.hitRate).toBeLessThanOrEqual(1); } else { expect(stats.hitRate).toBe(0); } }); it("should enforce cache size limit", () => { const stats = searchEngine.getCacheStats(); expect(stats.size).toBeLessThanOrEqual(stats.maxSize); }); }); describe("Analytics", () => { it("should provide analytics summary", () => { const summary = searchEngine.getAnalyticsSummary(); expect(summary).toHaveProperty("totalSearches"); expect(summary).toHaveProperty("avgExecutionTimeMs"); expect(summary).toHaveProperty("cacheHitRate"); expect(summary).toHaveProperty("strategiesUsed"); expect(summary).toHaveProperty("avgResultsCount"); expect(summary).toHaveProperty("topQueries"); }); it("should filter analytics by date range", () => { const startDate = new Date("2024-01-01"); const endDate = new Date("2024-12-31"); const summary = searchEngine.getAnalyticsSummary(startDate, endDate); expect(summary).toBeDefined(); expect(summary.totalSearches).toBeGreaterThanOrEqual(0); }); it("should return empty summary when no analytics", () => { const futureDate = new Date(Date.now() + 86400000 * 365); // 1 year in future const summary = searchEngine.getAnalyticsSummary(futureDate); expect(summary.totalSearches).toBe(0); expect(summary.avgExecutionTimeMs).toBe(0); expect(summary.cacheHitRate).toBe(0); expect(summary.avgResultsCount).toBe(0); }); it("should filter results by minStrength", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", minStrength: 0.8, }; // Mock full-text search results mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "", matched_terms: "{}" }, { memory_id: "mem-2", rank: 0.8, headline: "", matched_terms: "{}" }, ], }) .mockResolvedValueOnce({ rows: [{ count: "2" }] }) // Total count .mockResolvedValueOnce({ rows: [{ index_used: true }] }) // Index usage .mockResolvedValueOnce({ rows: [ { id: "mem-1", content: "Content 1", created_at: new Date(), salience: 0.8, strength: 0.9, // Above threshold }, ], }) .mockResolvedValueOnce({ rows: [ { id: "mem-2", content: "Content 2", created_at: new Date(), salience: 0.7, strength: 0.7, // Below threshold }, ], }); const results = await searchEngine.search(query); expect(results.length).toBeGreaterThanOrEqual(0); // If results found, verify strength filter if (results.length > 0) { results.forEach((result) => { expect(result.strength).toBeGreaterThanOrEqual(0.8); }); } }); it("should filter results by minSalience", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", minSalience: 0.7, }; // Mock full-text search results mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "", matched_terms: "{}" }, { memory_id: "mem-2", rank: 0.8, headline: "", matched_terms: "{}" }, ], }) .mockResolvedValueOnce({ rows: [{ count: "2" }] }) // Total count .mockResolvedValueOnce({ rows: [{ index_used: true }] }) // Index usage .mockResolvedValueOnce({ rows: [ { id: "mem-1", content: "Content 1", created_at: new Date(), salience: 0.8, // Above threshold strength: 0.9, }, ], }) .mockResolvedValueOnce({ rows: [ { id: "mem-2", content: "Content 2", created_at: new Date(), salience: 0.6, // Below threshold strength: 0.8, }, ], }); const results = await searchEngine.search(query); expect(results.length).toBeGreaterThanOrEqual(0); // If results found, verify salience filter if (results.length > 0) { results.forEach((result) => { expect(result.salience).toBeGreaterThanOrEqual(0.7); }); } }); }); describe("Pagination", () => { it("should apply limit to results", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", limit: 2, }; mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "", matched_terms: [] }, { memory_id: "mem-2", rank: 0.8, headline: "", matched_terms: [] }, { memory_id: "mem-3", rank: 0.7, headline: "", matched_terms: [] }, ], }) .mockResolvedValue({ rows: [ { id: "mem-1", content: "Content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); const results = await searchEngine.search(query); expect(results.length).toBeLessThanOrEqual(2); }); it("should apply offset to results", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", offset: 1, limit: 2, }; mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "", matched_terms: [] }, { memory_id: "mem-2", rank: 0.8, headline: "", matched_terms: [] }, { memory_id: "mem-3", rank: 0.7, headline: "", matched_terms: [] }, ], }) .mockResolvedValue({ rows: [ { id: "mem-2", content: "Content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); const results = await searchEngine.search(query); // With offset 1, should skip first result expect(results.length).toBeLessThanOrEqual(2); }); it("should use default limit when not specified", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [] }) // Search results .mockResolvedValueOnce({ rows: [{ count: "0" }] }) // Total count .mockResolvedValueOnce({ rows: [{ index_used: true }] }); // Index usage const results = await searchEngine.search(query); // Should return results (empty or not) expect(results).toBeDefined(); expect(Array.isArray(results)).toBe(true); }); }); describe("Query Caching", () => { it("should cache search results", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query.mockResolvedValue({ rows: [] }); // First search await searchEngine.search(query); const firstCallCount = mockDb.pool.query.mock.calls.length; // Second search with same query await searchEngine.search(query); const secondCallCount = mockDb.pool.query.mock.calls.length; // Should use cache, no additional DB calls expect(secondCallCount).toBe(firstCallCount); }); it("should return cached results on cache hit", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [{ memory_id: "mem-1", rank: 0.9, headline: "", matched_terms: [] }], }) .mockResolvedValueOnce({ rows: [ { id: "mem-1", content: "Test content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); const results1 = await searchEngine.search(query); const results2 = await searchEngine.search(query); expect(results1).toEqual(results2); }); it("should track cache hits and misses", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query.mockResolvedValue({ rows: [] }); // First search (cache miss) await searchEngine.search(query); // Second search (cache hit) await searchEngine.search(query); const stats = searchEngine.getCacheStats(); expect(stats.hits).toBeGreaterThan(0); expect(stats.misses).toBeGreaterThan(0); }); it("should clear cache when requested", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query.mockResolvedValue({ rows: [] }); // First search await searchEngine.search(query); // Clear cache searchEngine.clearCache(); // Second search should hit DB again await searchEngine.search(query); const stats = searchEngine.getCacheStats(); expect(stats.size).toBe(1); // Only one entry after clear }); it("should exclude pagination from cache key", async () => { mockDb.pool.query.mockResolvedValue({ rows: [] }); const query1: IntegratedSearchQuery = { userId: "user-1", text: "test", limit: 10, offset: 0, }; const query2: IntegratedSearchQuery = { userId: "user-1", text: "test", limit: 5, offset: 5, }; await searchEngine.search(query1); const firstCallCount = mockDb.pool.query.mock.calls.length; await searchEngine.search(query2); const secondCallCount = mockDb.pool.query.mock.calls.length; // Should use cache despite different pagination expect(secondCallCount).toBe(firstCallCount); }); }); describe("Search Analytics", () => { it("should track search analytics when enabled", async () => { const engineWithAnalytics = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { enableAnalytics: true, }); const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query.mockResolvedValue({ rows: [] }); await engineWithAnalytics.search(query); const summary = engineWithAnalytics.getAnalyticsSummary(); expect(summary.totalSearches).toBe(1); expect(summary.avgExecutionTimeMs).toBeGreaterThanOrEqual(0); }); it("should track strategies used", async () => { const engineWithAnalytics = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { enableAnalytics: true, }); const query: IntegratedSearchQuery = { userId: "user-1", text: "test", embedding: new Array(1536).fill(0.1), }; mockDb.pool.query.mockResolvedValue({ rows: [] }); mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([]); await engineWithAnalytics.search(query); const summary = engineWithAnalytics.getAnalyticsSummary(); expect(summary.strategiesUsed["full-text"]).toBeGreaterThan(0); expect(summary.strategiesUsed.vector).toBeGreaterThan(0); }); it("should filter analytics by date range", async () => { const engineWithAnalytics = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { enableAnalytics: true, }); const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query.mockResolvedValue({ rows: [] }); await engineWithAnalytics.search(query); const futureDate = new Date(Date.now() + 86400000); // Tomorrow const summary = engineWithAnalytics.getAnalyticsSummary(futureDate); expect(summary.totalSearches).toBe(0); }); }); describe("Error Handling", () => { it("should handle database connection failure gracefully", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query.mockRejectedValue(new Error("Connection failed")); // Strategy failures are caught and return empty results const results = await searchEngine.search(query); expect(results).toEqual([]); }); it("should handle missing memory data gracefully", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [{ memory_id: "mem-1", rank: 0.9, headline: "", matched_terms: [] }], }) .mockResolvedValueOnce({ rows: [] }); // Memory not found const results = await searchEngine.search(query); expect(results).toHaveLength(0); }); it("should handle strategy failure gracefully", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", embedding: new Array(1536).fill(0.1), }; mockDb.pool.query.mockResolvedValue({ rows: [] }); mockEmbeddingStorage.vectorSimilaritySearch.mockRejectedValue( new Error("Vector search failed") ); // Should not throw, just skip failed strategy const results = await searchEngine.search(query); expect(results).toBeDefined(); }); it("should have timeout configuration", () => { const engineWithTimeout = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { maxExecutionTimeMs: 10, // Very short timeout }); // Verify engine was created with timeout config expect(engineWithTimeout).toBeDefined(); }); }); describe("Configuration", () => { it("should accept custom weights configuration", () => { const engineWithWeights = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { weights: { fullText: 0.5, vector: 0.3, metadata: 0.1, similarity: 0.1, }, }); // Verify engine was created with custom weights expect(engineWithWeights).toBeDefined(); }); it("should accept parallel execution configuration", () => { const engineParallel = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { parallelExecution: true, }); // Verify engine was created with parallel execution enabled expect(engineParallel).toBeDefined(); }); it("should accept sequential execution configuration", () => { const engineSequential = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { parallelExecution: false, }); // Verify engine was created with sequential execution expect(engineSequential).toBeDefined(); }); }); describe("Configuration Options", () => { it("should accept enableCache configuration", () => { const engine = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { enableCache: false, }); expect(engine).toBeDefined(); const stats = engine.getCacheStats(); expect(stats).toBeDefined(); }); it("should accept cacheTTL configuration", () => { const engine = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { cacheTTL: 600, // 10 minutes }); expect(engine).toBeDefined(); }); it("should accept defaultLimit configuration", () => { const engine = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { defaultLimit: 20, }); expect(engine).toBeDefined(); }); it("should accept maxLimit configuration", () => { const engine = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { maxLimit: 500, }); expect(engine).toBeDefined(); }); it("should accept maxExecutionTimeMs configuration", () => { const engine = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { maxExecutionTimeMs: 5000, }); expect(engine).toBeDefined(); }); it("should accept enableAnalytics configuration", () => { const engine = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { enableAnalytics: false, }); expect(engine).toBeDefined(); const summary = engine.getAnalyticsSummary(); expect(summary.totalSearches).toBe(0); }); it("should accept analyticsRetentionDays configuration", () => { const engine = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { analyticsRetentionDays: 90, }); expect(engine).toBeDefined(); }); it("should accept custom strategy weights", () => { const engine = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { weights: { fullText: 0.4, vector: 0.4, metadata: 0.1, similarity: 0.1, }, }); expect(engine).toBeDefined(); }); it("should merge partial config with defaults", () => { const engine = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { enableCache: false, // Other options should use defaults }); expect(engine).toBeDefined(); }); }); describe("Edge Cases", () => { it("should handle query with all optional parameters", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", embedding: new Array(1536).fill(0.1), sector: "semantic", metadata: { keywords: ["test"], tags: ["tag1"], }, similarTo: "mem-123", minStrength: 0.5, minSalience: 0.5, limit: 10, offset: 0, }; mockDb.pool.query .mockResolvedValueOnce({ rows: [] }) // Full-text search .mockResolvedValueOnce({ rows: [{ count: "0" }] }) // Total count .mockResolvedValueOnce({ rows: [{ index_used: true }] }) // Index usage .mockResolvedValueOnce({ rows: [] }) // Metadata filter .mockResolvedValueOnce({ rows: [] }); // Similarity search mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([]); const results = await searchEngine.search(query); expect(results).toBeDefined(); expect(Array.isArray(results)).toBe(true); }); it("should handle query with boundary values", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", minStrength: 0, minSalience: 1, limit: 0, offset: 0, }; mockDb.pool.query .mockResolvedValueOnce({ rows: [] }) .mockResolvedValueOnce({ rows: [{ count: "0" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }); const results = await searchEngine.search(query); expect(results).toBeDefined(); }); it("should handle very long text query", async () => { const longText = "test ".repeat(1000); const query: IntegratedSearchQuery = { userId: "user-1", text: longText, }; mockDb.pool.query .mockResolvedValueOnce({ rows: [] }) .mockResolvedValueOnce({ rows: [{ count: "0" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }); const results = await searchEngine.search(query); expect(results).toBeDefined(); }); it("should handle empty userId", async () => { const query: IntegratedSearchQuery = { userId: "", text: "test", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [] }) .mockResolvedValueOnce({ rows: [{ count: "0" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }); const results = await searchEngine.search(query); expect(results).toBeDefined(); }); it("should handle special characters in text query", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test & query | with (special) <characters>", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [] }) .mockResolvedValueOnce({ rows: [{ count: "0" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }); const results = await searchEngine.search(query); expect(results).toBeDefined(); }); }); describe("Analytics Tracking", () => { it("should track strategy usage in analytics", () => { const engineWithAnalytics = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { enableAnalytics: true, }); const summary = engineWithAnalytics.getAnalyticsSummary(); expect(summary.strategiesUsed).toHaveProperty("full-text"); expect(summary.strategiesUsed).toHaveProperty("vector"); expect(summary.strategiesUsed).toHaveProperty("metadata"); expect(summary.strategiesUsed).toHaveProperty("similarity"); expect(summary.strategiesUsed).toHaveProperty("hybrid"); }); it("should track top queries", () => { const engineWithAnalytics = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { enableAnalytics: true, }); const summary = engineWithAnalytics.getAnalyticsSummary(); expect(Array.isArray(summary.topQueries)).toBe(true); }); it("should calculate average execution time", () => { const engineWithAnalytics = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { enableAnalytics: true, }); const summary = engineWithAnalytics.getAnalyticsSummary(); expect(summary.avgExecutionTimeMs).toBeGreaterThanOrEqual(0); }); it("should calculate cache hit rate", () => { const engineWithAnalytics = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { enableAnalytics: true, }); const summary = engineWithAnalytics.getAnalyticsSummary(); expect(summary.cacheHitRate).toBeGreaterThanOrEqual(0); expect(summary.cacheHitRate).toBeLessThanOrEqual(1); }); it("should calculate average results count", () => { const engineWithAnalytics = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { enableAnalytics: true, }); const summary = engineWithAnalytics.getAnalyticsSummary(); expect(summary.avgResultsCount).toBeGreaterThanOrEqual(0); }); }); describe("Multi-Strategy Execution", () => { it("should execute full-text search strategy", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test query", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "Test <b>query</b>", matched_terms: ["test", "query"], }, ], }) .mockResolvedValueOnce({ rows: [{ count: "1" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }) .mockResolvedValueOnce({ rows: [ { content: "Test query content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); const results = await searchEngine.search(query); // Should execute without errors expect(results).toBeDefined(); expect(Array.isArray(results)).toBe(true); }); it("should execute vector search strategy", async () => { const query: IntegratedSearchQuery = { userId: "user-1", embedding: new Array(1536).fill(0.1), }; mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([ { memoryId: "mem-1", similarity: 0.85, sector: "semantic", }, ]); // Mock userId filter query on the client mockClient.query.mockResolvedValueOnce({ rows: [{ id: "mem-1" }], // userId filter result }); // Mock fetchMemoryData query on pool mockDb.pool.query.mockResolvedValueOnce({ rows: [ { content: "Vector search content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], // fetchMemoryData result }); const results = await searchEngine.search(query); expect(results.length).toBeGreaterThan(0); expect(results[0].memoryId).toBe("mem-1"); expect(mockEmbeddingStorage.vectorSimilaritySearch).toHaveBeenCalled(); }); it("should execute metadata filter strategy", async () => { const query: IntegratedSearchQuery = { userId: "user-1", metadata: { keywords: ["test"], tags: ["tag1"], }, }; mockDb.pool.query .mockResolvedValueOnce({ rows: [{ id: "mem-1" }, { id: "mem-2" }], }) .mockResolvedValueOnce({ rows: [ { content: "Metadata content 1", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }) .mockResolvedValueOnce({ rows: [ { content: "Metadata content 2", created_at: new Date(), salience: 0.7, strength: 0.8, }, ], }); const results = await searchEngine.search(query); // Should execute without errors expect(results).toBeDefined(); expect(Array.isArray(results)).toBe(true); }); it("should skip metadata filter strategy when metadata is not provided", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test query", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "Test", matched_terms: '["test"]', }, ], }) .mockResolvedValueOnce({ rows: [{ count: "1" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }) .mockResolvedValueOnce({ rows: [ { content: "Text search content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); const results = await searchEngine.search(query); // Should execute without metadata filter expect(results).toBeDefined(); expect(Array.isArray(results)).toBe(true); }); it("should execute similarity search strategy", async () => { const query: IntegratedSearchQuery = { userId: "user-1", similarTo: "mem-source", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [ { id: "mem-source", content: "Source content", created_at: new Date(), primary_sector: "semantic", keywords: '["test"]', tags: '["tag1"]', category: "test", }, ], }) .mockResolvedValueOnce({ rows: [ { id: "mem-1", content: "Similar content", created_at: new Date(), keywords: '["test"]', tags: '["tag1"]', category: "test", }, ], }) .mockResolvedValueOnce({ rows: [ { id: "mem-1", content: "Similar content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); const results = await searchEngine.search(query); expect(results).toBeDefined(); }); it("should skip similarity search strategy when similarTo is not provided", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test query", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "Test", matched_terms: '["test"]', }, ], }) .mockResolvedValueOnce({ rows: [{ count: "1" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }) .mockResolvedValueOnce({ rows: [ { content: "Text search content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); const results = await searchEngine.search(query); // Should execute without similarity search expect(results).toBeDefined(); expect(Array.isArray(results)).toBe(true); }); it("should combine results from multiple strategies", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", embedding: new Array(1536).fill(0.1), }; mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "Test", matched_terms: ["test"], }, ], }) .mockResolvedValueOnce({ rows: [{ count: "1" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }) .mockResolvedValueOnce({ rows: [ { content: "Combined content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([ { memoryId: "mem-1", similarity: 0.85, sector: "semantic", }, ]); const results = await searchEngine.search(query); // Should execute both strategies without errors expect(results).toBeDefined(); expect(Array.isArray(results)).toBe(true); expect(mockEmbeddingStorage.vectorSimilaritySearch).toHaveBeenCalled(); }); it("should execute strategies in parallel when configured", async () => { const engineParallel = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { parallelExecution: true, }); const query: IntegratedSearchQuery = { userId: "user-1", text: "test", embedding: new Array(1536).fill(0.1), }; // Mock client queries - use mockImplementation to handle different query types mockClient.query.mockImplementation((sql: string) => { if (sql.includes("ts_rank")) { // Full-text search query return Promise.resolve({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "Test", matched_terms: '["test"]', }, ], }); } else if (sql.includes("COUNT(*)")) { // Count query return Promise.resolve({ rows: [{ count: "1" }] }); } else if (sql.includes("pg_indexes")) { // Index check query return Promise.resolve({ rows: [{ index_used: true }] }); } else if (sql.includes("user_id")) { // userId filter for vector search return Promise.resolve({ rows: [{ id: "mem-1" }] }); } return Promise.resolve({ rows: [] }); }); // Mock fetchMemoryData on pool mockDb.pool.query.mockResolvedValue({ rows: [ { content: "Parallel content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([ { memoryId: "mem-1", similarity: 0.85, sector: "semantic", }, ]); const results = await engineParallel.search(query); expect(results).toBeDefined(); expect(results.length).toBeGreaterThan(0); }); it("should execute strategies sequentially when configured", async () => { const engineSequential = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { parallelExecution: false, }); const query: IntegratedSearchQuery = { userId: "user-1", text: "test", embedding: new Array(1536).fill(0.1), }; // Mock full-text search queries on client (uses getConnection) mockClient.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "Test", matched_terms: '["test"]', }, ], }) .mockResolvedValueOnce({ rows: [{ count: "1" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }) .mockResolvedValueOnce({ rows: [{ id: "mem-1" }], // userId filter for vector search }); // Mock fetchMemoryData on pool mockDb.pool.query.mockResolvedValueOnce({ rows: [ { content: "Sequential content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([ { memoryId: "mem-1", similarity: 0.85, sector: "semantic", }, ]); const results = await engineSequential.search(query); expect(results).toBeDefined(); expect(results.length).toBeGreaterThan(0); }); it("should have timeout configuration for parallel execution", () => { const engineWithTimeout = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { parallelExecution: true, maxExecutionTimeMs: 10000, }); // Verify engine was created with timeout config expect(engineWithTimeout).toBeDefined(); }); it("should have timeout configuration for sequential execution", () => { const engineWithTimeout = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { parallelExecution: false, maxExecutionTimeMs: 10000, }); // Verify engine was created with timeout config expect(engineWithTimeout).toBeDefined(); }); }); describe("Composite Scoring", () => { it("should calculate composite score from multiple strategies", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", embedding: new Array(1536).fill(0.1), }; // Mock client queries - use mockImplementation to handle different query types mockClient.query.mockImplementation((sql: string) => { if (sql.includes("ts_rank")) { // Full-text search query return Promise.resolve({ rows: [ { memory_id: "mem-1", rank: 0.8, headline: "Test", matched_terms: ["test"], }, ], }); } else if (sql.includes("COUNT(*)")) { // Count query return Promise.resolve({ rows: [{ count: "1" }] }); } else if (sql.includes("pg_indexes")) { // Index check query return Promise.resolve({ rows: [{ index_used: true }] }); } else if (sql.includes("user_id")) { // userId filter for vector search return Promise.resolve({ rows: [{ id: "mem-1" }] }); } return Promise.resolve({ rows: [] }); }); // Mock fetchMemoryData on pool mockDb.pool.query.mockResolvedValue({ rows: [ { content: "Content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([ { memoryId: "mem-1", similarity: 0.9, sector: "semantic", }, ]); const results = await searchEngine.search(query); expect(results.length).toBeGreaterThan(0); expect(results[0].compositeScore).toBeGreaterThan(0); expect(results[0].compositeScore).toBeLessThanOrEqual(1); }); it("should handle composite scoring with empty results", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [] }) // No results .mockResolvedValueOnce({ rows: [{ count: "0" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }); const results = await searchEngine.search(query); expect(results).toEqual([]); }); it("should generate explanation for search results", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "Test", matched_terms: ["test"], }, ], }) .mockResolvedValueOnce({ rows: [{ count: "1" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }) .mockResolvedValueOnce({ rows: [ { content: "Content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); const results = await searchEngine.search(query); if (results.length > 0) { expect(results[0].explanation).toBeDefined(); expect(typeof results[0].explanation).toBe("string"); } }); it("should support custom strategy weights configuration", async () => { const engineWithWeights = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { weights: { fullText: 0.8, vector: 0.1, metadata: 0.05, similarity: 0.05, }, }); const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [] }) .mockResolvedValueOnce({ rows: [{ count: "0" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }); const results = await engineWithWeights.search(query); // Should handle empty results gracefully expect(results).toBeDefined(); expect(Array.isArray(results)).toBe(true); }); }); describe("Result Filtering", () => { it("should handle missing memory data gracefully", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "Test", matched_terms: ["test"], }, ], }) .mockResolvedValueOnce({ rows: [{ count: "1" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }) .mockResolvedValueOnce({ rows: [] }); // Memory not found const results = await searchEngine.search(query); // Should return empty array when memory not found expect(results).toEqual([]); }); it("should handle database not connected error gracefully", async () => { const engineWithoutDb = new MemorySearchEngine( { pool: null } as any, mockEmbeddingStorage, undefined ); const query: IntegratedSearchQuery = { userId: "user-1", text: "test", }; // Strategy failures are caught and return empty results const results = await engineWithoutDb.search(query); expect(results).toEqual([]); }); it("should apply minStrength filter during result combination", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", minStrength: 0.85, }; mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "Test", matched_terms: ["test"], }, ], }) .mockResolvedValueOnce({ rows: [{ count: "1" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }) .mockResolvedValueOnce({ rows: [ { content: "Content 1", created_at: new Date(), salience: 0.8, strength: 0.8, // Below threshold }, ], }); const results = await searchEngine.search(query); // Should filter out results below minStrength expect(results).toEqual([]); }); it("should apply minSalience filter during result combination", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test", minSalience: 0.85, }; mockDb.pool.query .mockResolvedValueOnce({ rows: [ { memory_id: "mem-1", rank: 0.9, headline: "Test", matched_terms: ["test"], }, ], }) .mockResolvedValueOnce({ rows: [{ count: "1" }] }) .mockResolvedValueOnce({ rows: [{ index_used: true }] }) .mockResolvedValueOnce({ rows: [ { content: "Content 1", created_at: new Date(), salience: 0.8, // Below threshold strength: 0.9, }, ], }); const results = await searchEngine.search(query); // Should filter out results below minSalience expect(results).toEqual([]); }); }); describe("Similarity Search Strategy", () => { it("should execute similarity search when similarTo is provided", async () => { const query: IntegratedSearchQuery = { userId: "user-1", similarTo: "mem-source", limit: 5, }; // Mock the similarity finder to return results const mockSimilarityFinder = { findSimilar: vi.fn().mockResolvedValue([ { memoryId: "mem-1", similarity: { overall: 0.85, factors: { keyword: 0.8, tag: 0.9, content: 0.85, category: 1.0, temporal: 0.7, }, }, explanation: "High similarity due to keyword and tag overlap", }, { memoryId: "mem-2", similarity: { overall: 0.72, factors: { keyword: 0.7, tag: 0.75, content: 0.7, category: 1.0, temporal: 0.65, }, }, explanation: "Moderate similarity with shared keywords", }, ]), }; // Replace the similarity finder in the search engine (searchEngine as any).similarityFinder = mockSimilarityFinder; // Mock database query for memory data mockDb.pool.query.mockResolvedValue({ rows: [ { content: "Similar content 1", created_at: new Date(), salience: 0.8, strength: 0.9, }, { content: "Similar content 2", created_at: new Date(), salience: 0.75, strength: 0.85, }, ], }); const results = await searchEngine.search(query); expect(mockSimilarityFinder.findSimilar).toHaveBeenCalledWith("mem-source", { limit: 5, minSimilarity: 0.0, includeExplanation: true, }); expect(results.length).toBe(2); expect(results[0].memoryId).toBe("mem-1"); expect(results[0].compositeScore).toBeGreaterThan(0); expect(results[0].strategyScores.similarity).toBeDefined(); expect(results[0].explanation).toContain("Similar memory"); }); it("should skip similarity search when similarTo is not provided", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test query", }; const mockSimilarityFinder = { findSimilar: vi.fn(), }; (searchEngine as any).similarityFinder = mockSimilarityFinder; // Mock full-text search mockDb.pool.query.mockResolvedValue({ rows: [ { id: "mem-1", content: "Test content", ts_rank: 0.8, created_at: new Date(), salience: 0.7, strength: 0.8, }, ], }); await searchEngine.search(query); // Similarity finder should not be called expect(mockSimilarityFinder.findSimilar).not.toHaveBeenCalled(); }); it("should include similarity factors in result metadata", async () => { const query: IntegratedSearchQuery = { userId: "user-1", similarTo: "mem-source", }; const mockSimilarityFinder = { findSimilar: vi.fn().mockResolvedValue([ { memoryId: "mem-1", similarity: { overall: 0.88, factors: { keyword: 0.85, tag: 0.9, content: 0.87, category: 1.0, temporal: 0.8, }, }, explanation: "Very high similarity across all factors", }, ]), }; (searchEngine as any).similarityFinder = mockSimilarityFinder; mockDb.pool.query.mockResolvedValue({ rows: [ { content: "Similar content", created_at: new Date(), salience: 0.85, strength: 0.9, }, ], }); const results = await searchEngine.search(query); expect(results.length).toBe(1); expect(results[0].strategyScores.similarity).toBeDefined(); expect(results[0].explanation).toContain("Similar memory"); }); it("should handle empty similarity results", async () => { const query: IntegratedSearchQuery = { userId: "user-1", similarTo: "mem-source", }; const mockSimilarityFinder = { findSimilar: vi.fn().mockResolvedValue([]), }; (searchEngine as any).similarityFinder = mockSimilarityFinder; const results = await searchEngine.search(query); expect(results).toEqual([]); }); it("should combine similarity search with other strategies", async () => { const query: IntegratedSearchQuery = { userId: "user-1", text: "test query", similarTo: "mem-source", }; const mockSimilarityFinder = { findSimilar: vi.fn().mockResolvedValue([ { memoryId: "mem-1", similarity: { overall: 0.85, factors: { keyword: 0.8, tag: 0.9, content: 0.85, category: 1.0, temporal: 0.7, }, }, explanation: "High similarity", }, ]), }; (searchEngine as any).similarityFinder = mockSimilarityFinder; // Mock full-text search returning same memory // First call: full-text search // Second call: memory data retrieval mockDb.pool.query .mockResolvedValueOnce({ rows: [ { id: "mem-1", content: "Test content", ts_rank: 0.75, created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }) .mockResolvedValueOnce({ rows: [ { content: "Test content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); const results = await searchEngine.search(query); expect(results.length).toBe(1); expect(results[0].memoryId).toBe("mem-1"); // Should have combined score from both strategies expect(results[0].strategyScores.similarity).toBeDefined(); expect(results[0].compositeScore).toBeGreaterThan(0); }); }); describe("Explanation Generation Coverage", () => { it("should cover explanation generation code paths", async () => { // This test ensures the explanation generation code is executed // by testing with vector similarity which has simpler mocking const query: IntegratedSearchQuery = { userId: "user-1", embedding: new Array(1536).fill(0.1), }; mockEmbeddingStorage.vectorSimilaritySearch.mockResolvedValue([ { memoryId: "mem-1", similarity: 0.85, sector: "semantic", }, ]); // Mock userId filter on client mockClient.query.mockResolvedValueOnce({ rows: [{ id: "mem-1" }], }); // Mock fetchMemoryData on pool mockDb.pool.query.mockResolvedValueOnce({ rows: [ { content: "Vector content", created_at: new Date(), salience: 0.8, strength: 0.9, }, ], }); const results = await searchEngine.search(query); expect(results.length).toBeGreaterThan(0); expect(results[0].explanation).toContain("Vector similarity"); }); it("should cover composite score calculation with all strategy types", () => { // Test that composite score calculation handles all strategy types const engine = new MemorySearchEngine(mockDb, mockEmbeddingStorage, undefined, { weights: { fullText: 0.4, vector: 0.3, metadata: 0.2, similarity: 0.1, }, }); expect(engine).toBeDefined(); }); }); });

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