moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
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 { rawTurnHeader } from '../../src/harvest/raw-turns.js';
import { fetchRawDetailLane, rawTurnBody } from '../../src/mind/raw-detail-lane.js';
import type { Reranker } from '../../src/mind/inprocess-reranker.js';
/**
* W4.6 — RAWDETAIL recall lane: pool (window/FTS) → CE top-K → ±1 dialogue
* neighbors → chronological order, excluding already-rendered frames.
*/
/** Deterministic fake CE: score = number of marker words present in the doc. */
function markerReranker(markers: string[]): Reranker {
return {
scoreBatch: async (_q: string, docs: string[]) =>
docs.map(d => markers.reduce((s, m) => s + (d.includes(m) ? 1 : 0), 0)),
} as Reranker;
}
describe('W4.6 — fetchRawDetailLane', () => {
let db: MindDB;
let frames: FrameStore;
let gopId: string;
beforeEach(() => {
db = new MindDB(':memory:');
frames = new FrameStore(db);
gopId = new SessionStore(db).create().gop_id;
});
afterEach(() => db.close());
/** Seed one conversation of consecutive turns, one day apart. */
function seedConv(conv: string, texts: string[], baseDay = 10): number[] {
const ids: number[] = [];
texts.forEach((text, i) => {
const day = String(baseDay + i).padStart(2, '0');
const f = frames.createIFrame(
gopId,
`${rawTurnHeader(conv, i, i % 2 === 0 ? 'user' : 'assistant')}\n${text}`,
'normal',
'import',
`2026-05-${day}T12:00:00Z`,
);
ids.push(f.id);
});
return ids;
}
it('returns CE-top turns expanded with ±1 dialogue neighbors, chronological', async () => {
seedConv('conv-a', [
'we talked about logistics planning',
'I saw a painting of a sunset with a pink sky', // CE hit (turn 1)
'it was at the Mauritshuis museum',
'unrelated chatter about lunch options',
]);
const hits = await fetchRawDetailLane(
db.getDatabase(),
'what painting did they discuss',
markerReranker(['painting']),
{ k: 1 },
);
// turn 1 + neighbors 0 and 2, in dialogue order
expect(hits.map(h => h.turn)).toEqual([0, 1, 2]);
expect(hits[1].speaker).toBe('assistant');
expect(rawTurnBody(hits[1].content)).toContain('pink sky');
});
it('respects excludeIds (already-rendered frames never double-render)', async () => {
const ids = seedConv('conv-b', [
'first turn about painting brushes',
'second turn about painting canvases',
'third turn about painting frames',
]);
const hits = await fetchRawDetailLane(
db.getDatabase(),
'painting supplies',
markerReranker(['painting']),
{ k: 1, excludeIds: new Set([ids[0]]) },
);
expect(hits.map(h => h.id)).not.toContain(ids[0]);
expect(hits.length).toBeGreaterThan(0);
});
it('date window restricts the pool to in-window turns', async () => {
seedConv('conv-c', [
'painting discussion in early may', // 2026-05-10
'painting discussion mid may', // 2026-05-11
'painting discussion late may', // 2026-05-12
]);
const hits = await fetchRawDetailLane(
db.getDatabase(),
'painting discussion',
markerReranker(['painting']),
{ k: 3, window: { since: '2026-05-11', until: '2026-05-11' } },
);
// pool = only the mid-may turn; neighbor expansion may pull ±1 — but the
// CE top itself must be the in-window turn
expect(hits.some(h => h.content.includes('mid may'))).toBe(true);
});
it('empty window falls back to FTS so the lane never loses recall', async () => {
seedConv('conv-d', ['the gallery showed a watercolor landscape']);
const hits = await fetchRawDetailLane(
db.getDatabase(),
'watercolor landscape gallery',
markerReranker(['watercolor']),
{ window: { since: '2020-01-01', until: '2020-01-31' } },
);
expect(hits.length).toBeGreaterThan(0);
});
it('returns [] when no raw turns exist', async () => {
frames.createIFrame(gopId, 'plain frame about painting', 'normal', 'system');
const hits = await fetchRawDetailLane(
db.getDatabase(),
'painting',
markerReranker(['painting']),
);
expect(hits).toEqual([]);
});
it('soft-fails to [] when the reranker throws', async () => {
seedConv('conv-e', ['painting one', 'painting two']);
const broken = { scoreBatch: async () => { throw new Error('model load failed'); } } as unknown as Reranker;
const hits = await fetchRawDetailLane(db.getDatabase(), 'painting', broken);
expect(hits).toEqual([]);
});
it('neighbor expansion never crosses conversations', async () => {
seedConv('conv-f', ['solo painting turn in conv f']);
seedConv('conv-g', ['unrelated turn in conv g'], 20);
const hits = await fetchRawDetailLane(
db.getDatabase(),
'painting',
markerReranker(['painting']),
{ k: 1 },
);
expect(hits).toHaveLength(1);
expect(hits[0].conv).toBe('conv-f');
});
it('pools Cyrillic raw turns via FTS (S1 Unicode sanitizer)', async () => {
seedConv('conv-cy', [
'разговор о логистици и плановима',
'видели смо слику заласка сунца у Београду',
'посета музеју је била сјајна',
]);
const hits = await fetchRawDetailLane(
db.getDatabase(),
'слику Београду',
markerReranker(['слику']),
{ k: 1 },
);
expect(hits.length).toBeGreaterThan(0);
expect(hits.some(h => rawTurnBody(h.content).includes('слику'))).toBe(true);
});
});