108 lines
4.3 KiB
TypeScript
108 lines
4.3 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 type { Reranker } from '../../src/mind/inprocess-reranker.js';
|
|
import { computeRelevance, SCORING_PROFILES } from '../../src/mind/scoring.js';
|
|
import { MockEmbedder } from './helpers/mock-embedder.js';
|
|
|
|
/**
|
|
* W4.2 — cross-encoder reranker integration (reverse-ported from the OSS
|
|
* benchmark-proven stack; W4-PRODUCTION-PORT-PLAN-2026-06-11.md component #4)
|
|
* + scoring bug #3 (temporal decay anchors on created_at, not last_accessed).
|
|
*
|
|
* Tests use a mock Reranker — the real ONNX model (~22MB download) is
|
|
* exercised only behind the WAGGLE_RERANKER=1 opt-in at runtime.
|
|
*/
|
|
|
|
describe('HybridSearch — reranker option (W4.2)', () => {
|
|
let db: MindDB;
|
|
let frames: FrameStore;
|
|
let sessions: SessionStore;
|
|
let search: HybridSearch;
|
|
let gopId: string;
|
|
|
|
beforeEach(() => {
|
|
db = new MindDB(':memory:');
|
|
frames = new FrameStore(db);
|
|
sessions = new SessionStore(db);
|
|
search = new HybridSearch(db, new MockEmbedder());
|
|
gopId = sessions.create().gop_id;
|
|
});
|
|
|
|
afterEach(() => {
|
|
db.close();
|
|
});
|
|
|
|
/** Reranker that scores by presence of a marker substring. */
|
|
function markerReranker(marker: string): Reranker {
|
|
const scoreOf = (doc: string): number => (doc.includes(marker) ? 10 : -10);
|
|
return {
|
|
score: async (_q, doc) => scoreOf(doc),
|
|
scoreBatch: async (_q, docs) => docs.map(scoreOf),
|
|
};
|
|
}
|
|
|
|
it('re-orders the RRF pool by reranker score', async () => {
|
|
// Both frames match the query keywords; the marker one should win
|
|
// ONLY when the reranker is active.
|
|
frames.createIFrame(gopId, 'deploy checklist for the staging rollout', 'normal', 'user_stated');
|
|
frames.createIFrame(gopId, 'deploy checklist MARKER for the production rollout', 'normal', 'user_stated');
|
|
|
|
const reranked = await search.search('deploy checklist rollout', {
|
|
limit: 2,
|
|
reranker: markerReranker('MARKER'),
|
|
});
|
|
expect(reranked.length).toBeGreaterThan(0);
|
|
expect(reranked[0].frame.content).toContain('MARKER');
|
|
expect(reranked[0].finalScore).toBe(10);
|
|
});
|
|
|
|
it('soft-fails to RRF ordering when the reranker throws', async () => {
|
|
frames.createIFrame(gopId, 'deploy checklist for the staging rollout', 'normal', 'user_stated');
|
|
|
|
const broken: Reranker = {
|
|
score: async () => { throw new Error('model load failed'); },
|
|
scoreBatch: async () => { throw new Error('model load failed'); },
|
|
};
|
|
const results = await search.search('deploy checklist', { limit: 5, reranker: broken });
|
|
expect(results.length).toBeGreaterThan(0); // recall survives
|
|
});
|
|
|
|
it('returns identical results when no reranker is passed (back-compat)', async () => {
|
|
frames.createIFrame(gopId, 'deploy checklist for the staging rollout', 'normal', 'user_stated');
|
|
const a = await search.search('deploy checklist', { limit: 5 });
|
|
const b = await search.search('deploy checklist', { limit: 5 });
|
|
expect(a.map(r => r.frame.id)).toEqual(b.map(r => r.frame.id));
|
|
});
|
|
});
|
|
|
|
describe('computeRelevance — temporal anchors on created_at (W4.2 bug #3)', () => {
|
|
it('scores an old-created frame low even when freshly accessed', () => {
|
|
const now = new Date().toISOString();
|
|
const yearAgo = new Date(Date.now() - 365 * 86400000).toISOString();
|
|
|
|
const oldButTouched = computeRelevance(
|
|
{ id: 1, created_at: yearAgo, last_accessed: now, access_count: 0, importance: 'normal' },
|
|
SCORING_PROFILES.recent,
|
|
);
|
|
const trulyRecent = computeRelevance(
|
|
{ id: 2, created_at: now, last_accessed: now, access_count: 0, importance: 'normal' },
|
|
SCORING_PROFILES.recent,
|
|
);
|
|
// Before the fix both scored identically (decay ran on last_accessed,
|
|
// which touch() bumps to now on every read).
|
|
expect(trulyRecent).toBeGreaterThan(oldButTouched);
|
|
});
|
|
|
|
it('falls back to last_accessed when created_at is absent (back-compat)', () => {
|
|
const now = new Date().toISOString();
|
|
const score = computeRelevance(
|
|
{ id: 3, last_accessed: now, access_count: 0, importance: 'normal' },
|
|
SCORING_PROFILES.recent,
|
|
);
|
|
expect(score).toBeGreaterThan(0);
|
|
});
|
|
});
|