import { describe, expect, it, beforeEach } from "bun:test"; import { maybeConsolidate, resetConsolidationState, DEFAULT_AUTO_CONSOLIDATION_CONFIG, type AutoConsolidationConfig, } from "../auto-consolidation.js"; import type { MemoryEntry, MemorySearchResult } from "../store.js"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeEntry(id: string, text: string, overrides: Partial = {}): MemoryEntry { return { id, text, vector: [1, 0, 0], category: "events", scope: "project:test", importance: 0.7, timestamp: Date.now(), metadata: "{}", ...overrides, }; } function createMockStore(entries: MemoryEntry[]) { const data = new Map(entries.map(e => [e.id, { ...e }])); return { async list(scopeFilter?: string[], _category?: string, limit = 500, _offset = 0) { return [...data.values()] .filter(e => !scopeFilter || scopeFilter.some(s => e.scope === s)) .slice(0, limit); }, async getById(id: string) { return data.get(id) ?? null; }, async vectorSearch(_vector: number[], _limit = 5, _minScore = 0.3, _scopeFilter?: string[]) { return [] as MemorySearchResult[]; }, async update(id: string, updates: any, _scopeFilter?: string[]) { const entry = data.get(id); if (!entry) return null; Object.assign(entry, updates); return entry; }, async stats(scopeFilter?: string[]) { const filtered = [...data.values()].filter( e => !scopeFilter || scopeFilter.some(s => e.scope === s), ); return { totalCount: filtered.length, scopeCounts: {}, categoryCounts: {} }; }, hasFtsSupport: false, } as any; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("auto-consolidation", () => { beforeEach(() => { resetConsolidationState(); }); it("skips when not enough new memories", async () => { const entries = Array.from({ length: 10 }, (_, i) => makeEntry(`id-${i}`, `memory ${i}`), ); const store = createMockStore(entries); const result = await maybeConsolidate(store, "project:test"); expect(result.triggered).toBe(false); expect(result.reason).toBe("insufficient_new_memories"); }); it("skips when too soon since last run", async () => { // Start with 60 entries, trigger first run, then grow to 120 so // the memory-count gate passes but the time gate blocks. const data: MemoryEntry[] = Array.from({ length: 60 }, (_, i) => makeEntry(`id-${i}`, `memory ${i}`), ); const store = createMockStore(data); const config: AutoConsolidationConfig = { minNewMemories: 50, minHoursSinceLastRun: 12, consolidation: DEFAULT_AUTO_CONSOLIDATION_CONFIG.consolidation, }; // First run: triggers (never run before, 60 > 50 new memories) const first = await maybeConsolidate(store, "project:test", config); expect(first.triggered).toBe(true); // Grow the store so memory-count gate would pass for (let i = 60; i < 120; i++) { data.push(makeEntry(`id-${i}`, `memory ${i}`)); } const bigStore = createMockStore(data); // Second run immediately: time gate blocks (< 12 hours) const second = await maybeConsolidate(bigStore, "project:test", config); expect(second.triggered).toBe(false); expect(second.reason).toBe("too_soon"); }); it("triggers when both conditions met", async () => { const entries = Array.from({ length: 60 }, (_, i) => makeEntry(`id-${i}`, `memory ${i}`), ); const store = createMockStore(entries); const config: AutoConsolidationConfig = { minNewMemories: 50, minHoursSinceLastRun: 0, // disable time gate for test consolidation: { clusterThreshold: 0.82, mergeThreshold: 0.92, maxEntriesPerRun: 500, }, }; const result = await maybeConsolidate(store, "project:test", config); expect(result.triggered).toBe(true); expect(result.consolidation).toBeDefined(); expect(result.consolidation!.scope).toBe("project:test"); }); it("returns consolidation result when triggered", async () => { const entries = Array.from({ length: 60 }, (_, i) => makeEntry(`id-${i}`, `memory ${i}`), ); const store = createMockStore(entries); const config: AutoConsolidationConfig = { minNewMemories: 50, minHoursSinceLastRun: 0, consolidation: { clusterThreshold: 0.82, mergeThreshold: 0.92, maxEntriesPerRun: 500, }, }; const result = await maybeConsolidate(store, "project:test", config); expect(result.triggered).toBe(true); expect(result.consolidation!.originalCount).toBe(60); }); it("respects custom thresholds", async () => { const entries = Array.from({ length: 200 }, (_, i) => makeEntry(`id-${i}`, `memory ${i}`), ); const store = createMockStore(entries); const config: AutoConsolidationConfig = { minNewMemories: 300, // higher than available minHoursSinceLastRun: 0, consolidation: DEFAULT_AUTO_CONSOLIDATION_CONFIG.consolidation, }; const result = await maybeConsolidate(store, "project:test", config); expect(result.triggered).toBe(false); expect(result.reason).toBe("insufficient_new_memories"); }); });