Files
waggle-os/packages/hive-mind-core/tests/mind/reconcile.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

217 lines
7.8 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { MindDB } from '../../src/mind/db.js';
import { FrameStore } from '../../src/mind/frames.js';
import { SessionStore } from '../../src/mind/sessions.js';
import { HybridSearch } from '../../src/mind/search.js';
import { reconcileIndexes, reconcileFtsIndex, reconcileVecIndex } from '../../src/mind/reconcile.js';
import { MockEmbedder } from './helpers/mock-embedder.js';
describe('Index Reconciliation', () => {
let db: MindDB;
let frames: FrameStore;
let sessions: SessionStore;
let embedder: MockEmbedder;
beforeEach(() => {
db = new MindDB(':memory:');
frames = new FrameStore(db);
sessions = new SessionStore(db);
embedder = new MockEmbedder();
});
afterEach(() => {
db.close();
});
describe('reconcileFtsIndex', () => {
it('returns 0 when all frames have FTS entries', () => {
const session = sessions.create();
frames.createIFrame(session.gop_id, 'Already indexed frame');
frames.createIFrame(session.gop_id, 'Another indexed frame');
const fixed = reconcileFtsIndex(db);
expect(fixed).toBe(0);
});
it('detects and repairs frames missing from FTS', () => {
const session = sessions.create();
const frame = frames.createIFrame(session.gop_id, 'Test frame for FTS reconciliation');
// Manually delete the FTS entry to simulate a crash between insert and FTS index
const raw = db.getDatabase();
raw.prepare('DELETE FROM memory_frames_fts WHERE rowid = ?').run(frame.id);
// Verify it's missing from FTS
const ftsCheck = raw.prepare(
'SELECT rowid FROM memory_frames_fts WHERE rowid = ?',
).get(frame.id);
expect(ftsCheck).toBeUndefined();
// Reconcile
const fixed = reconcileFtsIndex(db);
expect(fixed).toBe(1);
// Verify it's back in FTS
const ftsAfter = raw.prepare(
'SELECT rowid FROM memory_frames_fts WHERE rowid = ?',
).get(frame.id);
expect(ftsAfter).toBeDefined();
});
it('repairs multiple missing FTS entries', () => {
const session = sessions.create();
const f1 = frames.createIFrame(session.gop_id, 'Frame alpha for reconcile');
const f2 = frames.createPFrame(session.gop_id, 'Frame beta update', f1.id);
const f3 = frames.createIFrame(session.gop_id, 'Frame gamma snapshot');
const raw = db.getDatabase();
// Delete FTS for f1 and f3, leave f2 intact
raw.prepare('DELETE FROM memory_frames_fts WHERE rowid IN (?, ?)').run(f1.id, f3.id);
const fixed = reconcileFtsIndex(db);
expect(fixed).toBe(2);
// All three should now be searchable
for (const fid of [f1.id, f2.id, f3.id]) {
const entry = raw.prepare(
'SELECT rowid FROM memory_frames_fts WHERE rowid = ?',
).get(fid);
expect(entry).toBeDefined();
}
});
it('restored FTS entries are searchable via keyword search', async () => {
const session = sessions.create();
const frame = frames.createIFrame(session.gop_id, 'Quantum computing algorithms');
// Delete FTS entry
const raw = db.getDatabase();
raw.prepare('DELETE FROM memory_frames_fts WHERE rowid = ?').run(frame.id);
// Verify search fails
const search = new HybridSearch(db, embedder);
const beforeResults = await search.keywordSearch('quantum', 10);
expect(beforeResults).toHaveLength(0);
// Reconcile
reconcileFtsIndex(db);
// Verify search works again
const afterResults = await search.keywordSearch('quantum', 10);
expect(afterResults).toHaveLength(1);
expect(afterResults[0]).toBe(frame.id);
});
it('is idempotent — running twice changes nothing', () => {
const session = sessions.create();
const frame = frames.createIFrame(session.gop_id, 'Idempotent test frame');
const raw = db.getDatabase();
raw.prepare('DELETE FROM memory_frames_fts WHERE rowid = ?').run(frame.id);
const first = reconcileFtsIndex(db);
expect(first).toBe(1);
const second = reconcileFtsIndex(db);
expect(second).toBe(0);
});
});
describe('reconcileVecIndex', () => {
it('returns 0 when all frames have vector entries', async () => {
const session = sessions.create();
const frame = frames.createIFrame(session.gop_id, 'Vectorized frame');
const search = new HybridSearch(db, embedder);
await search.indexFrame(frame.id, frame.content);
const fixed = await reconcileVecIndex(db, embedder);
expect(fixed).toBe(0);
});
it('detects and repairs frames missing from vec index', async () => {
const session = sessions.create();
const frame = frames.createIFrame(session.gop_id, 'Missing vector entry');
// Frame exists in memory_frames and FTS, but NOT in vec index
// (simulates crash after FTS index but before vec index)
const fixed = await reconcileVecIndex(db, embedder);
expect(fixed).toBe(1);
// Verify it's in vec now — search should find it
const search = new HybridSearch(db, embedder);
const results = await search.vectorSearch('missing vector', 10);
expect(results).toContain(frame.id);
});
it('repairs multiple missing vec entries', async () => {
const session = sessions.create();
const f1 = frames.createIFrame(session.gop_id, 'Vector test alpha');
const f2 = frames.createIFrame(session.gop_id, 'Vector test beta');
frames.createIFrame(session.gop_id, 'Vector test gamma');
// Index only f1 so f2 and f3 are missing
const search = new HybridSearch(db, embedder);
await search.indexFrame(f1.id, f1.content);
const fixed = await reconcileVecIndex(db, embedder);
expect(fixed).toBe(2);
});
it('is idempotent — running twice changes nothing', async () => {
const session = sessions.create();
frames.createIFrame(session.gop_id, 'Idempotent vec test');
const first = await reconcileVecIndex(db, embedder);
expect(first).toBe(1);
const second = await reconcileVecIndex(db, embedder);
expect(second).toBe(0);
});
});
describe('reconcileIndexes (combined)', () => {
it('repairs both FTS and vec in one call', async () => {
const session = sessions.create();
const frame = frames.createIFrame(session.gop_id, 'Combined reconcile test');
// Delete FTS entry — frame was inserted but FTS index crashed
const raw = db.getDatabase();
raw.prepare('DELETE FROM memory_frames_fts WHERE rowid = ?').run(frame.id);
// Vec is also missing (never indexed)
const result = await reconcileIndexes(db, embedder);
expect(result.ftsFixed).toBe(1);
expect(result.vecFixed).toBe(1);
});
it('works without an embedder (FTS-only reconciliation)', async () => {
const session = sessions.create();
const frame = frames.createIFrame(session.gop_id, 'FTS only reconcile');
const raw = db.getDatabase();
raw.prepare('DELETE FROM memory_frames_fts WHERE rowid = ?').run(frame.id);
const result = await reconcileIndexes(db);
expect(result.ftsFixed).toBe(1);
expect(result.vecFixed).toBe(0);
});
it('returns zeros when everything is consistent', async () => {
const session = sessions.create();
const frame = frames.createIFrame(session.gop_id, 'Consistent frame');
const search = new HybridSearch(db, embedder);
await search.indexFrame(frame.id, frame.content);
const result = await reconcileIndexes(db, embedder);
expect(result.ftsFixed).toBe(0);
expect(result.vecFixed).toBe(0);
});
it('handles empty database gracefully', async () => {
const result = await reconcileIndexes(db, embedder);
expect(result.ftsFixed).toBe(0);
expect(result.vecFixed).toBe(0);
});
});
});