import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { GameServer, SupplyBox } from "@/payload-types"; import { processBoxEvent, sessionIdFromBoxId } from "@/lib/arma-bridge/crates"; let payload: Payload; const RUN = `t5-${Date.now().toString(36)}`; describe("Supply box crate sync", () => { let serverId: number | null = null; const syncEventIds: number[] = []; const opEventIds: number[] = []; const boxIds: number[] = []; beforeAll(async () => { const payloadConfig = await config; payload = await getPayload({ config: payloadConfig }); const server = (await payload.create({ collection: "game-servers", data: { serverId: `${RUN}-srv`, name: `${RUN} Server`, status: "online", }, overrideAccess: true, depth: 0, })) as unknown as GameServer; serverId = server.id; }); afterAll(async () => { if (!payload) return; for (const id of boxIds) { await payload.delete({ collection: "supply-boxes", id, overrideAccess: true }).catch(() => {}); } for (const id of opEventIds) { await payload.delete({ collection: "operation-events", id, overrideAccess: true }).catch(() => {}); } for (const id of syncEventIds) { await payload.delete({ collection: "arma-sync-events", id, overrideAccess: true }).catch(() => {}); } if (serverId) { await payload.delete({ collection: "game-servers", id: serverId, overrideAccess: true }).catch(() => {}); } }); // Creates a raw arma-sync-events row as a provenance anchor (the supply-boxes // sourceEvent relationship requires a real row id) with a neutral type so the // afterChange hook does not process it as a box event, then runs the handler // directly against it. async function emitRaw(eventId: string, type: string, eventPayload: unknown[]): Promise { const syncDoc = await payload.create({ collection: "arma-sync-events", data: { eventId, server: serverId!, type, occurredAt: new Date().toISOString(), payload: eventPayload, }, overrideAccess: true, depth: 0, }); syncEventIds.push(syncDoc.id); const opEvents = await payload.find({ collection: "operation-events", where: { sourceEvent: { equals: syncDoc.id } }, limit: 1, depth: 0, overrideAccess: true, }); for (const doc of opEvents.docs) opEventIds.push(doc.id); return syncDoc.id; } async function runBoxEvent(eventId: string, type: string, eventPayload: unknown[]): Promise { const rawId = await emitRaw(eventId, "fixture", []); await processBoxEvent(payload, { id: rawId, eventId, server: serverId!, type, occurredAt: new Date().toISOString(), payload: eventPayload, }); return rawId; } async function fetchBox(boxId: string): Promise { const found = await payload.find({ collection: "supply-boxes", where: { boxId: { equals: boxId } }, limit: 1, depth: 0, overrideAccess: true, }); if (found.docs.length === 0) return null; boxIds.push(found.docs[0].id); return found.docs[0] as unknown as SupplyBox; } it("registers a box with provenance from a box.register event", async () => { const boxId = `box:${RUN}-reg:1`; const rawId = await runBoxEvent(`${RUN}-evt-reg`, "box.register", [ boxId, "Box_NATO_Wps_F", "Ammo Crate", 100, 200, 0, ]); const box = await fetchBox(boxId); expect(box).not.toBeNull(); expect(box!.boxClass).toBe("Box_NATO_Wps_F"); expect(box!.displayName).toBe("Ammo Crate"); expect(box!.location).toEqual({ x: 100, y: 200, z: 0 }); expect(box!.server).toBe(serverId); expect(box!.status).toBe("active"); expect(box!.sessionId).toBe(`${RUN}-reg`); expect(box!.sourceEvent).toBe(rawId); expect(box!.lastSyncedAt).toBeTruthy(); }); it("updates contents and lastSyncedAt from a box.sync event", async () => { const boxId = `box:${RUN}-sync:1`; await runBoxEvent(`${RUN}-evt-sync-reg`, "box.register", [boxId, "Box_NATO_Wps_F", "Ammo Crate", 1, 2, 3]); const before = await fetchBox(boxId); expect(before).not.toBeNull(); await runBoxEvent(`${RUN}-evt-sync`, "box.sync", [ boxId, [ ["Item_1", 10], ["Item_2", 5], ], ]); const after = await fetchBox(boxId); expect(after).not.toBeNull(); expect(after!.contents).toEqual([ { class: "Item_1", count: 10 }, { class: "Item_2", count: 5 }, ]); expect(new Date(after!.lastSyncedAt).getTime()).toBeGreaterThanOrEqual( new Date(before!.lastSyncedAt).getTime(), ); }); it("is idempotent when the same box event is applied twice", async () => { const boxId = `box:${RUN}-idem:1`; const payloadArgs = [boxId, "Box_NATO_Wps_F", "Ammo Crate", 1, 2, 3] as unknown[]; await runBoxEvent(`${RUN}-evt-idem-a`, "box.register", payloadArgs); await runBoxEvent(`${RUN}-evt-idem-b`, "box.register", payloadArgs); const found = await payload.find({ collection: "supply-boxes", where: { boxId: { equals: boxId } }, limit: 10, depth: 0, overrideAccess: true, }); expect(found.docs).toHaveLength(1); for (const doc of found.docs) boxIds.push(doc.id); }); it("no-ops on malformed payloads without throwing", async () => { await runBoxEvent(`${RUN}-evt-malformed-1`, "box.sync", "not-an-array" as unknown as unknown[]); await runBoxEvent(`${RUN}-evt-malformed-2`, "box.register", []); await runBoxEvent(`${RUN}-evt-malformed-3`, "box.register", ["not-a-box-id", "Box_NATO_Wps_F", "X", 1, 2, 3]); const found = await payload.find({ collection: "supply-boxes", where: { boxId: { like: `${RUN}-malformed` } }, limit: 10, depth: 0, overrideAccess: true, }); expect(found.docs).toHaveLength(0); }); it("marks a box deleted and reactivates it on a later sync", async () => { const boxId = `box:${RUN}-del:1`; await runBoxEvent(`${RUN}-evt-del-reg`, "box.register", [boxId, "Box_NATO_Wps_F", "Ammo Crate", 1, 2, 3]); await runBoxEvent(`${RUN}-evt-del`, "box.deleted", [boxId]); const deleted = await fetchBox(boxId); expect(deleted).not.toBeNull(); expect(deleted!.status).toBe("deleted"); await runBoxEvent(`${RUN}-evt-del-sync`, "box.sync", [boxId, [["Item_1", 3]]]); const reactivated = await fetchBox(boxId); expect(reactivated).not.toBeNull(); expect(reactivated!.status).toBe("active"); expect(reactivated!.contents).toEqual([{ class: "Item_1", count: 3 }]); }); it("no-ops when the raw event has no server", async () => { const rawId = await emitRaw(`${RUN}-evt-null-server`, "fixture", []); await processBoxEvent(payload, { id: rawId, eventId: `${RUN}-evt-null-server`, server: null, type: "box.register", occurredAt: new Date().toISOString(), payload: [`box:${RUN}-null:1`, "Box_NATO_Wps_F", "X", 1, 2, 3], }); const found = await payload.find({ collection: "supply-boxes", where: { boxId: { equals: `box:${RUN}-null:1` } }, limit: 1, depth: 0, overrideAccess: true, }); expect(found.docs).toHaveLength(0); }); it("extracts the session id from the box id", () => { expect(sessionIdFromBoxId("box:Altis-20260921120000:42")).toBe("Altis-20260921120000"); expect(sessionIdFromBoxId("box:Altis:1")).toBe("Altis"); expect(sessionIdFromBoxId("not-a-box-id")).toBeNull(); expect(sessionIdFromBoxId("box:Altis-1:abc")).toBeNull(); expect(sessionIdFromBoxId("box::1")).toBeNull(); }); it("wires crate sync through the afterChange hook", async () => { const boxId = `box:${RUN}-hook:1`; const raw = await payload.create({ collection: "arma-sync-events", data: { eventId: `${RUN}-evt-hook`, server: serverId!, type: "box.register", occurredAt: new Date().toISOString(), payload: [boxId, "Box_NATO_Wps_F", "Hook Crate", 10, 20, 30], }, overrideAccess: true, depth: 0, }); syncEventIds.push(raw.id); const opEvents = await payload.find({ collection: "operation-events", where: { sourceEvent: { equals: raw.id } }, limit: 1, depth: 0, overrideAccess: true, }); for (const doc of opEvents.docs) opEventIds.push(doc.id); const box = await fetchBox(boxId); expect(box).not.toBeNull(); expect(box!.boxClass).toBe("Box_NATO_Wps_F"); expect(box!.sourceEvent).toBe(raw.id); }); });