Skip to main content
Glama
search.test.ts12.1 kB
/** * Tests for searchMemory function helper logic * Tests the pure function patterns used in search.ts */ import { describe, expect, test } from "bun:test"; describe("searchMemory", () => { describe("filter building", () => { interface SearchFilters { type?: string; tags?: string[]; minImportance?: number; relatedFiles?: string[]; } function buildFilters(input: { type?: string; tags?: string[]; minImportance?: number; relatedFiles?: string[]; }): SearchFilters { const filters: SearchFilters = {}; if (input.type !== undefined) { filters.type = input.type; } if (input.tags !== undefined && input.tags.length > 0) { filters.tags = input.tags; } if (input.minImportance !== undefined) { filters.minImportance = input.minImportance; } if (input.relatedFiles !== undefined && input.relatedFiles.length > 0) { filters.relatedFiles = input.relatedFiles; } return filters; } test("builds empty filters when no input", () => { const filters = buildFilters({}); expect(filters).toEqual({}); }); test("includes type filter", () => { const filters = buildFilters({ type: "decision" }); expect(filters.type).toBe("decision"); }); test("includes tags filter", () => { const filters = buildFilters({ tags: ["typescript", "api"] }); expect(filters.tags).toEqual(["typescript", "api"]); }); test("excludes empty tags array", () => { const filters = buildFilters({ tags: [] }); expect(filters.tags).toBeUndefined(); }); test("includes minImportance filter", () => { const filters = buildFilters({ minImportance: 0.7 }); expect(filters.minImportance).toBe(0.7); }); test("includes relatedFiles filter", () => { const filters = buildFilters({ relatedFiles: ["src/index.ts"] }); expect(filters.relatedFiles).toEqual(["src/index.ts"]); }); test("excludes empty relatedFiles array", () => { const filters = buildFilters({ relatedFiles: [] }); expect(filters.relatedFiles).toBeUndefined(); }); test("combines multiple filters", () => { const filters = buildFilters({ type: "pattern", tags: ["react"], minImportance: 0.5, relatedFiles: ["src/App.tsx"], }); expect(filters.type).toBe("pattern"); expect(filters.tags).toEqual(["react"]); expect(filters.minImportance).toBe(0.5); expect(filters.relatedFiles).toEqual(["src/App.tsx"]); }); }); describe("result mapping", () => { interface VectorResult { memoryId: string; score: number; } interface Memory { id: string; title: string; content: string; type: string; } interface SearchResult { memory: Memory; score: number; } function mapResultsToMemories( vectorResults: VectorResult[], memories: Memory[], ): SearchResult[] { const scoreMap = new Map(vectorResults.map((r) => [r.memoryId, r.score])); return memories .map((memory) => ({ memory, score: scoreMap.get(memory.id) ?? 0, })) .sort((a, b) => b.score - a.score); } test("maps scores to memories", () => { const vectorResults: VectorResult[] = [ { memoryId: "mem_1", score: 0.9 }, { memoryId: "mem_2", score: 0.7 }, ]; const memories: Memory[] = [ { id: "mem_1", title: "First", content: "Content 1", type: "note" }, { id: "mem_2", title: "Second", content: "Content 2", type: "note" }, ]; const results = mapResultsToMemories(vectorResults, memories); expect(results[0].memory.id).toBe("mem_1"); expect(results[0].score).toBe(0.9); expect(results[1].memory.id).toBe("mem_2"); expect(results[1].score).toBe(0.7); }); test("sorts by score descending", () => { const vectorResults: VectorResult[] = [ { memoryId: "mem_1", score: 0.5 }, { memoryId: "mem_2", score: 0.9 }, { memoryId: "mem_3", score: 0.7 }, ]; const memories: Memory[] = [ { id: "mem_1", title: "A", content: "A", type: "note" }, { id: "mem_2", title: "B", content: "B", type: "note" }, { id: "mem_3", title: "C", content: "C", type: "note" }, ]; const results = mapResultsToMemories(vectorResults, memories); expect(results[0].score).toBe(0.9); expect(results[1].score).toBe(0.7); expect(results[2].score).toBe(0.5); }); test("handles missing scores with default 0", () => { const vectorResults: VectorResult[] = [{ memoryId: "mem_1", score: 0.8 }]; const memories: Memory[] = [ { id: "mem_1", title: "A", content: "A", type: "note" }, { id: "mem_2", title: "B", content: "B", type: "note" }, ]; const results = mapResultsToMemories(vectorResults, memories); expect(results[0].score).toBe(0.8); expect(results[1].score).toBe(0); }); test("handles empty results", () => { const results = mapResultsToMemories([], []); expect(results).toEqual([]); }); test("handles equal scores", () => { const vectorResults: VectorResult[] = [ { memoryId: "mem_1", score: 0.8 }, { memoryId: "mem_2", score: 0.8 }, ]; const memories: Memory[] = [ { id: "mem_1", title: "A", content: "A", type: "note" }, { id: "mem_2", title: "B", content: "B", type: "note" }, ]; const results = mapResultsToMemories(vectorResults, memories); expect(results.length).toBe(2); expect(results[0].score).toBe(0.8); expect(results[1].score).toBe(0.8); }); }); describe("limit validation", () => { function validateLimit(limit: number | undefined): number { const defaultLimit = 10; const minLimit = 1; const maxLimit = 50; if (limit === undefined) return defaultLimit; return Math.max(minLimit, Math.min(maxLimit, limit)); } test("uses default limit when undefined", () => { expect(validateLimit(undefined)).toBe(10); }); test("accepts valid limit", () => { expect(validateLimit(5)).toBe(5); expect(validateLimit(25)).toBe(25); }); test("clamps to minimum", () => { expect(validateLimit(0)).toBe(1); expect(validateLimit(-5)).toBe(1); }); test("clamps to maximum", () => { expect(validateLimit(100)).toBe(50); expect(validateLimit(51)).toBe(50); }); test("accepts boundary values", () => { expect(validateLimit(1)).toBe(1); expect(validateLimit(50)).toBe(50); }); }); describe("memory ID extraction", () => { function extractMemoryIds( vectorResults: Array<{ memoryId: string }>, ): string[] { return vectorResults.map((r) => r.memoryId); } test("extracts IDs from results", () => { const results = [ { memoryId: "mem_1", score: 0.9 }, { memoryId: "mem_2", score: 0.8 }, ]; expect(extractMemoryIds(results)).toEqual(["mem_1", "mem_2"]); }); test("handles empty results", () => { expect(extractMemoryIds([])).toEqual([]); }); test("preserves order", () => { const results = [ { memoryId: "mem_3" }, { memoryId: "mem_1" }, { memoryId: "mem_2" }, ]; expect(extractMemoryIds(results)).toEqual(["mem_3", "mem_1", "mem_2"]); }); }); describe("score normalization", () => { function normalizeScore(score: number): number { return Math.max(0, Math.min(1, score)); } test("passes through valid scores", () => { expect(normalizeScore(0.5)).toBe(0.5); expect(normalizeScore(0)).toBe(0); expect(normalizeScore(1)).toBe(1); }); test("clamps negative scores to 0", () => { expect(normalizeScore(-0.5)).toBe(0); expect(normalizeScore(-1)).toBe(0); }); test("clamps scores above 1 to 1", () => { expect(normalizeScore(1.5)).toBe(1); expect(normalizeScore(2)).toBe(1); }); }); describe("empty result handling", () => { function handleEmptyResults<T>(results: T[]): T[] { if (results.length === 0) { return []; } return results; } test("returns empty array for empty input", () => { expect(handleEmptyResults([])).toEqual([]); }); test("returns input for non-empty array", () => { const input = [{ id: "1" }, { id: "2" }]; expect(handleEmptyResults(input)).toBe(input); }); }); describe("query preprocessing", () => { function preprocessQuery(query: string): string { return query.trim(); } test("trims whitespace", () => { expect(preprocessQuery(" query ")).toBe("query"); }); test("preserves internal spaces", () => { expect(preprocessQuery("multi word query")).toBe("multi word query"); }); test("handles empty query", () => { expect(preprocessQuery("")).toBe(""); }); test("handles whitespace only", () => { expect(preprocessQuery(" ")).toBe(""); }); }); describe("tag matching", () => { function matchesAnyTag( memoryTags: string[], filterTags: string[], ): boolean { if (filterTags.length === 0) return true; return filterTags.some((tag) => memoryTags.includes(tag)); } test("matches when memory has filter tag", () => { expect(matchesAnyTag(["a", "b", "c"], ["b"])).toBe(true); }); test("matches when any filter tag present", () => { expect(matchesAnyTag(["a", "b"], ["c", "d", "a"])).toBe(true); }); test("does not match when no tags overlap", () => { expect(matchesAnyTag(["a", "b"], ["c", "d"])).toBe(false); }); test("matches all when filter is empty", () => { expect(matchesAnyTag(["a", "b"], [])).toBe(true); }); test("does not match when memory has no tags", () => { expect(matchesAnyTag([], ["a"])).toBe(false); }); }); describe("importance filtering", () => { function meetsImportanceThreshold( importance: number, minImportance?: number, ): boolean { if (minImportance === undefined) return true; return importance >= minImportance; } test("passes when no threshold", () => { expect(meetsImportanceThreshold(0.3, undefined)).toBe(true); }); test("passes when above threshold", () => { expect(meetsImportanceThreshold(0.8, 0.5)).toBe(true); }); test("passes when equal to threshold", () => { expect(meetsImportanceThreshold(0.5, 0.5)).toBe(true); }); test("fails when below threshold", () => { expect(meetsImportanceThreshold(0.3, 0.5)).toBe(false); }); test("handles zero threshold", () => { expect(meetsImportanceThreshold(0, 0)).toBe(true); }); test("handles max threshold", () => { expect(meetsImportanceThreshold(1, 1)).toBe(true); expect(meetsImportanceThreshold(0.99, 1)).toBe(false); }); }); describe("file matching", () => { function matchesAnyFile( memoryFiles: string[], filterFiles: string[], ): boolean { if (filterFiles.length === 0) return true; return filterFiles.some((file) => memoryFiles.includes(file)); } test("matches when memory has filter file", () => { expect(matchesAnyFile(["src/a.ts", "src/b.ts"], ["src/a.ts"])).toBe(true); }); test("matches when any filter file present", () => { expect( matchesAnyFile(["src/a.ts"], ["src/x.ts", "src/a.ts", "src/y.ts"]), ).toBe(true); }); test("does not match when no files overlap", () => { expect(matchesAnyFile(["src/a.ts"], ["src/b.ts"])).toBe(false); }); test("matches all when filter is empty", () => { expect(matchesAnyFile(["src/a.ts"], [])).toBe(true); }); test("does not match when memory has no files", () => { expect(matchesAnyFile([], ["src/a.ts"])).toBe(false); }); }); });

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/docleaai/doclea-mcp'

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