import { describe, expect, it } from "bun:test"; import { runMemoryLint, formatMemoryLintReport, computeHealthScore, type MemoryLintReport } from "../memory-lint.js"; import type { MemoryEntry, MemoryStore } from "../store.js"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeEntry(overrides: Partial & { id: string }): MemoryEntry { return { text: "test memory", vector: [1, 0, 0, 0, 0], category: "events", scope: "project:test", importance: 0.7, timestamp: Date.now(), metadata: JSON.stringify({ evolution: { status: "active", version: 1, accessCount: 0, lastAccessedAt: null, supersededBy: null, supersedes: null, evolutionNote: null, consolidatedInto: null, contributedToPattern: null, sourceMemories: [], validFrom: Date.now(), validUntil: null, }, }), ...overrides, }; } function createMockStore(entries: MemoryEntry[]): Pick { return { // 复刻真实 store:list() 为性能不返回向量(vector: []),lint 需经 getVectors 补回。 // 旧 mock 直接返回带向量的 entry,与生产行为不符,曾掩盖矛盾/去重在生产哑火的 bug。 async list() { return entries.map(e => ({ ...e, vector: [] })); }, async getVectors(ids: string[]) { const map = new Map(); for (const e of entries) { if (ids.includes(e.id) && e.vector.length > 0) map.set(e.id, e.vector); } return map; }, } as Pick; } // --------------------------------------------------------------------------- // Cold memories (P0 B-1 observation) // --------------------------------------------------------------------------- describe("cold memory lint (P0 B-1)", () => { it("aggregates cold memories into a single info finding with top ids (flag on)", async () => { process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL = "true"; try { const coldMeta = (n: number) => JSON.stringify({ accessCount: n, evolution: { status: "active", version: 1 }, }); // 向量彼此正交、文本不同,避免触发 duplicate/contradiction 干扰 const entries = [ makeEntry({ id: "cold-a", text: "alpha topic", vector: [1, 0, 0, 0, 0], metadata: coldMeta(12) }), makeEntry({ id: "cold-b", text: "beta topic", vector: [0, 1, 0, 0, 0], metadata: coldMeta(7) }), makeEntry({ id: "warm-c", text: "gamma topic", vector: [0, 0, 1, 0, 0], metadata: JSON.stringify({ accessCount: 9, usage: { useCount: 2 }, evolution: { status: "active", version: 1 } }) }), ]; const report = await runMemoryLint({ store: createMockStore(entries) as MemoryStore }); expect(report.summary.coldMemories).toBe(2); const finding = report.findings.find(f => f.check === "coldMemory"); expect(finding).toBeTruthy(); expect(finding!.severity).toBe("info"); // top ids sorted by injection desc expect(finding!.memoryIds[0]).toBe("cold-a"); expect(finding!.memoryIds).toContain("cold-b"); expect(formatMemoryLintReport(report)).toContain("Cold Memories (2)"); } finally { delete process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL; } }); it("skips cold detection entirely when use signal is inactive (flag off)", async () => { delete process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL; const coldishMeta = JSON.stringify({ accessCount: 12, evolution: { status: "active", version: 1 } }); const report = await runMemoryLint({ store: createMockStore([makeEntry({ id: "a", text: "alpha", metadata: coldishMeta })]) as MemoryStore, }); expect(report.summary.coldMemories).toBe(0); expect(report.findings.find(f => f.check === "coldMemory")).toBeUndefined(); }); it("health score dents only when cold share exceeds 10%, capped at 10", () => { const base = { contradictions: 0, duplicates: 0, staleMemories: 0, orphans: 0, weakLessons: 0 }; // 5% cold -> no penalty expect(computeHealthScore({ ...base, coldMemories: 5 }, 100)).toBe(100); // 15% cold -> floor(15-10)=5 penalty expect(computeHealthScore({ ...base, coldMemories: 15 }, 100)).toBe(95); // 60% cold -> capped at 10 expect(computeHealthScore({ ...base, coldMemories: 60 }, 100)).toBe(90); // backward compat: summary without coldMemories expect(computeHealthScore(base, 100)).toBe(100); }); }); // --------------------------------------------------------------------------- // runMemoryLint // --------------------------------------------------------------------------- describe("runMemoryLint", () => { it("returns all-clear for healthy data", async () => { const entries = [ makeEntry({ id: "a1", text: "User prefers dark mode", category: "preferences", vector: [1, 0, 0, 0, 0] }), makeEntry({ id: "a2", text: "Project uses Bun runtime", category: "entities", vector: [0, 1, 0, 0, 0] }), ]; const store = createMockStore(entries); const report = await runMemoryLint({ store }); expect(report.healthScore).toBe(100); expect(report.findings).toHaveLength(0); expect(report.summary.contradictions).toBe(0); expect(report.summary.duplicates).toBe(0); expect(report.totalScanned).toBe(2); }); it("detects contradictions in same scope+category", async () => { const entries = [ makeEntry({ id: "c1", text: "Always use Bun for scripts", scope: "project:x", category: "patterns" }), makeEntry({ id: "c2", text: "Never use Bun for scripts", scope: "project:x", category: "patterns" }), ]; const store = createMockStore(entries); const report = await runMemoryLint({ store }); expect(report.summary.contradictions).toBeGreaterThanOrEqual(1); const finding = report.findings.find(f => f.check === "contradiction"); expect(finding).toBeDefined(); expect(finding!.memoryIds).toContain("c1"); expect(finding!.memoryIds).toContain("c2"); }); it("detects duplicates by vector similarity", async () => { const vec = [0.5, 0.5, 0.5, 0.5, 0.5]; const entries = [ makeEntry({ id: "d1", text: "Docker port is 4318", vector: vec, scope: "project:x", category: "entities" }), makeEntry({ id: "d2", text: "Docker port is 4318 config", vector: vec, scope: "project:x", category: "entities" }), ]; const store = createMockStore(entries); const report = await runMemoryLint({ store }); expect(report.summary.duplicates).toBeGreaterThanOrEqual(1); const finding = report.findings.find(f => f.check === "duplicate"); expect(finding).toBeDefined(); }); it("detects stale memories (old, never accessed)", async () => { const oldTimestamp = Date.now() - 120 * 24 * 60 * 60 * 1000; // 120 days ago const entries = [ makeEntry({ id: "s1", text: "Some old fact", timestamp: oldTimestamp, metadata: JSON.stringify({ evolution: { status: "active", version: 1, accessCount: 0, lastAccessedAt: null, supersededBy: null, supersedes: null, evolutionNote: null, consolidatedInto: null, contributedToPattern: null, sourceMemories: [], validFrom: oldTimestamp, validUntil: null, }, }), }), ]; const store = createMockStore(entries); const report = await runMemoryLint({ store }); expect(report.summary.staleMemories).toBe(1); const finding = report.findings.find(f => f.check === "stale"); expect(finding).toBeDefined(); expect(finding!.severity).toBe("info"); }); it("detects orphans with missing scope", async () => { const entries = [ makeEntry({ id: "o1", text: "Orphan memory", scope: "" }), makeEntry({ id: "o2", text: "Schema entry", scope: "__schema__" }), ]; const store = createMockStore(entries); const report = await runMemoryLint({ store }); expect(report.summary.orphans).toBe(2); }); it("detects broken consolidation links", async () => { const entries = [ makeEntry({ id: "b1", text: "Points to deleted", metadata: JSON.stringify({ evolution: { status: "active", version: 1, accessCount: 5, lastAccessedAt: Date.now(), supersededBy: null, supersedes: null, evolutionNote: null, consolidatedInto: "nonexistent-id-12345", contributedToPattern: null, sourceMemories: [], validFrom: Date.now(), validUntil: null, }, }), }), ]; const store = createMockStore(entries); const report = await runMemoryLint({ store }); expect(report.summary.orphans).toBe(1); const finding = report.findings.find(f => f.check === "orphan" && f.severity === "warning"); expect(finding).toBeDefined(); }); it("skips non-active entries", async () => { const entries = [ makeEntry({ id: "arc1", text: "Archived memory", scope: "", metadata: JSON.stringify({ evolution: { status: "archived", version: 1, accessCount: 0, lastAccessedAt: null, supersededBy: null, supersedes: null, evolutionNote: null, consolidatedInto: null, contributedToPattern: null, sourceMemories: [], validFrom: Date.now(), validUntil: null, }, }), }), ]; const store = createMockStore(entries); const report = await runMemoryLint({ store }); // Archived entry should be skipped, so no orphan finding expect(report.totalScanned).toBe(0); expect(report.findings).toHaveLength(0); }); // ---- Weak lessons (action-verb heuristic, inspired by Orb) ---------------- it("flags patterns without any action verb as weak lessons", async () => { const entries = [ makeEntry({ id: "wl1", text: "This phenomenon is very interesting and noteworthy", category: "patterns", scope: "project:x", vector: [1, 0, 0, 0, 0], }), ]; const store = createMockStore(entries); const report = await runMemoryLint({ store }); expect(report.summary.weakLessons).toBe(1); const finding = report.findings.find(f => f.check === "weakLesson"); expect(finding).toBeDefined(); expect(finding!.severity).toBe("info"); expect(finding!.memoryIds).toContain("wl1"); }); it("does not flag patterns that contain action verbs (English or Chinese)", async () => { const entries = [ makeEntry({ id: "wl2", text: "Use Bun to run scripts; verify output before committing", category: "patterns", scope: "project:x", vector: [1, 0, 0, 0, 0], }), makeEntry({ id: "wl3", text: "使用 playwright 验证网页交互,失败时重试最多三次", category: "patterns", scope: "project:x", vector: [0, 1, 0, 0, 0], }), ]; const store = createMockStore(entries); const report = await runMemoryLint({ store }); expect(report.summary.weakLessons).toBe(0); }); it("does not flag non-pattern categories for weak lessons (events/profile exempt)", async () => { const entries = [ makeEntry({ id: "wl4", text: "This phenomenon is interesting", // no verb, but not a pattern category: "events", scope: "project:x", vector: [1, 0, 0, 0, 0], }), makeEntry({ id: "wl5", text: "User background info without verbs", category: "profile", scope: "project:x", vector: [0, 1, 0, 0, 0], }), ]; const store = createMockStore(entries); const report = await runMemoryLint({ store }); expect(report.summary.weakLessons).toBe(0); }); }); // --------------------------------------------------------------------------- // computeHealthScore // --------------------------------------------------------------------------- describe("computeHealthScore", () => { it("returns 100 when no issues", () => { expect(computeHealthScore({ contradictions: 0, duplicates: 0, staleMemories: 0, orphans: 0 }, 100)).toBe(100); }); it("deducts correctly for mixed findings", () => { // 1 contradiction (-10) + 2 duplicates (-10) + 4 stale (-2) + 1 orphan (-3) = -25 → 75 expect(computeHealthScore({ contradictions: 1, duplicates: 2, staleMemories: 4, orphans: 1 }, 100)).toBe(75); }); it("clamps to 0", () => { expect(computeHealthScore({ contradictions: 20, duplicates: 0, staleMemories: 0, orphans: 0 }, 10)).toBe(0); }); }); // --------------------------------------------------------------------------- // formatMemoryLintReport // --------------------------------------------------------------------------- describe("formatMemoryLintReport", () => { it("shows all-clear when no findings", () => { const report: MemoryLintReport = { findings: [], healthScore: 100, totalScanned: 50, timestamp: "2026-04-08T12:00:00Z", summary: { contradictions: 0, duplicates: 0, staleMemories: 0, orphans: 0, weakLessons: 0 }, }; const output = formatMemoryLintReport(report); expect(output).toContain("All Clear"); expect(output).toContain("100/100"); }); it("includes findings when present", () => { const report: MemoryLintReport = { findings: [ { check: "contradiction", severity: "warning", detail: '"A" vs "B"', memoryIds: ["id1", "id2"] }, ], healthScore: 90, totalScanned: 100, timestamp: "2026-04-08T12:00:00Z", summary: { contradictions: 1, duplicates: 0, staleMemories: 0, orphans: 0, weakLessons: 0 }, }; const output = formatMemoryLintReport(report); expect(output).toContain("Contradictions (1)"); expect(output).toContain("90/100"); }); it("summarizes stale when count > 5", () => { const staleFindings = Array.from({ length: 10 }, (_, i) => ({ check: "stale" as const, severity: "info" as const, detail: `${100 + i}d old`, memoryIds: [`stale${i}`], })); const report: MemoryLintReport = { findings: staleFindings, healthScore: 95, totalScanned: 200, timestamp: "2026-04-08T12:00:00Z", summary: { contradictions: 0, duplicates: 0, staleMemories: 10, orphans: 0, weakLessons: 0 }, }; const output = formatMemoryLintReport(report); expect(output).toContain("Stale (10)"); expect(output).toContain("10 memories not accessed"); }); it("renders weak lessons section when present", () => { const report: MemoryLintReport = { findings: [ { check: "weakLesson", severity: "info", detail: 'Pattern without action verbs: "X is Y"', memoryIds: ["wl1"] }, { check: "weakLesson", severity: "info", detail: 'Pattern without action verbs: "abstract note"', memoryIds: ["wl2"] }, ], healthScore: 96, totalScanned: 10, timestamp: "2026-04-16T12:00:00Z", summary: { contradictions: 0, duplicates: 0, staleMemories: 0, orphans: 0, weakLessons: 2 }, }; const output = formatMemoryLintReport(report); expect(output).toContain("Weak Lessons (2)"); expect(output).toContain("96/100"); }); });