This commit is contained in:
123
packages/agent/tests/w46-rawdetail-recall.test.ts
Normal file
123
packages/agent/tests/w46-rawdetail-recall.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { MindDB, FrameStore, SessionStore, rawTurnHeader, type Reranker } from '@waggle/core';
|
||||
import { Orchestrator } from '../src/orchestrator.js';
|
||||
import { MockEmbedder } from '../../hive-mind-core/tests/mind/helpers/mock-embedder.js';
|
||||
|
||||
/**
|
||||
* W4.6 — RAWDETAIL lane rendering in recallMemory (plan component #10,
|
||||
* query side). Raw-turn frames are written by harvest (W4.6 write side);
|
||||
* here we seed them directly and assert: section renders LAST, CE-top
|
||||
* turn + dialogue neighbors included, kill switch + no-reranker skip.
|
||||
*/
|
||||
|
||||
/** Deterministic fake CE: score = marker-word hit count. */
|
||||
function markerReranker(marker: string): Reranker {
|
||||
return {
|
||||
scoreBatch: async (_q: string, docs: string[]) =>
|
||||
docs.map(d => (d.includes(marker) ? 1 : 0)),
|
||||
} as Reranker;
|
||||
}
|
||||
|
||||
describe('W4.6 — recallMemory raw dialogue excerpts', () => {
|
||||
let db: MindDB;
|
||||
let frames: FrameStore;
|
||||
let gopId: string;
|
||||
let savedKill: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new MindDB(':memory:');
|
||||
frames = new FrameStore(db);
|
||||
gopId = new SessionStore(db).create().gop_id;
|
||||
savedKill = process.env['WAGGLE_RAWDETAIL'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (savedKill === undefined) delete process.env['WAGGLE_RAWDETAIL'];
|
||||
else process.env['WAGGLE_RAWDETAIL'] = savedKill;
|
||||
db.close();
|
||||
});
|
||||
|
||||
function seedTurns(): void {
|
||||
const texts = [
|
||||
'we should plan the gallery visit for the weekend',
|
||||
'I loved the painting of a sunset with a pink sky',
|
||||
'it was hanging in the east wing near the entrance',
|
||||
];
|
||||
texts.forEach((text, i) => {
|
||||
frames.createIFrame(
|
||||
gopId,
|
||||
`${rawTurnHeader('conv-x', i, i % 2 === 0 ? 'user' : 'assistant')}\n${text}`,
|
||||
'normal',
|
||||
'import',
|
||||
`2026-05-${String(10 + i).padStart(2, '0')}T12:00:00Z`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it('renders the verbatim excerpts section with CE-top turn + neighbors, dated and speaker-tagged', async () => {
|
||||
seedTurns();
|
||||
const orchestrator = new Orchestrator({
|
||||
db, embedder: new MockEmbedder(), reranker: markerReranker('painting'),
|
||||
});
|
||||
const result = await orchestrator.recallMemory('which painting did I love?');
|
||||
|
||||
const text = result.text;
|
||||
expect(text).toContain('## Raw dialogue excerpts (verbatim)');
|
||||
expect(text).toContain('painting of a sunset with a pink sky');
|
||||
// ±1 neighbors ride along
|
||||
expect(text).toContain('gallery visit');
|
||||
expect(text).toContain('east wing');
|
||||
// speaker + date prefixes — parenthesized speaker (colon-suffixed role
|
||||
// labels would collide with the injection scanner's smuggling patterns)
|
||||
expect(text).toMatch(/- \[2026-05-11\] \(assistant\) I loved the painting/);
|
||||
// rendered LAST — after every other section
|
||||
const rawIdx = text.indexOf('## Raw dialogue excerpts');
|
||||
for (const section of ['## Workspace Memory', '## Personal Memory', '## Memory Facts']) {
|
||||
const idx = text.indexOf(section);
|
||||
if (idx >= 0) expect(idx).toBeLessThan(rawIdx);
|
||||
}
|
||||
});
|
||||
|
||||
it('counts raw excerpts in totalCount and surfaces them in recalled', async () => {
|
||||
seedTurns();
|
||||
const orchestrator = new Orchestrator({
|
||||
db, embedder: new MockEmbedder(), reranker: markerReranker('painting'),
|
||||
});
|
||||
const result = await orchestrator.recallMemory('which painting did I love?');
|
||||
expect(result.count).toBeGreaterThanOrEqual(3);
|
||||
expect(result.recalled?.some(s => s.includes('pink sky'))).toBe(true);
|
||||
});
|
||||
|
||||
it('WAGGLE_RAWDETAIL=0 kill switch suppresses the lane', async () => {
|
||||
seedTurns();
|
||||
process.env['WAGGLE_RAWDETAIL'] = '0';
|
||||
const orchestrator = new Orchestrator({
|
||||
db, embedder: new MockEmbedder(), reranker: markerReranker('painting'),
|
||||
});
|
||||
const result = await orchestrator.recallMemory('which painting did I love?');
|
||||
expect(result.text).not.toContain('## Raw dialogue excerpts');
|
||||
});
|
||||
|
||||
it('skips the lane when no reranker is available (no CE floor → no lane)', async () => {
|
||||
seedTurns();
|
||||
process.env['WAGGLE_RERANKER'] = '0';
|
||||
try {
|
||||
const orchestrator = new Orchestrator({ db, embedder: new MockEmbedder() });
|
||||
const result = await orchestrator.recallMemory('which painting did I love?');
|
||||
expect(result.text).not.toContain('## Raw dialogue excerpts');
|
||||
} finally {
|
||||
delete process.env['WAGGLE_RERANKER'];
|
||||
}
|
||||
});
|
||||
|
||||
it('raw turns surfaced as search snippets never double-render in the excerpts section', async () => {
|
||||
seedTurns();
|
||||
const orchestrator = new Orchestrator({
|
||||
db, embedder: new MockEmbedder(), reranker: markerReranker('painting'),
|
||||
});
|
||||
const result = await orchestrator.recallMemory('which painting did I love?');
|
||||
// the CE-top turn body must appear exactly once in the rendered block
|
||||
const occurrences = result.text.split('painting of a sunset with a pink sky').length - 1;
|
||||
expect(occurrences).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user