import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { buildingSchema, sceneSchema } from "@nengine/core-schema"; import type { SceneDef, SceneEntity } from "@nengine/core-schema"; import { parse, stringify } from "yaml"; import { describe, expect, it } from "vitest"; import { ANIMAL_COLOR, DEFAULT_SCENE_ID, NPC_COLOR, addAnimal, addBuilding, addNpc, canRedo, canUndo, clearHistory, createSceneModel, entityAt, entityColor, entityFootprint, entityIndexAt, markSaved, moveEntity, redo, removeEntity, sceneFitsAfterResize, sceneFitsIn, serialize, syncMapSize, undo, } from "../src/renderer/scene-model.js"; import type { SceneFootprint, SceneModel } from "../src/renderer/scene-model.js"; const fixturesDir = fileURLToPath(new URL("./fixtures", import.meta.url)); const FOOTPRINTS: Record = { house: { w: 2, h: 2 }, shed: { w: 2, h: 2 }, well: { w: 1, h: 1 }, }; function model(mapWidth = 10, mapHeight = 10, footprints: Record = FOOTPRINTS): SceneModel { const m = createSceneModel( { id: "farm", entities: [] }, { mapWidth, mapHeight, footprints }, ); if (m === null) throw new Error("expected a valid model"); return m; } function entities(m: SceneModel): SceneEntity[] { return m.def.entities; } describe("createSceneModel", () => { it("builds a model from a valid def without mutating the input", () => { const def: SceneDef = { id: "farm", entities: [{ type: "building", buildingId: "well", x: 1, y: 1 }], }; const snapshot = JSON.stringify(def); const m = createSceneModel(def, { mapWidth: 10, mapHeight: 10, footprints: FOOTPRINTS }); expect(m).not.toBeNull(); if (m === null) throw new Error("expected a valid model"); expect(m.dirty).toBe(false); addNpc(m, "mayor", 3, 3); expect(JSON.stringify(def)).toBe(snapshot); expect(m.dirty).toBe(true); }); it("rejects corrupt defs", () => { expect(createSceneModel({ id: 42 }, { mapWidth: 10, mapHeight: 10 })).toBeNull(); expect(createSceneModel({ id: "farm", entities: [{ type: "npc", x: 1 }] }, { mapWidth: 10, mapHeight: 10 })).toBeNull(); expect( createSceneModel({ id: "farm", entities: [{ type: "building", buildingId: "well", x: -1, y: 0 }] }, { mapWidth: 10, mapHeight: 10 }), ).toBeNull(); expect( createSceneModel({ id: "farm", entities: [{ type: "silo", x: 1, y: 1 }] }, { mapWidth: 10, mapHeight: 10 }), ).toBeNull(); }); it("accepts an empty entities array", () => { const m = createSceneModel({ id: "farm", entities: [] }, { mapWidth: 10, mapHeight: 10 }); expect(m?.def.entities).toEqual([]); }); it("creates a new empty scene from undefined input with the default id", () => { const m = createSceneModel(undefined, { mapWidth: 10, mapHeight: 10 }); expect(m).not.toBeNull(); expect(m?.def).toEqual({ id: DEFAULT_SCENE_ID, entities: [] }); expect(m?.dirty).toBe(false); }); it("honours a custom default id for new scenes", () => { const m = createSceneModel(undefined, { mapWidth: 10, mapHeight: 10, defaultId: "farm" }); expect(m?.def.id).toBe("farm"); }); it("deep-clones the footprints table", () => { const footprints: Record = { house: { w: 2, h: 2 } }; const m = model(10, 10, footprints); m.footprints.house = { w: 5, h: 5 }; expect(footprints.house).toEqual({ w: 2, h: 2 }); }); }); describe("addBuilding", () => { it("places a building and marks dirty", () => { const m = model(); expect(m.dirty).toBe(false); const result = addBuilding(m, "well", 3, 4); expect(result).toEqual({ ok: true }); expect(entities(m)).toEqual([{ type: "building", buildingId: "well", x: 3, y: 4 }]); expect(m.dirty).toBe(true); }); it("clamps out-of-bounds coordinates footprint-aware", () => { const m = model(10, 10); addBuilding(m, "house", 9, 9); expect(entities(m)[0]).toEqual({ type: "building", buildingId: "house", x: 8, y: 8 }); addBuilding(m, "well", -3, -3); expect(entities(m)[1]).toEqual({ type: "building", buildingId: "well", x: 0, y: 0 }); }); it("rejects non-integer coordinates", () => { const m = model(); expect(addBuilding(m, "well", 1.5, 1)).toEqual({ ok: false, reason: expect.stringContaining("integer") }); expect(addBuilding(m, "well", Number.NaN, 1)).toEqual({ ok: false, reason: expect.stringContaining("integer") }); expect(entities(m)).toHaveLength(0); expect(m.dirty).toBe(false); }); it("rejects an unknown building id", () => { const m = model(); const result = addBuilding(m, "silo", 1, 1); expect(result).toEqual({ ok: false, reason: expect.stringContaining('unknown building "silo"') }); expect(entities(m)).toHaveLength(0); }); it("rejects an empty building id", () => { const m = model(); expect(addBuilding(m, "", 1, 1)).toEqual({ ok: false, reason: expect.stringContaining("must not be empty") }); }); it("rejects overlap with another building's footprint", () => { const m = model(); addBuilding(m, "house", 0, 0); const result = addBuilding(m, "well", 1, 1); expect(result.ok).toBe(false); if (!result.ok) expect(result.reason).toContain("overlaps"); expect(entities(m)).toHaveLength(1); expect(m.dirty).toBe(true); }); it("rejects a duplicate building id at the same coordinates", () => { const m = model(); addBuilding(m, "well", 2, 2); const result = addBuilding(m, "well", 2, 2); expect(result).toEqual({ ok: false, reason: expect.stringContaining('duplicate building "well" at (2,2)') }); expect(entities(m)).toHaveLength(1); }); it("rejects overlap with a placed npc or animal", () => { const m = model(); addNpc(m, "mayor", 2, 2); expect(addBuilding(m, "house", 1, 1).ok).toBe(false); addAnimal(m, "chicken", 5, 5); expect(addBuilding(m, "house", 4, 4).ok).toBe(false); expect(entities(m).filter((e) => e.type === "building")).toHaveLength(0); }); it("allows adjacent placement that only touches edges", () => { const m = model(); addBuilding(m, "house", 0, 0); const result = addBuilding(m, "well", 2, 0); expect(result).toEqual({ ok: true }); expect(entities(m)).toHaveLength(2); }); it("rejects a footprint larger than the map", () => { const m = model(1, 1); const result = addBuilding(m, "house", 0, 0); expect(result.ok).toBe(false); if (!result.ok) expect(result.reason).toContain("does not fit"); expect(entities(m)).toHaveLength(0); }); }); describe("addNpc", () => { it("places an npc with optional dialogue and label", () => { const m = model(); const result = addNpc(m, "mayor", 4, 4, { dialogue: "intro", label: "Mayor" }); expect(result).toEqual({ ok: true }); expect(entities(m)).toEqual([ { type: "npc", npcId: "mayor", x: 4, y: 4, dialogue: "intro", label: "Mayor" }, ]); expect(m.dirty).toBe(true); }); it("omits empty dialogue and label", () => { const m = model(); addNpc(m, "mayor", 4, 4, { dialogue: "", label: "" }); expect(entities(m)).toEqual([{ type: "npc", npcId: "mayor", x: 4, y: 4 }]); }); it("rejects a duplicate npc id", () => { const m = model(); addNpc(m, "mayor", 1, 1); const result = addNpc(m, "mayor", 5, 5); expect(result).toEqual({ ok: false, reason: expect.stringContaining('duplicate npc id "mayor"') }); expect(entities(m)).toHaveLength(1); }); it("clamps out-of-bounds coordinates", () => { const m = model(10, 10); addNpc(m, "mayor", -5, 99); expect(entities(m)[0]).toEqual({ type: "npc", npcId: "mayor", x: 0, y: 9 }); }); it("rejects placement inside a building footprint", () => { const m = model(); addBuilding(m, "house", 0, 0); const result = addNpc(m, "mayor", 1, 1); expect(result.ok).toBe(false); if (!result.ok) expect(result.reason).toContain("overlaps"); expect(entities(m)).toHaveLength(1); }); }); describe("addAnimal", () => { it("places an animal and marks dirty", () => { const m = model(); const result = addAnimal(m, "chicken", 3, 3); expect(result).toEqual({ ok: true }); expect(entities(m)).toEqual([{ type: "animal", animalId: "chicken", x: 3, y: 3 }]); expect(m.dirty).toBe(true); }); it("clamps out-of-bounds coordinates", () => { const m = model(10, 10); addAnimal(m, "chicken", 10, -2); expect(entities(m)[0]).toEqual({ type: "animal", animalId: "chicken", x: 9, y: 0 }); }); it("allows multiple animals with the same animal id", () => { const m = model(); addAnimal(m, "chicken", 1, 1); addAnimal(m, "chicken", 3, 3); expect(entities(m)).toHaveLength(2); }); it("rejects overlap with a building", () => { const m = model(); addBuilding(m, "house", 0, 0); expect(addAnimal(m, "chicken", 0, 0).ok).toBe(false); expect(entities(m)).toHaveLength(1); }); }); describe("removeEntity", () => { it("removes the entity at the index and marks dirty", () => { const m = model(); addBuilding(m, "well", 1, 1); addNpc(m, "mayor", 3, 3); const result = removeEntity(m, 0); expect(result).toEqual({ ok: true }); expect(entities(m)).toHaveLength(1); expect(entities(m)[0]?.type).toBe("npc"); expect(m.dirty).toBe(true); }); it("rejects an out-of-range index", () => { const m = model(); expect(removeEntity(m, 0)).toEqual({ ok: false, reason: expect.stringContaining("no entity at index") }); addAnimal(m, "chicken", 1, 1); expect(removeEntity(m, 5)).toEqual({ ok: false, reason: expect.stringContaining("no entity at index") }); expect(entities(m)).toHaveLength(1); }); }); describe("moveEntity", () => { it("moves an entity and marks dirty", () => { const m = model(); addNpc(m, "mayor", 1, 1); const result = moveEntity(m, 0, 4, 5); expect(result).toEqual({ ok: true }); expect(entities(m)[0]).toEqual({ type: "npc", npcId: "mayor", x: 4, y: 5 }); expect(m.dirty).toBe(true); }); it("re-clamps out-of-bounds targets footprint-aware", () => { const m = model(10, 10); addBuilding(m, "house", 1, 1); moveEntity(m, 0, -5, 99); expect(entities(m)[0]).toEqual({ type: "building", buildingId: "house", x: 0, y: 8 }); }); it("rejects a move into another entity's footprint without mutating", () => { const m = model(); addBuilding(m, "house", 0, 0); addNpc(m, "mayor", 6, 6); const before = JSON.stringify(m.def); const result = moveEntity(m, 0, 6, 6); expect(result.ok).toBe(false); if (!result.ok) expect(result.reason).toContain("overlaps"); expect(JSON.stringify(m.def)).toBe(before); expect(m.dirty).toBe(true); }); it("rejects a move into its own footprint (same entity excluded from overlap)", () => { const m = model(); addBuilding(m, "house", 0, 0); addNpc(m, "mayor", 6, 6); const result = moveEntity(m, 0, 1, 1); expect(result).toEqual({ ok: true }); expect(entities(m)[0]).toEqual({ type: "building", buildingId: "house", x: 1, y: 1 }); }); it("rejects an invalid index and non-integer coordinates", () => { const m = model(); expect(moveEntity(m, 0, 1, 1)).toEqual({ ok: false, reason: expect.stringContaining("no entity at index") }); addAnimal(m, "chicken", 1, 1); expect(moveEntity(m, 0, 2.5, 1)).toEqual({ ok: false, reason: expect.stringContaining("integer") }); expect(entities(m)[0]).toEqual({ type: "animal", animalId: "chicken", x: 1, y: 1 }); }); }); describe("serialize", () => { it("roundtrips a valid scene as a fresh object", () => { const m = model(); addBuilding(m, "house", 0, 0); addNpc(m, "mayor", 3, 3, { dialogue: "intro" }); const out = serialize(m); expect(out).not.toBeNull(); expect(out).toEqual({ id: "farm", entities: [ { type: "building", buildingId: "house", x: 0, y: 0 }, { type: "npc", npcId: "mayor", x: 3, y: 3, dialogue: "intro" }, ], }); expect(out).not.toBe(m.def); expect(out?.entities[0]).not.toBe(m.def.entities[0]); expect(sceneSchema.safeParse(out as SceneDef).success).toBe(true); }); it("rejects an invalid working copy", () => { const m = model(); addAnimal(m, "chicken", 1, 1); const invalid = entities(m)[0] as { animalId: string }; delete invalid.animalId; expect(serialize(m)).toBeNull(); }); it("still serializes after a rejected op (invalid ops never corrupt)", () => { const m = model(); addBuilding(m, "house", 0, 0); addBuilding(m, "well", 1, 1); addNpc(m, "mayor", 1, 1); const out = serialize(m); expect(out).not.toBeNull(); expect(out?.entities).toHaveLength(1); }); it("serializes an empty scene", () => { const m = model(); const out = serialize(m); expect(out).toEqual({ id: "farm", entities: [] }); }); }); describe("entityAt / entityIndexAt", () => { it("finds entities by any footprint tile", () => { const m = model(); addBuilding(m, "house", 0, 0); addNpc(m, "mayor", 5, 5); expect(entityIndexAt(m, 0, 0)).toBe(0); expect(entityIndexAt(m, 1, 1)).toBe(0); expect(entityIndexAt(m, 0, 1)).toBe(0); expect(entityIndexAt(m, 5, 5)).toBe(1); expect(entityIndexAt(m, 3, 3)).toBe(-1); expect(entityAt(m, 1, 1)?.type).toBe("building"); expect(entityAt(m, 3, 3)).toBeUndefined(); }); it("returns the topmost entity for overlapping loaded defs", () => { const m = model(); addBuilding(m, "house", 0, 0); const raw = m.def as SceneDef; raw.entities.push({ type: "npc", npcId: "mayor", x: 1, y: 1 }); expect(entityIndexAt(m, 1, 1)).toBe(1); expect(entityAt(m, 1, 1)?.type).toBe("npc"); }); }); describe("entityFootprint / entityColor", () => { it("uses the footprint table for buildings and 1x1 for others", () => { const m = model(); addBuilding(m, "house", 0, 0); addNpc(m, "mayor", 5, 5); addAnimal(m, "chicken", 8, 8); expect(entityFootprint(m, entities(m)[0] as SceneEntity)).toEqual({ w: 2, h: 2 }); expect(entityFootprint(m, entities(m)[1] as SceneEntity)).toEqual({ w: 1, h: 1 }); expect(entityFootprint(m, entities(m)[2] as SceneEntity)).toEqual({ w: 1, h: 1 }); }); it("falls back to 1x1 for a building with no footprint entry", () => { const m = model(10, 10, {}); const raw = m.def as SceneDef; raw.entities.push({ type: "building", buildingId: "silo", x: 0, y: 0 }); expect(entityFootprint(m, entities(m)[0] as SceneEntity)).toEqual({ w: 1, h: 1 }); }); it("colors npc amber, animal red, buildings a stable hash hue", () => { expect(entityColor({ type: "npc", npcId: "mayor", x: 0, y: 0 })).toBe(NPC_COLOR); expect(entityColor({ type: "animal", animalId: "chicken", x: 0, y: 0 })).toBe(ANIMAL_COLOR); const shed = entityColor({ type: "building", buildingId: "shed", x: 0, y: 0 }); const shedAgain = entityColor({ type: "building", buildingId: "shed", x: 0, y: 0 }); expect(shed).toBe(shedAgain); expect(shed).toMatch(/^hsl\(\d+ 45% 45%\)$/); }); }); describe("dirty lifecycle", () => { it("tracks edits and clears on markSaved", () => { const m = model(); expect(m.dirty).toBe(false); addBuilding(m, "well", 1, 1); expect(m.dirty).toBe(true); markSaved(m); expect(m.dirty).toBe(false); moveEntity(m, 0, 2, 2); expect(m.dirty).toBe(true); markSaved(m); removeEntity(m, 0); expect(m.dirty).toBe(true); }); }); describe("real scene fixture end-to-end", () => { it("edits a real scene through read → parse → model → place → serialize → stringify → parse", async () => { const sceneText = await readFile(join(fixturesDir, "scene-farm.yaml"), "utf8"); const shedText = await readFile(join(fixturesDir, "buildings", "shed.yaml"), "utf8"); const wellText = await readFile(join(fixturesDir, "buildings", "well.yaml"), "utf8"); const footprints: Record = {}; for (const text of [shedText, wellText]) { const def = buildingSchema.safeParse(parse(text)); expect(def.success).toBe(true); if (def.success) footprints[def.data.id] = def.data.footprint; } const m = createSceneModel(parse(sceneText), { mapWidth: 16, mapHeight: 16, footprints }); expect(m).not.toBeNull(); if (m === null) throw new Error("fixture must be a valid scene"); expect(sceneSchema.safeParse(m.def).success).toBe(true); expect(m.def.id).toBe("farm-yard"); expect(entities(m)).toHaveLength(3); const placed = addBuilding(m, "well", 8, 8); expect(placed).toEqual({ ok: true }); const npc = addNpc(m, "farmer", 10, 10, { label: "Farmer" }); expect(npc).toEqual({ ok: true }); expect(addBuilding(m, "shed", 0, 0).ok).toBe(false); const out = serialize(m); expect(out).not.toBeNull(); if (out === null) throw new Error("edited fixture must serialize"); const yaml = stringify(out); const reparsed = parse(yaml); expect(reparsed).toEqual(out); expect(sceneSchema.safeParse(reparsed).success).toBe(true); const m2 = createSceneModel(reparsed, { mapWidth: 16, mapHeight: 16, footprints }); expect(m2).not.toBeNull(); expect(m2?.def.entities).toHaveLength(5); expect(m2?.def.entities[3]).toEqual({ type: "building", buildingId: "well", x: 8, y: 8 }); expect(m2?.def.entities[4]).toEqual({ type: "npc", npcId: "farmer", x: 10, y: 10, label: "Farmer" }); expect(await readFile(join(fixturesDir, "scene-farm.yaml"), "utf8")).toBe(sceneText); }); }); describe("undo/redo", () => { it("addBuilding/addNpc/addAnimal undo removes the entity and redo restores it", () => { const m = model(); addBuilding(m, "house", 4, 4); addNpc(m, "mayor", 0, 0, { dialogue: "intro" }); addAnimal(m, "chicken", 7, 7); expect(entities(m).length).toBe(3); expect(undo(m)).toBe(true); expect(entities(m).length).toBe(2); expect(undo(m)).toBe(true); expect(entities(m).length).toBe(1); expect(undo(m)).toBe(true); expect(entities(m).length).toBe(0); expect(m.dirty).toBe(false); expect(redo(m)).toBe(true); expect(entities(m)[0]).toEqual({ type: "building", buildingId: "house", x: 4, y: 4 }); expect(redo(m)).toBe(true); expect(entities(m)[1]).toEqual({ type: "npc", npcId: "mayor", x: 0, y: 0, dialogue: "intro" }); expect(m.dirty).toBe(true); }); it("removeEntity undo re-inserts the snapshot at the original index", () => { const m = model(); addBuilding(m, "house", 4, 4); addNpc(m, "mayor", 0, 0); expect(removeEntity(m, 0)).toEqual({ ok: true }); expect(entities(m).map((e) => e.type)).toEqual(["npc"]); expect(undo(m)).toBe(true); expect(entities(m).map((e) => e.type)).toEqual(["building", "npc"]); expect(entities(m)[0]).toEqual({ type: "building", buildingId: "house", x: 4, y: 4 }); }); it("history entries snapshot entities: a later move never corrupts redo", () => { const m = model(); addBuilding(m, "house", 4, 4); // add entry snapshots house at (4,4) expect(moveEntity(m, 0, 7, 7)).toEqual({ ok: true }); // live house → (7,7) expect(undo(m)).toBe(true); // undo the move expect(undo(m)).toBe(true); // undo the add expect(entities(m)).toHaveLength(0); expect(redo(m)).toBe(true); // redo the add — restores the SNAPSHOT coords, not (7,7) expect(entities(m)[0]).toEqual({ type: "building", buildingId: "house", x: 4, y: 4 }); }); it("moveEntity undo restores the previous coordinates", () => { const m = model(); addNpc(m, "mayor", 2, 2); expect(moveEntity(m, 0, 8, 8)).toEqual({ ok: true }); expect(entities(m)[0].x).toBe(8); expect(undo(m)).toBe(true); expect(entities(m)[0]).toEqual({ x: 2, y: 2, type: "npc", npcId: "mayor" }); }); it("rejected ops never enter the stack", () => { const m = model(); addBuilding(m, "house", 4, 4); expect(addBuilding(m, "house", 4, 4)).toEqual({ ok: false, reason: expect.stringContaining("duplicate") }); expect(addNpc(m, "mayor", 0, 0)); expect(addNpc(m, "mayor", 1, 1)).toEqual({ ok: false, reason: expect.stringContaining("duplicate npc") }); expect(moveEntity(m, 99, 0, 0)).toEqual({ ok: false, reason: expect.stringContaining("index") }); expect(m.history.length).toBe(2); // only the two successful adds }); it("markSaved is a checkpoint and undo-all returns clean", () => { const m = model(); addNpc(m, "mayor", 0, 0); undo(m); markSaved(m); expect(m.history.length).toBe(0); expect(m.future.length).toBe(0); expect(m.dirty).toBe(false); addNpc(m, "mia", 1, 1); addNpc(m, "cow", 2, 2); while (undo(m)) { // drain } expect(m.dirty).toBe(false); expect(entities(m).length).toBe(0); }); }); describe("syncMapSize", () => { it("updates bounds and clamps entities into the new size", () => { const m = model(10, 10); addBuilding(m, "house", 8, 8); // 2×2 at 8,8 → fits in 10×10 addNpc(m, "mayor", 9, 0); syncMapSize(m, 5, 5); expect(m.mapWidth).toBe(5); expect(m.mapHeight).toBe(5); expect(entities(m)[0]).toEqual({ type: "building", buildingId: "house", x: 3, y: 3 }); expect(entities(m)[1]).toEqual({ type: "npc", npcId: "mayor", x: 4, y: 0 }); }); it("leaves a building whose footprint cannot fit in place (defensive — sceneFitsIn guards first)", () => { const m = model(10, 10); addBuilding(m, "house", 4, 4); syncMapSize(m, 1, 1); expect(entities(m)[0]).toEqual({ type: "building", buildingId: "house", x: 4, y: 4 }); }); }); describe("sceneFitsIn", () => { it("rejects a size that cannot contain a placed building footprint", () => { const m = model(10, 10); addBuilding(m, "house", 4, 4); // 2×2 expect(sceneFitsIn(m, 10, 10)).toEqual({ ok: true }); expect(sceneFitsIn(m, 2, 2)).toEqual({ ok: true }); const result = sceneFitsIn(m, 1, 1); expect(result).toEqual({ ok: false, reason: expect.stringContaining('building "house"') }); expect(result).toEqual({ ok: false, reason: expect.stringContaining("1×1") }); }); it("always fits npc/animal (1×1)", () => { const m = model(10, 10); addNpc(m, "mayor", 9, 9); expect(addAnimal(m, "chicken", 8, 8)).toEqual({ ok: true }); expect(sceneFitsIn(m, 1, 1)).toEqual({ ok: true }); }); }); describe("clearHistory + sceneFitsAfterResize (bug-fix round)", () => { it("clearHistory keeps the dirty flag — the resize is an unsaved edit", () => { const m = model(); addNpc(m, "mayor", 0, 0); clearHistory(m); expect(m.history.length).toBe(0); expect(m.dirty).toBe(true); expect(canUndo(m)).toBe(false); }); it("sceneFitsAfterResize rejects a shrink whose clamping would overlap buildings", () => { const m = model(10, 10); addBuilding(m, "house", 0, 0); // 2×2 addBuilding(m, "shed", 3, 0); // 2×2 — clamp target (1,0) would overlap the house const result = sceneFitsAfterResize(m, 3, 3); expect(result).toEqual({ ok: false, reason: expect.stringContaining("would overlap") }); // per-building fit still passes at 3×3 (each 2×2 fits) — the overlap // simulation is what rejects it expect(sceneFitsIn(m, 3, 3)).toEqual({ ok: true }); }); it("sceneFitsAfterResize passes when clamped entities stay apart", () => { const m = model(10, 10); addBuilding(m, "house", 0, 0); addBuilding(m, "shed", 5, 0); addNpc(m, "mayor", 9, 9); expect(sceneFitsAfterResize(m, 4, 4)).toEqual({ ok: true }); }); it("sceneFitsAfterResize still rejects uncontainable buildings", () => { const m = model(10, 10); addBuilding(m, "house", 0, 0); // 2×2 expect(sceneFitsAfterResize(m, 1, 1)).toEqual({ ok: false, reason: expect.stringContaining("does not fit") }); }); });