168 lines
6.2 KiB
TypeScript
168 lines
6.2 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { MindDB, FrameStore, SessionStore, KnowledgeGraph } from '@waggle/core';
|
|
import { MemoryWeaver } from '../src/consolidation.js';
|
|
|
|
describe('Enhanced Memory Consolidation', () => {
|
|
let db: MindDB;
|
|
let frames: FrameStore;
|
|
let sessions: SessionStore;
|
|
let kg: KnowledgeGraph;
|
|
let weaver: MemoryWeaver;
|
|
|
|
beforeEach(() => {
|
|
db = new MindDB(':memory:');
|
|
frames = new FrameStore(db);
|
|
sessions = new SessionStore(db);
|
|
kg = new KnowledgeGraph(db);
|
|
weaver = new MemoryWeaver(db, frames, sessions);
|
|
});
|
|
|
|
afterEach(() => {
|
|
db.close();
|
|
});
|
|
|
|
describe('decayByAge — time-based decay', () => {
|
|
it('deprecates temporary frames older than maxAgeDays with low access', () => {
|
|
const session = sessions.create();
|
|
const raw = db.getDatabase();
|
|
|
|
// Create a temporary frame and backdate it to 45 days ago
|
|
const oldFrame = frames.createIFrame(session.gop_id, 'Old temporary note', 'temporary');
|
|
raw.prepare(
|
|
"UPDATE memory_frames SET created_at = datetime('now', '-45 days') WHERE id = ?"
|
|
).run(oldFrame.id);
|
|
|
|
// Create a recent temporary frame (should NOT be deprecated)
|
|
frames.createIFrame(session.gop_id, 'Recent temporary note', 'temporary');
|
|
|
|
// Create an old normal frame (should NOT be deprecated — wrong importance)
|
|
const normalFrame = frames.createIFrame(session.gop_id, 'Old normal note', 'normal');
|
|
raw.prepare(
|
|
"UPDATE memory_frames SET created_at = datetime('now', '-45 days') WHERE id = ?"
|
|
).run(normalFrame.id);
|
|
|
|
const deprecated = weaver.decayByAge(30);
|
|
expect(deprecated).toBe(1);
|
|
|
|
// Verify the old temporary frame is now deprecated
|
|
const updated = frames.getById(oldFrame.id);
|
|
expect(updated!.importance).toBe('deprecated');
|
|
|
|
// Verify the recent temporary frame is still temporary
|
|
const allFrames = frames.getGopFrames(session.gop_id);
|
|
const stillTemp = allFrames.filter(f => f.importance === 'temporary');
|
|
expect(stillTemp).toHaveLength(1);
|
|
expect(stillTemp[0].content).toBe('Recent temporary note');
|
|
});
|
|
|
|
it('does not deprecate old temporary frames with high access count', () => {
|
|
const session = sessions.create();
|
|
const raw = db.getDatabase();
|
|
|
|
const frame = frames.createIFrame(session.gop_id, 'Frequently accessed old temp', 'temporary');
|
|
raw.prepare(
|
|
"UPDATE memory_frames SET created_at = datetime('now', '-45 days') WHERE id = ?"
|
|
).run(frame.id);
|
|
|
|
// Give it many accesses
|
|
for (let i = 0; i < 5; i++) frames.touch(frame.id);
|
|
|
|
const deprecated = weaver.decayByAge(30);
|
|
expect(deprecated).toBe(0);
|
|
|
|
const updated = frames.getById(frame.id);
|
|
expect(updated!.importance).toBe('temporary');
|
|
});
|
|
|
|
it('returns 0 when no frames match criteria', () => {
|
|
const session = sessions.create();
|
|
frames.createIFrame(session.gop_id, 'Normal frame', 'normal');
|
|
frames.createIFrame(session.gop_id, 'Important frame', 'important');
|
|
|
|
const deprecated = weaver.decayByAge(30);
|
|
expect(deprecated).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('linkRelatedFrames — entity-aware linking', () => {
|
|
it('creates B-frames linking frames that share entities', () => {
|
|
const s1 = sessions.create('project:test');
|
|
const s2 = sessions.create('project:test');
|
|
|
|
// Create frames mentioning the same entity
|
|
frames.createIFrame(s1.gop_id, 'Working on the React frontend today');
|
|
frames.createIFrame(s2.gop_id, 'React component performance optimization');
|
|
|
|
// Create an entity in the knowledge graph
|
|
kg.createEntity('technology', 'React', { category: 'frontend' });
|
|
|
|
const linked = weaver.linkRelatedFrames(kg);
|
|
expect(linked).toBe(1);
|
|
|
|
// Verify a B-frame was created
|
|
const gopFrames = frames.getGopFrames(s1.gop_id);
|
|
const bframes = gopFrames.filter(f => f.frame_type === 'B');
|
|
expect(bframes).toHaveLength(1);
|
|
|
|
// Verify B-frame content references the shared entity
|
|
const parsed = JSON.parse(bframes[0].content);
|
|
expect(parsed.description).toContain('React');
|
|
expect(parsed.references).toHaveLength(1);
|
|
});
|
|
|
|
it('does not create B-frames when no shared entities exist', () => {
|
|
const s1 = sessions.create('project:test');
|
|
const s2 = sessions.create('project:test');
|
|
|
|
frames.createIFrame(s1.gop_id, 'Working on the frontend');
|
|
frames.createIFrame(s2.gop_id, 'Backend database migration');
|
|
|
|
kg.createEntity('technology', 'React', { category: 'frontend' });
|
|
|
|
// Only one frame mentions React, so no link should be created
|
|
const linked = weaver.linkRelatedFrames(kg);
|
|
expect(linked).toBe(0);
|
|
});
|
|
|
|
it('handles multiple shared entities without duplicate B-frames', () => {
|
|
const s1 = sessions.create('project:test');
|
|
const s2 = sessions.create('project:test');
|
|
|
|
// Both frames mention React and TypeScript
|
|
frames.createIFrame(s1.gop_id, 'Building React components with TypeScript');
|
|
frames.createIFrame(s2.gop_id, 'React + TypeScript best practices');
|
|
|
|
kg.createEntity('technology', 'React', {});
|
|
kg.createEntity('technology', 'TypeScript', {});
|
|
|
|
const linked = weaver.linkRelatedFrames(kg);
|
|
// Only 1 B-frame for the pair, not 2 (one per entity)
|
|
expect(linked).toBe(1);
|
|
});
|
|
|
|
it('returns 0 when no entities exist in the knowledge graph', () => {
|
|
const session = sessions.create();
|
|
frames.createIFrame(session.gop_id, 'Some content');
|
|
|
|
const linked = weaver.linkRelatedFrames(kg);
|
|
expect(linked).toBe(0);
|
|
});
|
|
|
|
it('links three frames sharing an entity with correct B-frame count', () => {
|
|
const s1 = sessions.create('project:multi');
|
|
const s2 = sessions.create('project:multi');
|
|
const s3 = sessions.create('project:multi');
|
|
|
|
frames.createIFrame(s1.gop_id, 'SQLite schema design');
|
|
frames.createIFrame(s2.gop_id, 'SQLite performance tuning');
|
|
frames.createIFrame(s3.gop_id, 'SQLite backup strategy');
|
|
|
|
kg.createEntity('technology', 'SQLite', {});
|
|
|
|
const linked = weaver.linkRelatedFrames(kg);
|
|
// 3 frames → 3 pairs: (1,2), (1,3), (2,3)
|
|
expect(linked).toBe(3);
|
|
});
|
|
});
|
|
});
|