Files
wehub-resource-sync 979fb22d7c
CI / test (20, macos-latest) (push) Has been cancelled
CI / test (20, ubuntu-latest) (push) Has been cancelled
CI / test (22, macos-latest) (push) Has been cancelled
CI / test (22, ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:18 +08:00

353 lines
13 KiB
TypeScript

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<string, Map<string, unknown>>();
return {
get: async <T>(scope: string, key: string): Promise<T | null> => {
return (store.get(scope)?.get(key) as T) ?? null;
},
set: async <T>(scope: string, key: string, data: T): Promise<T> => {
if (!store.has(scope)) store.set(scope, new Map());
store.get(scope)!.set(key, data);
return data;
},
delete: async (scope: string, key: string): Promise<void> => {
store.get(scope)?.delete(key);
},
list: async <T>(scope: string): Promise<T[]> => {
const entries = store.get(scope);
return entries ? (Array.from(entries.values()) as T[]) : [];
},
};
}
function mockSdk() {
const functions = new Map<string, Function>();
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 = `<insights>
<insight confidence="0.85" title="Defense in Depth">
Security requires layered protection: input validation, safe APIs, and deny-lists together.
</insight>
<insight confidence="0.7" title="Testing at Boundaries">
Focus test effort on system boundaries where trust transitions occur.
</insight>
</insights>`;
describe("Reflect", () => {
let sdk: ReturnType<typeof mockSdk>;
let kv: ReturnType<typeof mockKV>;
let provider: { name: string; compress: ReturnType<typeof vi.fn>; summarize: ReturnType<typeof vi.fn> };
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<Insight>("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<Insight>("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<Insight>("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<Insight & { score: number }> };
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<Insight>("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<Insight>("mem:insights", "ins_weak");
expect(after!.deleted).toBe(true);
});
});
});