Consume raw arma-sync-events into a validated, idempotent operation ledger: contract v1, reject/dead-letter with zero effects, effect keys with unique-constraint race handling, and a staged-activation flag hook. Registers the operations permission domains. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
400 lines
No EOL
13 KiB
TypeScript
400 lines
No EOL
13 KiB
TypeScript
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<RawSyncEvent> {
|
|
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);
|
|
});
|
|
});
|
|
}); |