82 lines
3.8 KiB
TypeScript
82 lines
3.8 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { MindDB, FrameStore, SessionStore, MIND_FACT_PREFIX, MIND_EVENT_PREFIX, MIND_PROFILE_PREFIX } from '@waggle/core';
|
|
import { Orchestrator } from '../src/orchestrator.js';
|
|
import { MockEmbedder } from '../../hive-mind-core/tests/mind/helpers/mock-embedder.js';
|
|
|
|
/**
|
|
* W4.3b — extraction-lane rendering in recallMemory (plan components
|
|
* #5/#6/#8 query side). Lane frames are written by extract-memory-lanes
|
|
* (W4.3a); here we seed them directly and assert the render contract:
|
|
* profiles first, dated facts, chronological events, windowed events
|
|
* section, and the profile-never-as-snippet dedup rule.
|
|
*/
|
|
|
|
describe('W4.3b — recallMemory extraction lanes', () => {
|
|
let db: MindDB;
|
|
let orchestrator: Orchestrator;
|
|
let frames: FrameStore;
|
|
let gopId: string;
|
|
|
|
beforeEach(() => {
|
|
db = new MindDB(':memory:');
|
|
orchestrator = new Orchestrator({ db, embedder: new MockEmbedder() });
|
|
frames = new FrameStore(db);
|
|
gopId = new SessionStore(db).create().gop_id;
|
|
});
|
|
|
|
afterEach(() => {
|
|
db.close();
|
|
});
|
|
|
|
function seedLanes(): void {
|
|
frames.createIFrame(gopId, `${MIND_PROFILE_PREFIX} Ana]\nAna is a designer based in Belgrade who plays Hollow Knight.`, 'normal', 'system');
|
|
frames.createIFrame(gopId, `${MIND_FACT_PREFIX}\nUser preference: Ana prefers dark mode`, 'normal', 'system');
|
|
frames.createIFrame(gopId, `${MIND_EVENT_PREFIX}\n[2026-05-07] Ana visited the dentist`, 'normal', 'system', '2026-05-07T00:00:00.000Z');
|
|
frames.createIFrame(gopId, `${MIND_EVENT_PREFIX}\n[2026-06-02] Ana shipped the redesign`, 'normal', 'system', '2026-06-02T00:00:00.000Z');
|
|
}
|
|
|
|
it('renders profiles, facts, and chronological events sections', async () => {
|
|
seedLanes();
|
|
const result = await orchestrator.recallMemory('what is happening');
|
|
|
|
expect(result.count).toBeGreaterThanOrEqual(4);
|
|
const text = result.text;
|
|
expect(text).toContain('## Profiles');
|
|
expect(text).toContain('- Ana: Ana is a designer');
|
|
expect(text).toContain('## Memory Facts');
|
|
expect(text).toContain('Ana prefers dark mode');
|
|
expect(text).toContain('## Events (chronological)');
|
|
expect(text).toContain('[2026-05-07] Ana visited the dentist');
|
|
|
|
// render order: profiles before facts before events
|
|
expect(text.indexOf('## Profiles')).toBeLessThan(text.indexOf('## Memory Facts'));
|
|
expect(text.indexOf('## Memory Facts')).toBeLessThan(text.indexOf('## Events (chronological)'));
|
|
});
|
|
|
|
it('explicit-period query gets a dedicated windowed events section', async () => {
|
|
seedLanes();
|
|
const result = await orchestrator.recallMemory('what did Ana do in May 2026?');
|
|
expect(result.text).toContain('## Events during may 2026');
|
|
// windowed section carries the in-window event, not the June one
|
|
const windowed = result.text.slice(result.text.indexOf('## Events during'));
|
|
expect(windowed).toContain('dentist');
|
|
expect(windowed).not.toContain('redesign');
|
|
});
|
|
|
|
it('profile frames never double-render as search snippets', async () => {
|
|
seedLanes();
|
|
// Query keyword-matches the profile card content — the semantic lane
|
|
// would surface the profile frame; the dedup rule must keep it out.
|
|
const result = await orchestrator.recallMemory('Hollow Knight designer Belgrade');
|
|
const occurrences = result.text.split('Hollow Knight').length - 1;
|
|
expect(occurrences).toBe(1); // once under ## Profiles, never as a snippet
|
|
});
|
|
|
|
it('lane-only recall still returns content (no search hits required)', async () => {
|
|
frames.createIFrame(gopId, `${MIND_FACT_PREFIX}\nUser preference: Ana prefers dark mode`, 'normal', 'system');
|
|
const result = await orchestrator.recallMemory('zebra quantum xylophone');
|
|
expect(result.count).toBeGreaterThan(0);
|
|
expect(result.text).toContain('## Memory Facts');
|
|
});
|
|
});
|