import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { hashPayload, processOperationEvent, stableStringify } from "@/lib/operations/process"; import type { RawSyncEvent } from "@/lib/operations/types"; type JsonValue = string | number | boolean | unknown[] | { [k: string]: unknown } | null; let payload: Payload; const RUN = `op-${Date.now().toString(36)}`; describe("Operation ledger", () => { let userId: number; let profileId: number; let serverId: number; const createdSyncEventIds: number[] = []; const createdEventIds: number[] = []; const createdEffectIds: number[] = []; beforeAll(async () => { const payloadConfig = await config; payload = await getPayload({ config: payloadConfig }); const user = await payload.create({ collection: "users", data: { username: `${RUN}-op`, discordUsername: `${RUN}-op-discord`, displayName: `${RUN} Operator`, steamId: `${RUN}-steam-001`, password: "Test1234", }, overrideAccess: true, depth: 0, }); userId = user.id; const profiles = await payload.find({ collection: "profiles", where: { user: { equals: userId } }, limit: 1, depth: 0, overrideAccess: true, }); profileId = profiles.docs[0].id; const server = await payload.create({ collection: "game-servers", data: { serverId: `${RUN}-srv`, name: `${RUN} Server`, status: "online", }, overrideAccess: true, depth: 0, }); serverId = server.id; }); afterAll(async () => { for (const id of createdEffectIds) { await payload.delete({ collection: "operation-effects", id, overrideAccess: true }).catch(() => {}); } for (const id of createdEventIds) { await payload.delete({ collection: "operation-events", id, overrideAccess: true }).catch(() => {}); } for (const id of createdSyncEventIds) { await payload.delete({ collection: "arma-sync-events", id, overrideAccess: true }).catch(() => {}); } if (profileId) { await payload.delete({ collection: "profiles", id: profileId, overrideAccess: true }).catch(() => {}); } if (userId) { await payload.delete({ collection: "users", id: userId, overrideAccess: true }).catch(() => {}); } if (serverId) { await payload.delete({ collection: "game-servers", id: serverId, overrideAccess: true }).catch(() => {}); } }); async function createRawEvent(opts: { eventId: string; type: string; payload: JsonValue; }): Promise { const occurredAt = new Date().toISOString(); const syncDoc = await payload.create({ collection: "arma-sync-events", data: { eventId: opts.eventId, server: serverId, type: opts.type, occurredAt, payload: opts.payload, }, overrideAccess: true, depth: 0, }); createdSyncEventIds.push(syncDoc.id); return { id: syncDoc.id, eventId: syncDoc.eventId, server: serverId, type: syncDoc.type, occurredAt, payload: syncDoc.payload, }; } describe("stableStringify", () => { it("produces stable output regardless of key order", () => { expect(stableStringify({ b: 1, a: 2 })).toBe(stableStringify({ a: 2, b: 1 })); expect(stableStringify([{ b: 1, a: 2 }])).toBe(stableStringify([{ a: 2, b: 1 }])); expect(stableStringify([{ b: 1 }, { a: 2 }])).not.toBe(stableStringify([{ a: 2 }, { b: 1 }])); }); }); describe("processOperationEvent", () => { it("records a validated operation.start and derives an operation.started effect", async () => { const raw = await createRawEvent({ eventId: `${RUN}-evt-start`, type: "operation.start", payload: { operationId: RUN, version: 1 }, }); await processOperationEvent(payload, raw); const events = await payload.find({ collection: "operation-events", where: { eventId: { equals: raw.eventId } }, limit: 1, depth: 0, overrideAccess: true, }); expect(events.docs).toHaveLength(1); const event = events.docs[0]; expect(event.status).toBe("validated"); expect(event.operationId).toBe(RUN); expect(event.type).toBe("operation.start"); expect(event.version).toBe(1); expect(event.sourceEvent).toBe(raw.id); expect(event.payloadHash).toBe(hashPayload(raw.payload)); createdEventIds.push(event.id); const effects = await payload.find({ collection: "operation-effects", where: { operationEvent: { equals: event.id } }, limit: 10, depth: 0, overrideAccess: true, }); expect(effects.docs).toHaveLength(1); const effect = effects.docs[0]; expect(effect.effectType).toBe("operation.started"); expect(effect.status).toBe("applied"); expect(effect.sourceEvent).toBe(raw.id); expect(effect.operationId).toBe(RUN); createdEffectIds.push(effect.id); }); it("is idempotent for a duplicate raw event", async () => { const raw = await createRawEvent({ eventId: `${RUN}-evt-dup`, type: "operation.end", payload: { operationId: RUN, version: 1 }, }); await processOperationEvent(payload, raw); await processOperationEvent(payload, raw); const events = await payload.find({ collection: "operation-events", where: { eventId: { equals: raw.eventId } }, limit: 10, depth: 0, overrideAccess: true, }); expect(events.docs).toHaveLength(1); createdEventIds.push(events.docs[0].id); const effects = await payload.find({ collection: "operation-effects", where: { operationEvent: { equals: events.docs[0].id } }, limit: 10, depth: 0, overrideAccess: true, }); expect(effects.docs).toHaveLength(1); createdEffectIds.push(effects.docs[0].id); }); it("retains an unsupported-version event with zero effects", async () => { const raw = await createRawEvent({ eventId: `${RUN}-evt-v2`, type: "operation.start", payload: { operationId: RUN, version: 2 }, }); await processOperationEvent(payload, raw); const events = await payload.find({ collection: "operation-events", where: { eventId: { equals: raw.eventId } }, limit: 1, depth: 0, overrideAccess: true, }); expect(events.docs).toHaveLength(1); const event = events.docs[0]; expect(event.status).toBe("rejected"); expect(event.version).toBe(2); expect(event.error).toMatchObject({ code: "unsupported-version" }); createdEventIds.push(event.id); const effects = await payload.find({ collection: "operation-effects", where: { operationEvent: { equals: event.id } }, limit: 10, depth: 0, overrideAccess: true, }); expect(effects.docs).toHaveLength(0); }); it("records player.kill as validated with zero effects", async () => { const raw = await createRawEvent({ eventId: `${RUN}-evt-kill`, type: "player.kill", payload: [ "Victim", "", `${RUN} Killer`, `${RUN}-steam-001`, `${RUN} Killer`, `${RUN}-steam-001`, "O_T_Soldier_AT_F", "B_Officer_Parade_Veteran_F", ], }); await processOperationEvent(payload, raw); const events = await payload.find({ collection: "operation-events", where: { eventId: { equals: raw.eventId } }, limit: 1, depth: 0, overrideAccess: true, }); expect(events.docs).toHaveLength(1); expect(events.docs[0].status).toBe("validated"); expect(events.docs[0].operationId).toBe("unknown"); createdEventIds.push(events.docs[0].id); const effects = await payload.find({ collection: "operation-effects", where: { operationEvent: { equals: events.docs[0].id } }, limit: 10, depth: 0, overrideAccess: true, }); expect(effects.docs).toHaveLength(0); }); it("rejects a malformed payload with zero effects", async () => { const raw = await createRawEvent({ eventId: `${RUN}-evt-bad`, type: "operation.start", payload: [], }); await processOperationEvent(payload, raw); const events = await payload.find({ collection: "operation-events", where: { eventId: { equals: raw.eventId } }, limit: 1, depth: 0, overrideAccess: true, }); expect(events.docs).toHaveLength(1); expect(events.docs[0].status).toBe("rejected"); expect(events.docs[0].error).toMatchObject({ code: "invalid-payload" }); createdEventIds.push(events.docs[0].id); const effects = await payload.find({ collection: "operation-effects", where: { operationEvent: { equals: events.docs[0].id } }, limit: 10, depth: 0, overrideAccess: true, }); expect(effects.docs).toHaveLength(0); }); it("dead-letters an unknown type with zero effects", async () => { const raw = await createRawEvent({ eventId: `${RUN}-evt-unknown`, type: "some.unknown.type", payload: { anything: true }, }); await processOperationEvent(payload, raw); const events = await payload.find({ collection: "operation-events", where: { eventId: { equals: raw.eventId } }, limit: 1, depth: 0, overrideAccess: true, }); expect(events.docs).toHaveLength(1); expect(events.docs[0].status).toBe("dead-letter"); expect(events.docs[0].type).toBe("some.unknown.type"); expect(events.docs[0].error).toMatchObject({ code: "unsupported-type" }); createdEventIds.push(events.docs[0].id); const effects = await payload.find({ collection: "operation-effects", where: { operationEvent: { equals: events.docs[0].id } }, limit: 10, depth: 0, overrideAccess: true, }); expect(effects.docs).toHaveLength(0); }); it("links actor from the payload", async () => { const raw = await createRawEvent({ eventId: `${RUN}-evt-extract`, type: "operation.extraction", payload: { operationId: RUN, version: 1, actorId: userId, player: `${RUN} Operator` }, }); await processOperationEvent(payload, raw); const events = await payload.find({ collection: "operation-events", where: { eventId: { equals: raw.eventId } }, limit: 1, depth: 0, overrideAccess: true, }); expect(events.docs).toHaveLength(1); expect(events.docs[0].actor).toBe(userId); createdEventIds.push(events.docs[0].id); const effects = await payload.find({ collection: "operation-effects", where: { operationEvent: { equals: events.docs[0].id } }, limit: 10, depth: 0, overrideAccess: true, }); expect(effects.docs).toHaveLength(1); expect(effects.docs[0].effectType).toBe("operation.extraction-recorded"); createdEffectIds.push(effects.docs[0].id); }); }); describe("end-to-end via arma-sync-events create", () => { it("consumes a real sync event through the afterChange hook", async () => { const syncEvent = await payload.create({ collection: "arma-sync-events", data: { eventId: `${RUN}-real-1`, server: serverId, type: "operation.start", occurredAt: new Date().toISOString(), payload: { operationId: RUN, version: 1 }, }, overrideAccess: true, depth: 0, }); createdSyncEventIds.push(syncEvent.id); const events = await payload.find({ collection: "operation-events", where: { sourceEvent: { equals: syncEvent.id } }, limit: 1, depth: 0, overrideAccess: true, }); expect(events.docs).toHaveLength(1); const event = events.docs[0]; expect(event.status).toBe("validated"); expect(event.operationId).toBe(RUN); expect(event.eventId).toBe(`${RUN}-real-1`); createdEventIds.push(event.id); const effects = await payload.find({ collection: "operation-effects", where: { operationEvent: { equals: event.id } }, limit: 10, depth: 0, overrideAccess: true, }); expect(effects.docs).toHaveLength(1); const effect = effects.docs[0]; expect(effect.effectType).toBe("operation.started"); expect(effect.sourceEvent).toBe(syncEvent.id); expect(effect.operationEvent).toBe(event.id); createdEffectIds.push(effect.id); }); }); });