/** * HybridSearch tests — full-file port from * hive-mind/packages/core/src/mind/search.test.ts. * * Memory Sync Repair Step 2. Source: hive-mind file at HEAD c363257. * * Filename suffix `-hive-mind` keeps this distinct from waggle-os's own * `search.test.ts` (which focuses on scoring profiles, temporal decay, * NaN/Infinity guards). Hive-mind's file focuses on the search-pipeline * smoke contract: keywordSearch + scoping + stop-word handling, vector * indexing + retrieval, indexFramesBatch atomicity, search() fusion + * scoping end-to-end. * * Adapted imports: `./db.js`, `./frames.js`, `./search.js`, * `./embedding-provider.js` → `../../src/mind/...`. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { rmSync, existsSync } from 'node:fs'; import { MindDB } from '../../src/mind/db.js'; import { FrameStore } from '../../src/mind/frames.js'; import { HybridSearch } from '../../src/mind/search.js'; import { createEmbeddingProvider, type EmbeddingProviderInstance } from '../../src/mind/embedding-provider.js'; describe('HybridSearch (hive-mind port)', () => { let dbPath: string; let db: MindDB; let frames: FrameStore; let embedder: EmbeddingProviderInstance; let search: HybridSearch; beforeEach(async () => { dbPath = join(tmpdir(), `waggle-mind-search-test-${Date.now()}-${Math.random()}.mind`); db = new MindDB(dbPath); db.getDatabase() .prepare( "INSERT INTO sessions (gop_id, status, started_at) VALUES ('gop-a', 'active', datetime('now'))", ) .run(); db.getDatabase() .prepare( "INSERT INTO sessions (gop_id, status, started_at) VALUES ('gop-b', 'active', datetime('now'))", ) .run(); frames = new FrameStore(db); embedder = await createEmbeddingProvider({ provider: 'mock' }); search = new HybridSearch(db, embedder); }); afterEach(() => { db.close(); if (existsSync(dbPath)) rmSync(dbPath); for (const suffix of ['-shm', '-wal']) { if (existsSync(dbPath + suffix)) rmSync(dbPath + suffix); } }); it('keywordSearch finds frames containing the query terms', async () => { const a = frames.createIFrame('gop-a', 'user prefers TypeScript over JavaScript'); frames.createIFrame('gop-a', 'user likes weekend hiking trips in the Alps'); const ids = await search.keywordSearch('TypeScript preferences', 10); expect(ids).toContain(a.id); }); it('keywordSearch scopes results to gopId when provided', async () => { const inA = frames.createIFrame('gop-a', 'deployment blueprint alpha'); const inB = frames.createIFrame('gop-b', 'deployment blueprint bravo'); const scopedToA = await search.keywordSearch('deployment blueprint', 10, 'gop-a'); expect(scopedToA).toContain(inA.id); expect(scopedToA).not.toContain(inB.id); const scopedToB = await search.keywordSearch('deployment blueprint', 10, 'gop-b'); expect(scopedToB).toContain(inB.id); expect(scopedToB).not.toContain(inA.id); }); it('keywordSearch returns [] for queries containing only stop words', async () => { frames.createIFrame('gop-a', 'content that will not match a stop-word query'); const ids = await search.keywordSearch('the a an of to in for on with', 10); expect(ids).toEqual([]); }); it('indexFrame inserts into memory_frames_vec and vectorSearch retrieves it', async () => { const frame = frames.createIFrame('gop-a', 'quantum annealing implementation notes'); await search.indexFrame(frame.id, frame.content); const count = db .getDatabase() .prepare('SELECT COUNT(*) as n FROM memory_frames_vec') .get() as { n: number }; expect(count.n).toBe(1); const ids = await search.vectorSearch('quantum annealing implementation notes', 10); expect(ids).toContain(frame.id); }); it('indexFramesBatch inserts multiple rows atomically', async () => { const a = frames.createIFrame('gop-a', 'alpha content'); const b = frames.createIFrame('gop-a', 'bravo content'); const c = frames.createIFrame('gop-a', 'charlie content'); await search.indexFramesBatch([ { id: a.id, content: a.content }, { id: b.id, content: b.content }, { id: c.id, content: c.content }, ]); const count = db .getDatabase() .prepare('SELECT COUNT(*) as n FROM memory_frames_vec') .get() as { n: number }; expect(count.n).toBe(3); }); it('search() fuses keyword + vector ranks and returns sorted SearchResults', async () => { const a = frames.createIFrame('gop-a', 'roadmap for Q2 launch', 'important'); const b = frames.createIFrame('gop-a', 'Q2 launch success criteria', 'critical'); const c = frames.createIFrame('gop-a', 'unrelated conversation about coffee'); await search.indexFramesBatch([ { id: a.id, content: a.content }, { id: b.id, content: b.content }, { id: c.id, content: c.content }, ]); const results = await search.search('Q2 launch', { limit: 3 }); expect(results.length).toBeGreaterThan(0); for (const r of results) { expect(r.rrfScore).toBeGreaterThan(0); expect(r.relevanceScore).toBeGreaterThan(0); expect(r.finalScore).toBe(r.rrfScore * r.relevanceScore); } for (let i = 1; i < results.length; i++) { expect(results[i - 1].finalScore).toBeGreaterThanOrEqual(results[i].finalScore); } const topIds = results.map((r) => r.frame.id); expect(topIds).toContain(a.id); expect(topIds).toContain(b.id); }); it('search() honours gopId scoping end-to-end', async () => { const inA = frames.createIFrame('gop-a', 'report for project apollo alpha'); const inB = frames.createIFrame('gop-b', 'report for project apollo bravo'); await search.indexFramesBatch([ { id: inA.id, content: inA.content }, { id: inB.id, content: inB.content }, ]); const results = await search.search('report apollo', { gopId: 'gop-a', limit: 10 }); const ids = results.map((r) => r.frame.id); expect(ids).toContain(inA.id); expect(ids).not.toContain(inB.id); }); });