import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { MAX_POSITION_UPDATES, parsePositionUpdates, writeTreePositions, } from "@/lib/tech-tree/positions"; const RUN = `ttpos-${Date.now().toString(36)}`; const TIMEOUT = 30_000; describe("tech tree position bulk-write validation", () => { it("accepts pins and clears", () => { const result = parsePositionUpdates({ positions: [ { id: 1, x: 10.4, y: 20.6 }, { id: 2, x: null, y: null }, ], }); expect(result).toEqual({ ok: true, updates: [ { id: 1, x: 10, y: 21 }, { id: 2, x: null, y: null }, ], }); }); it("keeps the last write for a repeated id", () => { const result = parsePositionUpdates({ positions: [ { id: 3, x: 1, y: 1 }, { id: 3, x: null, y: null }, ], }); expect(result).toEqual({ ok: true, updates: [{ id: 3, x: null, y: null }] }); }); it("rejects malformed bodies", () => { expect(parsePositionUpdates(null).ok).toBe(false); expect(parsePositionUpdates({ positions: "nope" }).ok).toBe(false); expect(parsePositionUpdates({ positions: [{ id: 0, x: 1, y: 1 }] }).ok).toBe(false); expect(parsePositionUpdates({ positions: [{ id: 1.5, x: 1, y: 1 }] }).ok).toBe(false); // Half-cleared and non-finite coordinates are both invalid. expect(parsePositionUpdates({ positions: [{ id: 1, x: 5, y: null }] }).ok).toBe(false); expect(parsePositionUpdates({ positions: [{ id: 1, x: Number.NaN, y: 1 }] }).ok).toBe(false); }); it("caps the batch size", () => { const positions = Array.from({ length: MAX_POSITION_UPDATES + 1 }, (_, i) => ({ id: i + 1, x: 0, y: 0, })); expect(parsePositionUpdates({ positions }).ok).toBe(false); }); }); describe("tech tree position bulk write (database)", () => { let payload: Payload; const ids: number[] = []; beforeAll(async () => { payload = await getPayload({ config }); for (const label of ["a", "b", "c"]) { const doc = await payload.create({ collection: "technologies", data: { name: `${RUN}-${label}`, summary: "Bulk position write fixture.", type: "upgrade", approvalStatus: "in_progress", researchCosts: { minimumResearchDuration: 1 }, }, overrideAccess: true, depth: 0, }); ids.push(doc.id); } }, TIMEOUT); afterAll(async () => { for (const id of ids) { await payload.delete({ collection: "technologies", id, overrideAccess: true }); } }, TIMEOUT); const positionOf = async (id: number) => { const doc = await payload.findByID({ collection: "technologies", id, overrideAccess: true, depth: 0, }); return doc.treePosition ?? null; }; it( "pins many technologies in one write and clears them again", async () => { const pinned = await writeTreePositions(payload, [ { id: ids[0], x: 46, y: 92 }, { id: ids[1], x: 368, y: 184 }, { id: ids[2], x: 0, y: 0 }, ]); expect(pinned).toBe(3); expect(await positionOf(ids[0])).toMatchObject({ x: 46, y: 92 }); expect(await positionOf(ids[1])).toMatchObject({ x: 368, y: 184 }); expect(await positionOf(ids[2])).toMatchObject({ x: 0, y: 0 }); // Auto-arrange path: clear every pin in a single statement. const cleared = await writeTreePositions( payload, ids.map((id) => ({ id, x: null, y: null })), ); expect(cleared).toBe(3); for (const id of ids) { const position = await positionOf(id); expect(position?.x ?? null).toBeNull(); expect(position?.y ?? null).toBeNull(); } }, TIMEOUT, ); it( "ignores ids that do not exist", async () => { const touched = await writeTreePositions(payload, [{ id: 999_999_999, x: 1, y: 1 }]); expect(touched).toBe(0); }, TIMEOUT, ); });