import { describe, it, expect, beforeEach, vi } from "vitest"; vi.mock("../src/logger.js", () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, })); import { registerReflectFunctions } from "../src/functions/reflect.js"; import type { Insight, GraphNode, GraphEdge, SemanticMemory, Lesson, Crystal } from "../src/types.js"; function mockKV() { const store = new Map>(); return { get: async (scope: string, key: string): Promise => { return (store.get(scope)?.get(key) as T) ?? null; }, set: async (scope: string, key: string, data: T): Promise => { if (!store.has(scope)) store.set(scope, new Map()); store.get(scope)!.set(key, data); return data; }, delete: async (scope: string, key: string): Promise => { store.get(scope)?.delete(key); }, list: async (scope: string): Promise => { const entries = store.get(scope); return entries ? (Array.from(entries.values()) as T[]) : []; }, }; } function mockSdk() { const functions = new Map(); return { registerFunction: (idOrOpts: string | { id: string }, handler: Function) => { const id = typeof idOrOpts === "string" ? idOrOpts : idOrOpts.id; functions.set(id, handler); }, registerTrigger: () => {}, trigger: async (idOrInput: string | { function_id: string; payload: unknown }, data?: unknown) => { const id = typeof idOrInput === "string" ? idOrInput : idOrInput.function_id; const payload = typeof idOrInput === "string" ? data : idOrInput.payload; const fn = functions.get(id); if (!fn) throw new Error(`No function: ${id}`); return fn(payload); }, }; } function makeConceptNode(name: string): GraphNode { return { id: `node_${name}`, type: "concept", name, properties: {}, sourceObservationIds: [], createdAt: "2026-04-01T00:00:00Z", }; } function makeEdge(src: string, tgt: string): GraphEdge { return { id: `edge_${src}_${tgt}`, type: "related_to", sourceNodeId: `node_${src}`, targetNodeId: `node_${tgt}`, weight: 1, sourceObservationIds: [], createdAt: "2026-04-01T00:00:00Z", }; } function makeSemantic(fact: string, id?: string): SemanticMemory { return { id: id || `sem_${fact.slice(0, 8)}`, fact, confidence: 0.8, sourceSessionIds: [], sourceMemoryIds: [], accessCount: 1, lastAccessedAt: "2026-04-01T00:00:00Z", strength: 0.8, createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-04-01T00:00:00Z", }; } function makeLesson(content: string, tags: string[]): Lesson { return { id: `lsn_${content.slice(0, 8)}`, content, context: "", confidence: 0.7, reinforcements: 0, source: "manual", sourceIds: [], tags, createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-04-01T00:00:00Z", decayRate: 0.05, }; } function makeCrystal(narrative: string, lessons: string[]): Crystal { return { id: `crys_${narrative.slice(0, 8)}`, narrative, keyOutcomes: [], filesAffected: [], lessons, sourceActionIds: [], createdAt: "2026-04-01T00:00:00Z", }; } const XML_RESPONSE = ` Security requires layered protection: input validation, safe APIs, and deny-lists together. Focus test effort on system boundaries where trust transitions occur. `; describe("Reflect", () => { let sdk: ReturnType; let kv: ReturnType; let provider: { name: string; compress: ReturnType; summarize: ReturnType }; beforeEach(() => { sdk = mockSdk(); kv = mockKV(); provider = { name: "test", compress: vi.fn(), summarize: vi.fn().mockResolvedValue(XML_RESPONSE), }; registerReflectFunctions(sdk as never, kv as never, provider as never); }); describe("mem::reflect", () => { it("returns empty when no graph nodes or memories exist", async () => { const result = (await sdk.trigger("mem::reflect", {})) as { success: boolean; newInsights: number; clustersProcessed: number; }; expect(result.success).toBe(true); expect(result.newInsights).toBe(0); expect(result.clustersProcessed).toBe(0); }); it("synthesizes insights from graph concept clusters", async () => { await kv.set("mem:graph:nodes", "node_security", makeConceptNode("security")); await kv.set("mem:graph:nodes", "node_validation", makeConceptNode("validation")); await kv.set("mem:graph:nodes", "node_testing", makeConceptNode("testing")); await kv.set("mem:graph:edges", "edge_1", makeEdge("security", "validation")); await kv.set("mem:graph:edges", "edge_2", makeEdge("security", "testing")); await kv.set("mem:semantic", "sem_1", makeSemantic("Always validate security inputs")); await kv.set("mem:semantic", "sem_2", makeSemantic("Testing improves security coverage")); await kv.set("mem:semantic", "sem_3", makeSemantic("Validation prevents injection attacks")); await kv.set("mem:lessons", "lsn_1", makeLesson("Use execFile for security", ["security"])); const result = (await sdk.trigger("mem::reflect", {})) as { success: boolean; newInsights: number; }; expect(result.success).toBe(true); expect(result.newInsights).toBe(2); expect(provider.summarize).toHaveBeenCalled(); const insights = await kv.list("mem:insights"); expect(insights.length).toBe(2); expect(insights[0].title).toBeTruthy(); expect(insights[0].sourceConceptCluster.length).toBeGreaterThan(0); }); it("skips clusters with fewer than 3 supporting items", async () => { await kv.set("mem:graph:nodes", "node_sparse", makeConceptNode("sparse")); await kv.set("mem:graph:nodes", "node_topic", makeConceptNode("topic")); await kv.set("mem:graph:edges", "edge_1", makeEdge("sparse", "topic")); await kv.set("mem:semantic", "sem_1", makeSemantic("One sparse fact")); const result = (await sdk.trigger("mem::reflect", {})) as { clustersSkipped: number; newInsights: number; }; expect(result.clustersSkipped).toBe(1); expect(result.newInsights).toBe(0); expect(provider.summarize).not.toHaveBeenCalled(); }); it("deduplicates insights by fingerprint", async () => { await kv.set("mem:graph:nodes", "node_security", makeConceptNode("security")); await kv.set("mem:graph:nodes", "node_validation", makeConceptNode("validation")); await kv.set("mem:graph:edges", "edge_1", makeEdge("security", "validation")); await kv.set("mem:semantic", "sem_1", makeSemantic("Always validate security inputs")); await kv.set("mem:semantic", "sem_2", makeSemantic("Testing improves security coverage")); await kv.set("mem:semantic", "sem_3", makeSemantic("Validation prevents injection")); await sdk.trigger("mem::reflect", {}); const first = await kv.list("mem:insights"); expect(first.length).toBe(2); const result = (await sdk.trigger("mem::reflect", {})) as { reinforced: number; newInsights: number; }; expect(result.reinforced).toBe(2); expect(result.newInsights).toBe(0); const after = await kv.list("mem:insights"); expect(after.length).toBe(2); expect(after[0].reinforcements).toBe(1); }); it("falls back to Jaccard grouping when graph is empty", async () => { await kv.set("mem:semantic", "sem_1", makeSemantic("security validation is important")); await kv.set("mem:semantic", "sem_2", makeSemantic("security testing prevents bugs")); await kv.set("mem:semantic", "sem_3", makeSemantic("validation testing framework")); await kv.set("mem:lessons", "lsn_1", makeLesson("Use security headers", ["security", "validation"])); const result = (await sdk.trigger("mem::reflect", {})) as { success: boolean; usedFallback: boolean; }; expect(result.success).toBe(true); expect(result.usedFallback).toBe(true); }); it("handles LLM failure gracefully", async () => { provider.summarize.mockRejectedValue(new Error("LLM timeout")); await kv.set("mem:graph:nodes", "node_a", makeConceptNode("concept_a")); await kv.set("mem:graph:nodes", "node_b", makeConceptNode("concept_b")); await kv.set("mem:graph:edges", "edge_1", makeEdge("concept_a", "concept_b")); await kv.set("mem:semantic", "sem_1", makeSemantic("fact about concept_a")); await kv.set("mem:semantic", "sem_2", makeSemantic("fact about concept_b")); await kv.set("mem:semantic", "sem_3", makeSemantic("concept_a and concept_b together")); const result = (await sdk.trigger("mem::reflect", {})) as { success: boolean; newInsights: number; }; expect(result.success).toBe(true); expect(result.newInsights).toBe(0); }); }); describe("mem::insight-list", () => { beforeEach(async () => { const now = new Date().toISOString(); await kv.set("mem:insights", "ins_1", { id: "ins_1", title: "Insight A", content: "Content A", confidence: 0.9, reinforcements: 2, sourceConceptCluster: ["security"], sourceMemoryIds: [], sourceLessonIds: [], sourceCrystalIds: [], project: "/app", tags: ["security"], createdAt: now, updatedAt: now, decayRate: 0.05, }); await kv.set("mem:insights", "ins_2", { id: "ins_2", title: "Insight B", content: "Content B", confidence: 0.4, reinforcements: 0, sourceConceptCluster: ["testing"], sourceMemoryIds: [], sourceLessonIds: [], sourceCrystalIds: [], project: "/other", tags: ["testing"], createdAt: now, updatedAt: now, decayRate: 0.05, }); }); it("lists all non-deleted insights sorted by confidence", async () => { const result = (await sdk.trigger("mem::insight-list", {})) as { insights: Insight[] }; expect(result.insights.length).toBe(2); expect(result.insights[0].confidence).toBe(0.9); }); it("filters by project", async () => { const result = (await sdk.trigger("mem::insight-list", { project: "/app" })) as { insights: Insight[] }; expect(result.insights.length).toBe(1); }); it("filters by minConfidence", async () => { const result = (await sdk.trigger("mem::insight-list", { minConfidence: 0.5 })) as { insights: Insight[] }; expect(result.insights.length).toBe(1); }); }); describe("mem::insight-search", () => { beforeEach(async () => { const now = new Date().toISOString(); await kv.set("mem:insights", "ins_1", { id: "ins_1", title: "Defense in Depth", content: "Security requires layered protection", confidence: 0.85, reinforcements: 1, sourceConceptCluster: ["security"], sourceMemoryIds: [], sourceLessonIds: [], sourceCrystalIds: [], tags: ["security"], createdAt: now, updatedAt: now, decayRate: 0.05, }); }); it("finds insights matching query", async () => { const result = (await sdk.trigger("mem::insight-search", { query: "security layered protection", })) as { insights: Array }; expect(result.insights.length).toBe(1); expect(result.insights[0].title).toBe("Defense in Depth"); }); it("rejects empty query", async () => { const result = (await sdk.trigger("mem::insight-search", { query: "" })) as { success: boolean }; expect(result.success).toBe(false); }); }); describe("mem::insight-decay-sweep", () => { it("decays old insights incrementally", async () => { await kv.set("mem:insights", "ins_old", { id: "ins_old", title: "Old", content: "Old insight", confidence: 0.8, reinforcements: 1, sourceConceptCluster: [], sourceMemoryIds: [], sourceLessonIds: [], sourceCrystalIds: [], tags: [], createdAt: new Date(Date.now() - 21 * 86400000).toISOString(), updatedAt: new Date(Date.now() - 21 * 86400000).toISOString(), decayRate: 0.05, }); const result = (await sdk.trigger("mem::insight-decay-sweep", {})) as { decayed: number }; expect(result.decayed).toBe(1); const after = await kv.get("mem:insights", "ins_old"); expect(after!.confidence).toBeLessThan(0.8); expect(after!.lastDecayedAt).toBeDefined(); }); it("soft-deletes low-confidence unreinforced insights", async () => { await kv.set("mem:insights", "ins_weak", { id: "ins_weak", title: "Weak", content: "Weak insight", confidence: 0.12, reinforcements: 0, sourceConceptCluster: [], sourceMemoryIds: [], sourceLessonIds: [], sourceCrystalIds: [], tags: [], createdAt: new Date(Date.now() - 21 * 86400000).toISOString(), updatedAt: new Date(Date.now() - 21 * 86400000).toISOString(), decayRate: 0.05, }); const result = (await sdk.trigger("mem::insight-decay-sweep", {})) as { softDeleted: number }; expect(result.softDeleted).toBe(1); const after = await kv.get("mem:insights", "ins_weak"); expect(after!.deleted).toBe(true); }); }); });