import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { MindDB, FrameStore, SessionStore, type LLMCallFn } from '@waggle/core'; import { runMemoryLaneExtraction } from '../../src/local/memory-lane-cron.js'; /** * W4.3d โ€” memory-lane extraction cron routine (plan ยง5 W4.3 extraction side). * LLM mocked; covers the watermark contract, the min-frames skip, lane-frame * self-feeding exclusion, and idempotency across runs. */ describe('runMemoryLaneExtraction', () => { let db: MindDB; let frames: FrameStore; let gopId: string; const mockLLM: LLMCallFn = async (prompt: string) => { if (prompt.includes('synthesis-level memory facts')) { return '{"facts":[{"category":"preference","speaker":"Ana","text":"User preference: Ana prefers dark mode"}]}'; } if (prompt.includes('datable events')) { return '{"events":[{"session_date":"2026-05-08","cue":"yesterday","event_date":"2026-05-07","text":"Ana visited the dentist"}]}'; } if (prompt.includes('profile card')) { return '{"profiles":[{"speaker":"Ana","card":"Ana is a designer."}]}'; } if (prompt.includes('Extract named entities from the FRAMES')) { // Same entity mentioned in two frames โ€” exercises findEntityByName dedup. return [ '{"frame_id": 1, "name": "Hive Mind", "type": "project"}', '{"frame_id": 2, "name": "Hive Mind", "type": "project"}', ].join('\n'); } return '{}'; }; beforeEach(() => { db = new MindDB(':memory:'); frames = new FrameStore(db); gopId = new SessionStore(db).create().gop_id; }); afterEach(() => { db.close(); }); function seedSourceFrames(n: number): void { for (let i = 0; i < n; i++) { frames.createIFrame(gopId, `Conversation note ${i}: Ana said something useful about topic ${i}.`, 'normal', 'user_stated'); } } it('skips when fewer than the minimum new frames exist', async () => { seedSourceFrames(2); const r = await runMemoryLaneExtraction(db, mockLLM); expect(r.skipped).toBe(true); expect(r.framesProcessed).toBe(0); }); it('extracts lanes and advances the watermark', async () => { seedSourceFrames(8); const r = await runMemoryLaneExtraction(db, mockLLM); expect(r.skipped).toBe(false); expect(r.framesProcessed).toBe(8); expect(r.written).toMatchObject({ factsWritten: 1, eventsWritten: 1, profilesWritten: 1 }); const raw = db.getDatabase(); const event = raw.prepare( `SELECT created_at FROM memory_frames WHERE content LIKE '[mind-event]%'` ).get() as { created_at: string }; expect(event.created_at).toBe('2026-05-07T00:00:00.000Z'); }); it('writes KG entities over the same window; findEntityByName dedups to one row', async () => { seedSourceFrames(8); const r = await runMemoryLaneExtraction(db, mockLLM); expect(r.skipped).toBe(false); // Two mentions of "Hive Mind": one create + one seen_count bump. expect(r.kgEntitiesWritten).toBe(2); expect(r.errors).toHaveLength(0); const raw = db.getDatabase(); const rows = raw.prepare( `SELECT entity_type, properties FROM knowledge_entities WHERE name = 'Hive Mind'` ).all() as Array<{ entity_type: string; properties: string }>; expect(rows).toHaveLength(1); // deduped, not duplicated expect(rows[0].entity_type).toBe('project'); expect(JSON.parse(rows[0].properties)).toMatchObject({ seen_count: 2, source: 'cognify-llm' }); }); it('second run with no new frames skips (watermark holds)', async () => { seedSourceFrames(8); await runMemoryLaneExtraction(db, mockLLM); const r2 = await runMemoryLaneExtraction(db, mockLLM); // lane frames written by run 1 are excluded (no self-feeding), and the // watermark has moved past the 8 source frames โ†’ nothing new. expect(r2.skipped).toBe(true); }); it('excludes [Loop:] automation tick frames from extraction (#13)', async () => { seedSourceFrames(8); for (let i = 0; i < 3; i++) { frames.createIFrame(gopId, `[Loop: nightly-digest] tick ${i}: processed 4 items.`, 'normal', 'agent_inferred'); } const r = await runMemoryLaneExtraction(db, mockLLM); expect(r.skipped).toBe(false); // Only the 8 real conversation frames are fed to the LLM; loop ticks stay out. expect(r.framesProcessed).toBe(8); }); it('processes genuinely new content on a later run', async () => { seedSourceFrames(8); await runMemoryLaneExtraction(db, mockLLM); for (let i = 0; i < 6; i++) { frames.createIFrame(gopId, `Fresh note ${i}: Ana planned the spring offsite agenda item ${i}.`, 'normal', 'user_stated'); } const r2 = await runMemoryLaneExtraction(db, mockLLM); expect(r2.skipped).toBe(false); expect(r2.framesProcessed).toBe(6); }); });