Files
buzz-sheet/lib/content/lifecycle.test.ts
T

380 lines
10 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { ContentLifecycle } from "./lifecycle";
const characterId = "17d23f26-df04-47e5-b8da-907d62b15260";
const pageId = "432cf444-dd8a-4ee5-8b21-6dc6d5d325e7";
const secondPageId = "9e52ac3a-b3fe-431b-adb6-d9cae2798705";
const sourceId = "33a89cab-d063-4551-9b13-5b3eb6ab6e61";
const mediaId = "cb661013-c489-4da0-8b1f-f7afcb327dd6";
const authorId = "admin";
function setup(now = new Date("2026-08-28T10:15:00.000Z")) {
let sequence = 0;
const store = new ContentLifecycle({
now: () => new Date(now),
idFactory: () =>
`00000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}`,
});
store.addCharacter({
id: characterId,
slug: "mizuki",
nameTh: "มิซึกิ",
nameEn: "Mizuki",
description: null,
portraitMediaId: null,
element: "ลม",
role: "สนับสนุน",
rarity: 5,
sortOrder: 0,
});
store.addDataSource({
id: sourceId,
name: "อาวุธ",
version: 1,
fields: [
{ key: "name", label: "ชื่อ", type: "text" },
{ key: "base", label: "พื้นฐาน", type: "number" },
{ key: "bonus", label: "โบนัส", type: "number" },
{
key: "score",
label: "คะแนน",
type: "number",
formula: "base + bonus",
},
],
rows: [
{
id: "06200523-1a7b-478c-8587-c54483458dac",
values: { name: "ตัวเลือก A", base: "10", bonus: "2", score: null },
},
],
});
store.addPage({
id: pageId,
characterId,
slug: "info",
title: "ข้อมูล",
navOrder: 1,
publicNote: null,
});
store.addPage({
id: secondPageId,
characterId,
slug: "combo",
title: "คอมโบ",
navOrder: 0,
publicNote: null,
});
return store;
}
function rankingBlock() {
return {
id: "1e0b3229-549c-4897-a765-50c56d26b414",
type: "ranking",
schemaVersion: 1,
sortOrder: 0,
config: {
title: "อันดับอาวุธ",
dataSourceId: sourceId,
labelField: "name",
valueField: "score",
higherIsBetter: true,
},
};
}
describe("content lifecycle", () => {
it("starts new content hidden and requires both visibility flags", () => {
const store = setup();
expect(store.getDirectory()).toEqual([]);
store.setCharacterVisibility(characterId, true);
expect(store.getDirectory()).toEqual([]);
const saved = store.savePage({
pageId,
expectedVersion: 1,
authorId,
title: "ข้อมูล",
slug: "info",
visible: true,
publicNote: "อัปเดตล่าสุดสำหรับเวอร์ชัน 1.0",
blocks: [rankingBlock()],
});
expect(saved.status).toBe("accepted");
expect(store.getDirectory()).toMatchObject([
{ slug: "mizuki", firstPageSlug: "info" },
]);
expect(store.resolvePublicRoute("mizuki", "info")).toMatchObject({
type: "page",
snapshot: { page: { publicNote: "อัปเดตล่าสุดสำหรับเวอร์ชัน 1.0" } },
});
});
it("orders pages and redirects a character route to the first visible page", () => {
const store = setup();
store.setCharacterVisibility(characterId, true);
store.savePage({
pageId,
expectedVersion: 1,
authorId,
title: "ข้อมูล",
slug: "info",
visible: true,
blocks: [],
});
store.savePage({
pageId: secondPageId,
expectedVersion: 1,
authorId,
title: "คอมโบ",
slug: "combo",
visible: true,
blocks: [],
});
expect(store.resolvePublicRoute("mizuki")).toEqual({
type: "redirect",
location: "/mizuki/combo",
status: 308,
});
});
it("creates permanent aliases after custom character and page slug changes", () => {
const store = setup();
store.setCharacterVisibility(characterId, true);
store.savePage({
pageId,
expectedVersion: 1,
authorId,
title: "ข้อมูล",
slug: "build-info",
visible: true,
blocks: [],
});
expect(store.resolvePublicRoute("mizuki", "info")).toEqual({
type: "redirect",
location: "/mizuki/build-info",
status: 308,
});
store.renameCharacter(characterId, "mizuki-guide");
expect(store.resolvePublicRoute("mizuki", "build-info")).toEqual({
type: "redirect",
location: "/mizuki-guide/build-info",
status: 308,
});
});
it("rejects stale concurrent changes and invalid blocks", () => {
const store = setup();
const accepted = store.savePage({
pageId,
expectedVersion: 1,
authorId,
title: "ข้อมูลใหม่",
slug: "info",
visible: false,
blocks: [],
});
expect(accepted.status).toBe("accepted");
expect(
store.savePage({
pageId,
expectedVersion: 1,
authorId,
title: "เขียนทับ",
slug: "info",
visible: false,
blocks: [],
}),
).toEqual({ status: "conflict", currentVersion: 2 });
expect(
store.savePage({
pageId,
expectedVersion: 2,
authorId,
title: "ข้อมูลใหม่",
slug: "info",
visible: false,
blocks: [
{
id: "c9c1fc08-8180-439b-8962-26fe1c8977b1",
type: "callout",
schemaVersion: 1,
sortOrder: 0,
config: { body: "", tone: "unknown" },
},
],
}).status,
).toBe("validation-error");
});
it("versions global sources independently and refreshes only latest consumers", () => {
const store = setup();
const saved = store.savePage({
pageId,
expectedVersion: 1,
authorId,
title: "ข้อมูล",
slug: "info",
visible: false,
blocks: [rankingBlock()],
});
expect(saved.status).toBe("accepted");
const beforeEvents = store.getOutbox().length;
const source = store.saveDataSource({
id: sourceId,
expectedVersion: 1,
name: "อาวุธ",
fields: [
{ key: "value", label: "ค่า", type: "number" },
{
key: "broken",
label: "สูตรที่ผิด",
type: "number",
formula: "value / 0",
},
],
rows: [
{
id: "560bb5b4-f4c7-46ba-82d5-e3dca8217b0a",
values: { value: "4", broken: null },
},
],
});
expect(source.status).toBe("accepted");
expect(store.getOutbox().slice(beforeEvents)).toMatchObject([
{ eventType: "data-source.updated", payload: { version: 2 } },
{ eventType: "page.updated", aggregateId: pageId },
]);
});
it("coalesces hourly checkpoints, keeps named revisions, and expires only checkpoints", () => {
const store = setup();
store.savePage({
pageId,
expectedVersion: 1,
authorId,
title: "ครั้งที่ 1",
slug: "info",
visible: false,
blocks: [],
now: new Date("2026-08-28T10:05:00.000Z"),
});
store.createNamedRevision(
pageId,
"เวอร์ชันเปิดตัว",
authorId,
new Date("2026-08-28T10:10:00.000Z"),
);
store.savePage({
pageId,
expectedVersion: 2,
authorId,
title: "ครั้งที่ 2",
slug: "info",
visible: false,
blocks: [],
now: new Date("2026-08-28T10:55:00.000Z"),
});
expect(store.listRevisions(pageId)).toHaveLength(2);
expect(
store.listRevisions(pageId).filter((revision) => revision.kind === "checkpoint"),
).toHaveLength(1);
const expired = store.expireCheckpoints(
new Date("2026-09-28T11:00:00.000Z"),
);
expect(expired).toHaveLength(1);
expect(store.listRevisions(pageId)).toMatchObject([
{ kind: "named", label: "เวอร์ชันเปิดตัว", expiresAt: null },
]);
});
it("restores a new page version pinned to historical data and can use latest again", () => {
const store = setup();
store.savePage({
pageId,
expectedVersion: 1,
authorId,
title: "เวอร์ชันเดิม",
slug: "info",
visible: false,
blocks: [rankingBlock()],
});
const revision = store.createNamedRevision(pageId, "ก่อนปรับสูตร", authorId)!;
store.saveDataSource({
id: sourceId,
expectedVersion: 1,
name: "อาวุธรุ่นใหม่",
fields: [{ key: "value", label: "ค่า", type: "number" }],
rows: [],
});
store.savePage({
pageId,
expectedVersion: 2,
authorId,
title: "เวอร์ชันใหม่",
slug: "info",
visible: false,
blocks: [rankingBlock()],
});
const restored = store.restoreRevision(revision.id, 3, authorId);
expect(restored.status).toBe("accepted");
if (restored.status === "accepted") {
expect(restored.value.page.version).toBe(4);
expect(restored.value.page.title).toBe("เวอร์ชันเดิม");
expect(restored.value.versionVector[sourceId]).toBe(1);
}
const latest = store.useLatestDataSource(pageId, sourceId, 4, authorId);
expect(latest.status).toBe("accepted");
if (latest.status === "accepted") {
expect(latest.value.page.version).toBe(5);
expect(latest.value.versionVector[sourceId]).toBe(2);
}
});
it("retains media for current content or revisions and exposes it only from visible snapshots", () => {
const store = setup();
store.setCharacterVisibility(characterId, true);
const imageBlock = {
id: "949499c7-3162-4ff5-87f0-f90711a97e6f",
type: "image",
schemaVersion: 1,
sortOrder: 0,
config: { mediaId, alt: "ภาพตัวอย่าง", fit: "cover" },
};
store.savePage({
pageId,
expectedVersion: 1,
authorId,
title: "ข้อมูล",
slug: "info",
visible: true,
blocks: [imageBlock],
});
store.createNamedRevision(pageId, "มีภาพ", authorId);
expect(store.isMediaPublic(mediaId)).toBe(true);
expect(store.isMediaRetained(mediaId)).toBe(true);
store.savePage({
pageId,
expectedVersion: 2,
authorId,
title: "ข้อมูล",
slug: "info",
visible: true,
blocks: [],
});
expect(store.isMediaPublic(mediaId)).toBe(false);
expect(store.isMediaRetained(mediaId)).toBe(true);
});
});