172 lines
8.4 KiB
JavaScript
172 lines
8.4 KiB
JavaScript
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")));
|
||
});
|