import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; import { BOOTSTRAP_LIMIT_BYTES, bootstrap, readNode, reviewTree, route, verifyTree } from "./hldp-memory.mjs"; function validTree() { return { protocol: "HLDP-v1.0", root: "root", nodes: { root: { path: "root", summary: "总入口", trigger: "用户需要恢复", emergence: "长历史 → 根索引", lock: "按需展开", why: "避免全量加载", children: ["root/protocol", "root/decision"], sources: ["source://root"], rejected: [] }, "root/protocol": { path: "root/protocol", summary: "HLDP 协议与递归折叠", trigger: "需要解码", emergence: "摘要 → 因果树", lock: "使用v1", why: "保留为什么", children: [], sources: ["hldp/HLDP-RUNTIME-ROOT-v1.0.hdlp"], rejected: ["每轮注入全文:会撑爆上下文"] }, "root/decision": { path: "root/decision", summary: "方案变化与最终决策", trigger: "出现三个方案", emergence: "A失败 → B不足 → C锁定", lock: "采用C | 失效=证据变化", why: "后续执行依赖转折", children: [], sources: ["source://decision"], rejected: ["A:失败", "B:不足"] } } }; } test("validates a bounded causal tree", () => assert.deepEqual(verifyTree(validTree()), [])); test("reviews whether a leaf preserves causal transitions instead of becoming a diary entry", () => { const reviewed = reviewTree(validTree(), "root/decision"); assert.equal(reviewed.ok, true); assert.equal(reviewed.reviewed, 1); const diary = validTree(); diary.nodes["root/decision"].emergence = "上午讨论方案,下午继续工作,晚上保存文件"; diary.nodes["root/decision"].why = "记一下"; const rejected = reviewTree(diary, "root/decision"); assert.equal(rejected.ok, false); assert(rejected.errors.some((error) => error.includes("transition chain"))); assert(rejected.errors.some((error) => error.includes("why is too generic"))); }); test("rejects more than ten siblings and missing why", () => { const tree = validTree(); tree.nodes.root.children = Array.from({ length: 11 }, (_, i) => `child-${i}`); tree.nodes["root/protocol"].why = ""; tree.nodes["root/decision"].sources = []; const errors = verifyTree(tree); assert(errors.some((error) => error.includes("more than 10"))); assert(errors.some((error) => error.includes("missing why"))); assert(errors.some((error) => error.includes("sources must be a non-empty array"))); }); test("route returns paths and summaries only, capped at three", () => { const candidates = route(validTree(), "最终决策 方案", 9); assert.equal(candidates.length, 2); assert.equal(candidates[0].path, "root/decision"); assert.deepEqual(Object.keys(candidates[0]).sort(), ["path", "score", "summary"]); assert(!JSON.stringify(candidates).includes("A失败")); }); test("route ranks a matching Chinese branch instead of treating a sentence as one token", () => { const candidates = route(validTree(), "为什么最终方案发生变化", 3); assert.equal(candidates[0].path, "root/decision"); assert(candidates[0].score > 0); }); test("read-node reads exactly one bounded node", () => { const node = readNode(validTree(), "root/decision"); assert.equal(node.path, "root/decision"); }); test("bootstrap is small and contains no memory body", () => { const text = bootstrap("hldp/HLDP-RUNTIME-ROOT-v1.0.hdlp"); assert(Buffer.byteLength(text) <= BOOTSTRAP_LIMIT_BYTES); assert(!text.includes("capsule")); assert(!text.includes("session log")); }); test("Codex hook emits context only for SessionStart", () => { const script = fileURLToPath(new URL("./codex-session-bootstrap.mjs", import.meta.url)); const started = execFileSync(process.execPath, [script], { input: JSON.stringify({ hook_event_name: "SessionStart" }), encoding: "utf8" }); assert(JSON.parse(started).hookSpecificOutput.additionalContext); const prompt = execFileSync(process.execPath, [script], { input: JSON.stringify({ hook_event_name: "UserPromptSubmit" }), encoding: "utf8" }); assert.equal(prompt, ""); }); test("repository routes HLDP to v1 root and marks v3 historical", () => { const repo = path.resolve(fileURLToPath(new URL("../../", import.meta.url))); const index = fs.readFileSync(path.join(repo, "INDEX.hdlp"), "utf8"); const tower = fs.readFileSync(path.join(repo, "BROADCAST-TOWER.hdlp"), "utf8"); const v3 = fs.readFileSync(path.join(repo, "hldp/HLDP-SPEC-v3.0-TECHNICAL.md"), "utf8"); assert(index.includes("hldp/HLDP-RUNTIME-ROOT-v1.0.hdlp")); assert(tower.includes("先读 hldp/HLDP-RUNTIME-ROOT-v1.0.hdlp")); assert(v3.includes("HISTORICAL-EVOLUTION-DRAFT")); });