Skip to main content
Glama
similar-memory-finder.test.ts8.43 kB
/** * Similar Memory Finder Tests * * Tests for multi-factor similarity calculation including: * - Keyword overlap (Jaccard similarity) * - Tag similarity * - Content similarity (cosine) * - Category matching * - Temporal proximity * - Composite scoring */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { DatabaseConnectionManager } from "../../../database/connection-manager"; import { SimilarMemoryFinder } from "../../../search/similar-memory-finder"; import { createTestMemory, createTestMemoryMetadata } from "../../utils/test-fixtures"; describe("SimilarMemoryFinder", () => { let finder: SimilarMemoryFinder; let mockDb: DatabaseConnectionManager; let mockClient: { query: ReturnType<typeof vi.fn>; release: ReturnType<typeof vi.fn>; }; beforeEach(() => { mockClient = { query: vi.fn(), release: vi.fn(), }; mockDb = { getConnection: vi.fn().mockResolvedValue(mockClient), releaseConnection: vi.fn(), } as unknown as DatabaseConnectionManager; finder = new SimilarMemoryFinder(mockDb, { enableCache: false }); }); afterEach(() => { vi.clearAllMocks(); }); describe("calculateKeywordSimilarity", () => { it("should return 1.0 for identical keywords", async () => { const memory1 = createTestMemory({ id: "mem1", metadata: createTestMemoryMetadata({ keywords: ["machine", "learning", "algorithms"], }), }); const memory2 = createTestMemory({ id: "mem2", metadata: createTestMemoryMetadata({ keywords: ["machine", "learning", "algorithms"], }), }); mockClient.query.mockResolvedValueOnce({ rows: [ { keywords1: memory1.metadata.keywords, keywords2: memory2.metadata.keywords, }, ], }); const similarity = await finder.calculateKeywordSimilarity(memory1.id, memory2.id); expect(similarity).toBe(1.0); }); it("should return 0.0 for no keyword overlap", async () => { mockClient.query.mockResolvedValueOnce({ rows: [ { keywords1: ["machine", "learning"], keywords2: ["cooking", "recipes"], }, ], }); const similarity = await finder.calculateKeywordSimilarity("mem1", "mem2"); expect(similarity).toBe(0.0); }); it("should calculate correct Jaccard similarity for partial overlap", async () => { mockClient.query.mockResolvedValueOnce({ rows: [ { keywords1: ["machine", "learning", "algorithms"], keywords2: ["machine", "learning", "data"], }, ], }); const similarity = await finder.calculateKeywordSimilarity("mem1", "mem2"); // Jaccard: intersection(2) / union(4) = 0.5 expect(similarity).toBe(0.5); }); it("should handle empty keywords gracefully", async () => { mockClient.query.mockResolvedValueOnce({ rows: [{ keywords1: [], keywords2: [] }], }); const similarity = await finder.calculateKeywordSimilarity("mem1", "mem2"); expect(similarity).toBe(0.0); }); }); describe("calculateTagSimilarity", () => { it("should return 1.0 for identical tags", async () => { mockClient.query.mockResolvedValueOnce({ rows: [ { tags1: ["important", "work"], tags2: ["important", "work"], }, ], }); const similarity = await finder.calculateTagSimilarity("mem1", "mem2"); expect(similarity).toBe(1.0); }); it("should return 0.0 for no tag overlap", async () => { mockClient.query.mockResolvedValueOnce({ rows: [ { tags1: ["important", "work"], tags2: ["personal", "hobby"], }, ], }); const similarity = await finder.calculateTagSimilarity("mem1", "mem2"); expect(similarity).toBe(0.0); }); }); describe("calculateContentSimilarity", () => { it("should return 1.0 for identical embeddings", async () => { const embedding = [0.5, 0.5, 0.5, 0.5]; mockClient.query.mockResolvedValueOnce({ rows: [ { embedding1: embedding, embedding2: embedding, }, ], }); const similarity = await finder.calculateContentSimilarity("mem1", "mem2"); expect(similarity).toBe(1.0); }); it("should return 0.0 for orthogonal embeddings", async () => { mockClient.query.mockResolvedValueOnce({ rows: [ { embedding1: [1, 0, 0, 0], embedding2: [0, 1, 0, 0], }, ], }); const similarity = await finder.calculateContentSimilarity("mem1", "mem2"); expect(similarity).toBe(0.0); }); it("should handle missing embeddings gracefully", async () => { mockClient.query.mockResolvedValueOnce({ rows: [{ embedding1: null, embedding2: null }], }); const similarity = await finder.calculateContentSimilarity("mem1", "mem2"); expect(similarity).toBe(0.0); }); }); describe("calculateCategoryMatch", () => { it("should return 1.0 for matching categories", async () => { mockClient.query.mockResolvedValueOnce({ rows: [{ category1: "work", category2: "work" }], }); const match = await finder.calculateCategoryMatch("mem1", "mem2"); expect(match).toBe(1.0); }); it("should return 0.0 for different categories", async () => { mockClient.query.mockResolvedValueOnce({ rows: [{ category1: "work", category2: "personal" }], }); const match = await finder.calculateCategoryMatch("mem1", "mem2"); expect(match).toBe(0.0); }); }); describe("calculateTemporalProximity", () => { it("should return 1.0 for identical timestamps", async () => { const timestamp = new Date(); mockClient.query.mockResolvedValueOnce({ rows: [{ created_at1: timestamp, created_at2: timestamp }], }); const proximity = await finder.calculateTemporalProximity("mem1", "mem2"); expect(proximity).toBe(1.0); }); it("should return lower proximity for distant timestamps", async () => { const now = new Date(); const oneYearAgo = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); mockClient.query.mockResolvedValueOnce({ rows: [{ created_at1: now, created_at2: oneYearAgo }], }); const proximity = await finder.calculateTemporalProximity("mem1", "mem2"); expect(proximity).toBeLessThan(0.5); }); }); describe("Configuration", () => { it("should create finder with default config", () => { const defaultFinder = new SimilarMemoryFinder(mockDb); expect(defaultFinder).toBeDefined(); }); it("should create finder with custom config", () => { const customFinder = new SimilarMemoryFinder(mockDb, { enableCache: true, cacheTTL: 60000, }); expect(customFinder).toBeDefined(); }); }); describe("Error Handling", () => { it("should handle database errors in keyword similarity", async () => { mockClient.query.mockRejectedValueOnce(new Error("Database error")); await expect(finder.calculateKeywordSimilarity("mem1", "mem2")).rejects.toThrow(); }); it("should handle database errors in tag similarity", async () => { mockClient.query.mockRejectedValueOnce(new Error("Database error")); await expect(finder.calculateTagSimilarity("mem1", "mem2")).rejects.toThrow(); }); it("should handle database errors in content similarity", async () => { mockClient.query.mockRejectedValueOnce(new Error("Database error")); await expect(finder.calculateContentSimilarity("mem1", "mem2")).rejects.toThrow(); }); it("should handle database errors in category match", async () => { mockClient.query.mockRejectedValueOnce(new Error("Database error")); await expect(finder.calculateCategoryMatch("mem1", "mem2")).rejects.toThrow(); }); it("should handle database errors in temporal proximity", async () => { mockClient.query.mockRejectedValueOnce(new Error("Database error")); await expect(finder.calculateTemporalProximity("mem1", "mem2")).rejects.toThrow(); }); }); });

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