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"))); });