Restore default template binding across browser sessions

This commit is contained in:
2026-10-10 10:51:59 +08:00
parent 608229a497
commit 5db283c05a
20 changed files with 334 additions and 8 deletions
@@ -23,6 +23,16 @@ def solid_scene(color=(12, 34, 56)):
}
def test_homepage_exposes_escaped_boot_session_without_changing_rest_contract(tmp_path):
boot_id = 'boot-"<unsafe>&'
with TestClient(create_app(project_root=tmp_path, driver_kind="mock", startup_boot_id=boot_id)) as client:
response = client.get("/")
assert response.status_code == 200
assert '<meta name="device-boot-id" content="boot-&quot;&lt;unsafe&gt;&amp;">' in response.text
assert response.headers["cache-control"] == "no-store"
assert 'name="device-boot-id"' not in client.get("/api/status").text
def test_status_config_and_display_api(tmp_path):
client = TestClient(create_app(project_root=tmp_path, driver_kind="mock"))
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { BOARD_SESSION_KEY, readBoardSession, writeBoardSession, loadDefaultBoard, initializeDefaultBoard } from "../app/static/board-startup-model.js";
import {
createTemplateEditContext,
@@ -22,6 +23,7 @@ test("template edit baseline distinguishes clean, dirty, and unbound scenes", ()
assert.equal(sceneNeedsTemplateSave(context, "scene-a"), false);
assert.equal(sceneNeedsTemplateSave(context, "scene-b"), true);
assert.deepEqual(templateEditSnapshot(context, "scene-a"), {
type: "template",
id: "template-1",
name: "测试模板",
revision: "revision-1",
@@ -31,6 +33,71 @@ test("template edit baseline distinguishes clean, dirty, and unbound scenes", ()
assert.equal(templateEditSnapshot(context, "scene-b").dirty, true);
});
test("same boot restores binding and original dirty baseline; new boot or mismatched draft does not", () => {
const values = new Map();
const storage = { getItem: (key) => values.get(key), setItem: (key, value) => values.set(key, value) };
const context = createTemplateEditContext({ id: "saved", name: "默认画面", revision: "original" }, "clean-scene");
writeBoardSession(storage, "boot-a", "dirty-scene", context);
const restored = readBoardSession(storage, "dirty-scene", "boot-a");
assert.deepEqual(restored.context, context);
assert.equal(templateEditSnapshot(restored.context, "dirty-scene").dirty, true);
assert.equal(restored.context.revision, "original");
assert.equal(readBoardSession(storage, "dirty-scene", "boot-b").context, null);
assert.equal(readBoardSession(storage, "another-scene", "boot-a").context, null);
assert.equal(readBoardSession(storage, "another-scene", "boot-a").bootId, "boot-a");
values.set(BOARD_SESSION_KEY, '{"version":99}');
assert.throws(() => readBoardSession(storage, "dirty-scene", "boot-a"), /原值已保留/);
assert.equal(values.get(BOARD_SESSION_KEY), '{"version":99}');
});
test("default static loads its complete latest scene and both demos keep read-only ownership", async () => {
const staticDetail = { id: "static", name: "默认静态", revision: "r2", scene: "complete-scene" };
const calls = [];
const normal = await loadDefaultBoard(async (url) => {
calls.push(url);
return url.endsWith("default-content") ? { type: "template", id: "static" } : staticDetail;
});
assert.deepEqual(normal, { ...staticDetail, type: "template" });
assert.deepEqual(calls, ["/api/display/default-content", "/api/templates/static"]);
const staticDemo = createTemplateEditContext({ ...normal, read_only: true }, "baseline");
assert.equal(staticDemo.readOnly, true);
const demo = await loadDefaultBoard(async (url) => {
if (url.endsWith("default-content")) return { type: "animation", id: "demo", is_demo: true };
if (url.endsWith("/frames")) return { id: "demo", name: "演示动图", revision: "r1", frames: [{ id: "first" }, { id: "second" }] };
assert.equal(url, "/api/animations/demo/frames/first");
return { scene: "first-scene" };
});
assert.equal(demo.scene, "first-scene");
const context = createTemplateEditContext(demo, demo.scene);
assert.equal(context.type, "animation");
assert.equal(context.readOnly, true);
assert.equal(context.id, "demo");
assert.equal(await loadDefaultBoard(async () => ({ type: "animation", is_demo: false })), null);
});
test("startup skips protected drafts and never replaces intervening edits or failed reads", async () => {
let allowed = false, applied = 0, retained = 0, loaded = 0;
const options = {
canLoad: () => allowed,
load: async () => { loaded++; return { scene: "default" }; },
apply: () => applied++, retain: () => retained++,
};
assert.equal(await initializeDefaultBoard(options), false);
assert.equal(loaded, 0);
allowed = true;
assert.equal(await initializeDefaultBoard(options), true);
assert.equal(applied, 1);
options.load = async () => { allowed = false; return { scene: "late-default" }; };
assert.equal(await initializeDefaultBoard(options), false);
assert.equal(applied, 1);
assert.equal(retained, 1);
allowed = true;
options.load = async () => { throw Error("deleted or unavailable"); };
await assert.rejects(initializeDefaultBoard(options), /deleted or unavailable/);
assert.equal(applied, 1);
assert.equal(retained, 1);
});
test("template manager uses a monitor and unified four-row card actions", async () => {
const [html, board, templates, style] = await Promise.all([
readFile(new URL("index.html", staticRoot), "utf8"),
@@ -54,6 +121,7 @@ test("template manager uses a monitor and unified four-row card actions", async
assert.match(html, /id="template-save-choice-dialog"/);
assert.match(html, /value="current">保存到当前模板/);
assert.match(html, /value="new">另存为新模板/);
assert.match(style, /\.template-save-choice-actions \.demo-restricted-action\[aria-disabled="true"\][\s\S]*background: #050505/);
});
test("animation editing backs up and restores template edit ownership", async () => {