Keep creation as a living DAG and archive playable products separately.

Opening accept no longer locks the graph; session.json stays creation-only while play starts from an instance snapshot. Sidebar products nest run saves, and play reading fills the feed at a denser type size.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 00:52:05 +08:00
co-authored by Cursor
parent bd5d88abfa
commit f37388761b
22 changed files with 930 additions and 303 deletions
+36 -1
View File
@@ -64,6 +64,20 @@ describe("run snapshot store", () => {
expect(loaded!.note).toBe("测试快照");
});
it("stores instanceId on run snapshots and lists it", async () => {
const { saveRunSnapshot, loadRunSnapshot, listRunSnapshots } = await import(
"../src/book/run-snapshot-store.js"
);
const bookId = randomUUID();
const instanceId = randomUUID();
const snapshot = makeSnapshot(bookId, "巷战");
snapshot.instanceId = instanceId;
saveRunSnapshot(snapshot);
expect(loadRunSnapshot(bookId, snapshot.id)?.instanceId).toBe(instanceId);
expect(listRunSnapshots(bookId)[0].instanceId).toBe(instanceId);
});
it("listRunSnapshots returns saved items newest first", async () => {
const { saveRunSnapshot, listRunSnapshots } = await import(
"../src/book/run-snapshot-store.js"
@@ -156,6 +170,7 @@ describe("run snapshot store", () => {
runtime: { getSession: () => { slots: Record<string, unknown> }; getBlackboard: () => { write: (i: object) => void } };
};
managed.runtime.getSession().slots.startupCompleted = true;
managed.runtime.getSession().slots.designInstanceReady = true;
const bb = managed.runtime.getBlackboard();
bb.write({ tag: "情境.实验.设定", content: "囚徒困境", source: "worker" });
bb.write({ tag: "运行.事件流", content: "第1轮", source: "worker" });
@@ -168,6 +183,26 @@ describe("run snapshot store", () => {
expect(stored.blackboardItems.some((i) => i.tag === "运行.事件流")).toBe(false);
const loaded = await mgr.loadGameSnapshot(book.id, save.id);
expect(loaded.hints.some((h) => h.includes("实例"))).toBe(true);
expect(loaded.hints.some((h) => h.includes("产物"))).toBe(true);
});
it("renameGameSnapshot updates the label", async () => {
const { createBook } = await import("../src/book/store.js");
const { SessionManager } = await import("../src/server/session-manager.js");
const { loadRunSnapshot } = await import("../src/book/run-snapshot-store.js");
const book = createBook({ title: "改名测试" });
const mgr = new SessionManager();
const view = await mgr.createForBook(book.id, "world-simulator");
const managed = mgr["require"](view.id) as {
runtime: { getSession: () => { slots: Record<string, unknown> } };
};
managed.runtime.getSession().slots.startupCompleted = true;
managed.runtime.getSession().slots.designInstanceReady = true;
const save = mgr.saveGameSnapshot(view.id, "定稿", "instance");
const renamed = mgr.renameGameSnapshot(book.id, save.id, "丧尸街巷");
expect(renamed.label).toBe("丧尸街巷");
expect(loadRunSnapshot(book.id, save.id)?.label).toBe("丧尸街巷");
});
});