import test from "node:test"; import assert from "node:assert/strict"; import { ProjectStore } from "../engine/store.ts"; import { validateProject } from "../engine/schema.ts"; import { project, node, scene, findNode } from "./fixtures.ts"; const add = ( id: string, parentId: string | null = null, components: Record = {}, ) => ({ op: "node.create", args: { entity: node(id, parentId, components) } }); const rename = (name: string) => ({ op: "project.rename", args: { name } }); test("a failed multi-command transaction rolls back every preceding mutation", () => { const store = new ProjectStore(project([node("existing")])); const before = structuredClone(store.project); assert.throws(() => store.transaction({ commands: [ rename("Should not persist"), add("new_object"), { op: "node.reparent", args: { id: "existing", parentId: "missing" } }, ], expectedRevision: 0, }), ); assert.deepEqual(store.project, before); }); test("stale revisions cannot overwrite a newer editor change", () => { const store = new ProjectStore(project()); store.transaction({ commands: [rename("Current")], expectedRevision: 0 }); assert.equal(store.project.revision, 1); assert.throws(() => store.transaction({ commands: [rename("Stale")], expectedRevision: 0 }), ); assert.equal(store.project.name, "Current"); assert.equal(store.project.revision, 1); }); test("undo and redo restore complete edits with monotonic revisions", () => { const store = new ProjectStore(project()); store.transaction({ commands: [add("root"), add("child", "root")], expectedRevision: 0, }); assert.equal(scene(store.project).entities.length, 2); const revision = store.project.revision; store.undo(); assert.equal(scene(store.project).entities.length, 0); assert.ok(store.project.revision > revision); const undoneRevision = store.project.revision; store.redo(); assert.equal(findNode(store.project, "child").parentId, "root"); assert.ok(store.project.revision > undoneRevision); validateProject(store.project); }); test("a retry of an already accepted request cannot duplicate objects", () => { const store = new ProjectStore(project()); const tx = { commands: [add("only_once")], expectedRevision: 0, requestId: "request_123", }; store.transaction(tx); store.transaction(tx); assert.equal(store.project.revision, 1); assert.equal(scene(store.project).entities.length, 1); }); test("a fresh edit after undo discards redo history", () => { const store = new ProjectStore(project()); store.transaction({ commands: [rename("First")] }); store.transaction({ commands: [rename("Second")] }); store.undo(); store.transaction({ commands: [rename("Branch")] }); const before = structuredClone(store.project); try { store.redo(); } catch {} assert.deepEqual(store.project, before); }); test("hierarchy cycles are rejected atomically, including indirect cycles", () => { const store = new ProjectStore( project([node("a"), node("b", "a"), node("c", "b")]), ); const before = structuredClone(store.project); assert.throws(() => store.transaction({ commands: [{ op: "node.reparent", args: { id: "a", parentId: "c" } }], }), ); assert.deepEqual(store.project, before); assert.throws(() => store.transaction({ commands: [{ op: "node.reparent", args: { id: "b", parentId: "b" } }], }), ); }); test("deleting a hierarchy root removes its full subtree but preserves siblings", () => { const store = new ProjectStore( project([node("a"), node("b", "a"), node("c", "b"), node("survivor")]), ); store.transaction({ commands: [{ op: "node.delete", args: { id: "a" } }] }); assert.deepEqual( scene(store.project).entities.map((e: any) => e.id), ["survivor"], ); store.undo(); assert.equal(scene(store.project).entities.length, 4); }); test("duplicate remaps internal hierarchy, camera and entity properties only", () => { const p = project([ node("root"), node("child", "root"), node("outside"), node("camera", "root", { camera: { targetId: "child", offset: [0, 10, -8] }, }), node("behavior", "root", { script: { scriptId: "script_refs", params: { target: "child", external: "outside", literal: "child" }, }, }), ]); p.scripts = [ { id: "script_refs", name: "References", source: "({ update() {} })", fields: { target: { type: "entity", default: "" }, external: { type: "entity", default: "" }, literal: { type: "string", default: "" }, }, }, ]; const store = new ProjectStore(p); const oldIds = new Set(scene(store.project).entities.map((e: any) => e.id)); store.transaction({ commands: [{ op: "node.duplicate", args: { id: "root" } }], }); const copies = scene(store.project).entities.filter( (e: any) => !oldIds.has(e.id), ); assert.equal(copies.length, 4); const copyRoot = copies.find((e: any) => e.parentId === null); assert.ok(copyRoot); const copyCamera = copies.find((e: any) => e.components.camera), copyBehavior = copies.find((e: any) => e.components.script); const copyChild = copies.find( (e: any) => e !== copyRoot && !e.components.camera && !e.components.script, ); assert.ok(copyChild); for (const e of copies.filter((e: any) => e !== copyRoot)) assert.equal(e.parentId, copyRoot.id); assert.equal(copyCamera.components.camera.targetId, copyChild.id); assert.equal(copyBehavior.components.script.params.target, copyChild.id); assert.equal(copyBehavior.components.script.params.external, "outside"); assert.equal(copyBehavior.components.script.params.literal, "child"); validateProject(store.project); }); test("schema rejects duplicate node IDs, dangling parents and prototype keys", () => { assert.throws(() => validateProject(project([node("same"), node("same")]))); assert.throws(() => validateProject(project([node("child", "missing")]))); const p = project([node("a")]); p.scenes[0].entities[0].components = JSON.parse( '{"data":{"__proto__":{"admin":true}}}', ); assert.throws(() => validateProject(p)); assert.equal(({} as any).admin, undefined); }); test("component type cannot alter the components object prototype", () => { const store = new ProjectStore(project([node("safe")])); const before = structuredClone(store.project); assert.throws(() => store.transaction({ commands: [ { op: "component.set", args: { id: "safe", type: "__proto__", value: { mesh: { type: "box", size: [1, 1, 1] } }, }, }, ], }), ); assert.deepEqual(store.project, before); assert.equal( Object.getPrototypeOf(findNode(store.project, "safe").components), Object.prototype, ); }); test("prefab instances remap internal references independently from their source", () => { const p = project([ node("root"), node("child", "root"), node("camera", "root", { camera: { targetId: "child", offset: [0, 10, -8] }, }), node("behavior", "root", { script: { scriptId: "target_default", params: {} }, }), ]); p.scripts = [ { id: "target_default", name: "Default target", source: "({ update() {} })", fields: { target: { type: "entity", default: "child" } }, }, ]; const store = new ProjectStore(p); store.transaction({ commands: [ { op: "prefab.create", args: { id: "root", assetId: "prefab_test" } }, ], }); const originalIds = new Set( scene(store.project).entities.map((e: any) => e.id), ); store.transaction({ commands: [ { op: "prefab.instantiate", args: { assetId: "prefab_test", position: [4, 0, 2] }, }, ], }); const copies = scene(store.project).entities.filter( (e: any) => !originalIds.has(e.id), ); assert.equal(copies.length, 4); const root = copies.find((e: any) => e.parentId === null), child = copies.find( (e: any) => e.parentId !== null && !e.components.camera && !e.components.script, ); assert.deepEqual(root.transform.position, [4, 0, 2]); assert.equal( copies.find((e: any) => e.components.camera).components.camera.targetId, child.id, ); assert.equal( copies.find((e: any) => e.components.script).components.script.params .target, child.id, ); assert.equal( findNode(store.project, "camera").components.camera.targetId, "child", ); assert.equal(store.project.scripts[0].fields.target.default, "child"); validateProject(store.project); }); test("deleting an in-use asset cannot leave broken scene references", () => { const p = project([node("model", null, { mesh: { assetId: "triangle" } })]); p.assets = [ { id: "triangle", name: "Triangle", kind: "geometry", geometry: { positions: [0, 0, 0, 1, 0, 0, 0, 1, 0], indices: [0, 1, 2] }, }, ]; const store = new ProjectStore(p), before = structuredClone(store.project); assert.throws(() => store.transaction({ commands: [{ op: "asset.delete", args: { id: "triangle" } }], }), ); assert.deepEqual(store.project, before); });