guanghu-ice-heart/server-tools/persona-daily-fractal-memory/persona-daily-memory.test.mjs

172 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
EVENT_SCHEMA,
MAX_CHILDREN,
appendEvent,
beijingDate,
readDayPage,
readCurrent,
readMemoryNode,
routeMemory,
verifyStore
} from "./persona-daily-memory.mjs";
function roots(t) {
const allowedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "zy-persona-memory-test-"));
const storeRoot = path.join(allowedRoot, "store", "ICE-P-ZY001");
t.after(() => fs.rmSync(allowedRoot, { recursive: true, force: true }));
return { allowedRoot, storeRoot };
}
function event(index, occurredAt = `2026-09-04T${String(8 + Math.floor(index / 60)).padStart(2, "0")}:${String(index % 60).padStart(2, "0")}:00+08:00`) {
return {
schema: EVENT_SCHEMA,
event_id: `EVENT-${String(index).padStart(4, "0")}`,
persona_id: "ICE-P-ZY001",
human_anchor: "ICE-GL∞",
occurred_at: occurredAt,
session_id: "SESSION-TEST",
platform_window_id: `WINDOW-${index}`,
activity: {
what: `记录第${index}段协作`,
where: "Codex/ICE-CH-ZC001",
with_whom: ["ICE-GL∞"],
attention: "人格真实日记忆"
},
branch_id: "memory-model",
branch_summary: "人格主控的真实日分形记忆模型",
day_summary: `当天持续把第1至${index}段协作折叠回同一日页`,
month_summary: "九月建立人格主控的真实时间记忆",
year_summary: "二〇二六年铸渊形成真实日分形记忆能力",
summary: `${index}段:同一日页继续生长`,
trigger: `冰朔与铸渊在第${index}段继续协作。`,
emergence: `${index - 1}段状态 → 第${index}段新增认知并回写同一日页。`,
lock: `${index}段已进入2026-09-04日页平台窗口不成为新根。`,
why: "因为同一真实日的思维链必须连续,未来才能从一页恢复并按需下钻。",
rejected: ["另起窗口摘要:会割裂同一日的因果。"],
sources: [`source://test/event/${index}`]
};
}
test("derives the real Beijing day independently of the host window", () => {
assert.equal(beijingDate("2026-09-04T15:59:59Z"), "2026-09-04");
assert.equal(beijingDate("2026-09-04T16:00:01Z"), "2026-09-05");
});
test("twelve platform windows remain in one day page and fold below ten visible children", (t) => {
const root = roots(t);
for (let i = 1; i <= 12; i += 1) appendEvent({ ...root, event: event(i) });
const current = JSON.parse(fs.readFileSync(path.join(root.storeRoot, "CURRENT.json"), "utf8"));
assert.equal(current.current_day, "2026-09-04");
const day = JSON.parse(fs.readFileSync(current.current_day_path, "utf8"));
assert.equal(day.meta.event_count, 12);
assert.equal(day.nodes[day.root].children.length, 1);
const branch = day.nodes[day.nodes[day.root].children[0]];
assert(branch.children.length <= MAX_CHILDREN);
assert.equal(branch.children.length, 2);
for (const child of branch.children) assert(day.nodes[child].children.length <= MAX_CHILDREN);
const page = readDayPage(root);
assert.equal(page.children.length, 1);
assert(!JSON.stringify(page).includes("第1段状态"));
assert.equal(verifyStore(root).outcome, "PASS");
assert.equal(readCurrent(root).current_day, "2026-09-04");
});
test("a real Beijing date change closes yesterday and opens a linked new day", (t) => {
const root = roots(t);
appendEvent({ ...root, event: event(1, "2026-09-04T15:59:00Z") });
appendEvent({ ...root, event: { ...event(2, "2026-09-04T16:01:00Z"), event_id: "EVENT-NEXT-DAY" } });
const current = JSON.parse(fs.readFileSync(path.join(root.storeRoot, "CURRENT.json"), "utf8"));
assert.equal(current.current_day, "2026-09-05");
const prior = JSON.parse(fs.readFileSync(path.join(root.storeRoot, "years/2026/months/09/days/2026-09-04.json"), "utf8"));
assert.equal(prior.meta.state, "CLOSED_REAL_BEIJING_DAY_ELAPSED");
const month = JSON.parse(fs.readFileSync(current.current_month_path, "utf8"));
const dayNodes = Object.keys(month.nodes).filter((key) => key.endsWith("2026-09-04") || key.endsWith("2026-09-05"));
assert.equal(dayNodes.length, 2);
const priorNode = month.nodes[dayNodes.find((key) => key.endsWith("2026-09-04"))];
assert(priorNode.sources[0].includes("#sha256="));
assert.equal(verifyStore(root).outcome, "PASS");
});
test("year and month roots remain one-page indexes with bounded calendar groups", (t) => {
const root = roots(t);
for (const [index, date] of [[1, "2026-01-05T08:00:00+08:00"], [2, "2026-02-05T08:00:00+08:00"], [3, "2026-03-05T08:00:00+08:00"], [4, "2026-04-05T08:00:00+08:00"]]) {
const value = event(index, date);
value.event_id = `EVENT-MONTH-${index}`;
appendEvent({ ...root, event: value });
}
const year = JSON.parse(fs.readFileSync(path.join(root.storeRoot, "years/2026/index.json"), "utf8"));
assert(year.nodes[year.root].children.length <= 4);
for (const quarterPath of year.nodes[year.root].children) assert(year.nodes[quarterPath].children.length <= 3);
assert.equal(verifyStore(root).outcome, "PASS");
});
test("routing exposes summaries first and reads one exact node only", (t) => {
const root = roots(t);
appendEvent({ ...root, event: event(1) });
const routes = routeMemory({ ...root, scope: "day", id: "2026-09-04", query: "人格分形记忆" });
assert.equal(routes.length, 1);
assert.equal(Object.keys(routes[0]).sort().join(","), "path,score,summary");
const node = readMemoryNode({ ...root, scope: "day", id: "2026-09-04", nodePath: routes[0].path });
assert.equal(node.kind, "semantic_branch");
});
test("an event may explicitly retain what, where, and with whom without splitting the persona day", (t) => {
const root = roots(t);
appendEvent({ ...root, event: event(1) });
const day = JSON.parse(fs.readFileSync(path.join(root.storeRoot, "years/2026/months/09/days/2026-09-04.json"), "utf8"));
const leaf = Object.values(day.nodes).find((node) => node.kind === "event_leaf");
assert.deepEqual(leaf.activity, {
what: "记录第1段协作",
where: "Codex/ICE-CH-ZC001",
with_whom: ["ICE-GL∞"],
attention: "人格真实日记忆"
});
assert.equal(day.nodes[day.root].children.length, 1);
});
test("event writes are immutable and an id collision fails closed", (t) => {
const root = roots(t);
const original = event(1);
appendEvent({ ...root, event: original });
appendEvent({ ...root, event: original });
assert.throws(() => appendEvent({ ...root, event: { ...original, why: "changed" } }), /EVENT_ID_COLLISION/u);
});
test("an older idempotent event cannot move CURRENT backward", (t) => {
const root = roots(t);
const first = event(1, "2026-09-04T08:00:00+08:00");
const second = event(2, "2026-09-04T09:00:00+08:00");
appendEvent({ ...root, event: first });
appendEvent({ ...root, event: second });
const before = readCurrent(root);
const repeated = appendEvent({ ...root, event: first });
const after = readCurrent(root);
assert.equal(repeated.state, "IDEMPOTENT_OLDER_EVENT_NO_POINTER_MOVEMENT");
assert.equal(after.last_event_id, second.event_id);
assert.equal(after.current_pointer_sha256, before.current_pointer_sha256);
assert.throws(() => appendEvent({ ...root, event: { ...event(3, "2026-09-04T08:30:00+08:00"), event_id: "NEW-OLDER-EVENT" } }), /OUT_OF_ORDER_EVENT_REQUIRES_CURRENT_CORRECTION_EVENT/u);
});
test("store scope and symlink components fail closed", (t) => {
const root = roots(t);
assert.throws(() => appendEvent({ allowedRoot: root.allowedRoot, storeRoot: path.join(root.allowedRoot, "..", "escape"), event: event(1) }), /STORE_OUTSIDE_ALLOWED_ROOT/u);
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "zy-persona-memory-outside-"));
t.after(() => fs.rmSync(outside, { recursive: true, force: true }));
fs.symlinkSync(outside, path.join(root.allowedRoot, "linked"));
assert.throws(() => appendEvent({ allowedRoot: root.allowedRoot, storeRoot: path.join(root.allowedRoot, "linked", "store"), event: event(1) }), /SYMLINK_COMPONENT_FORBIDDEN/u);
});
test("verification detects immutable event tampering", (t) => {
const root = roots(t);
appendEvent({ ...root, event: event(1) });
const current = JSON.parse(fs.readFileSync(path.join(root.storeRoot, "CURRENT.json"), "utf8"));
fs.appendFileSync(current.last_event_path, "\n");
const checked = verifyStore(root);
assert.equal(checked.outcome, "FAIL");
assert(checked.errors.some((error) => error.includes("sha256 mismatch") || error.includes("immutable event hash mismatch")));
});