import { getPayload, Payload } from "payload"; import config from "@/payload.config"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { processOperationEvent } from "@/lib/operations/process"; import { reserveOperationAssets, type SettlementRecord } from "@/lib/operations/allocation"; import { ensureLockerStorage, getLockerGridDimensions } from "@/lib/locker"; import type { GameStructure, LockerStorage, OperationReservation, Structure } from "@/payload-types"; let payload: Payload; const RUN = `settle-${Date.now().toString(36)}`; const LEXICAL_EMPTY = { root: { children: [{ text: "" }], direction: null, format: "" as const, indent: 0, type: "text", version: 1, }, }; // --------------------------------------------------------------------------- // Fixture tracking for cleanup // --------------------------------------------------------------------------- const reservationIds: number[] = []; const syncEventIds: number[] = []; const opEventIds: number[] = []; const opEffectIds: number[] = []; const userIds: number[] = []; const roleIds: number[] = []; const gameStructureIds: number[] = []; const blueprintIds: number[] = []; const resourceIds: number[] = []; const assetIds: number[] = []; const missionIds: number[] = []; const campaignIds: number[] = []; const mapIds: number[] = []; let serverId: number; let commandUserId: number; let extractorId: number; let deathUserId: number; let fullLockerUserId: number; let malformedUserId: number; let mapId: number; let campaignId: number; let missionId: number; let resourceAId: number; let stackableAssetId: number; let fillerAssetId: number; let hqBlueprintId: number; let cappedBlueprintId: number; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function relId(value: unknown): number { return typeof value === "object" && value !== null ? (value as { id: number }).id : (value as number); } function settlementOf(doc: OperationReservation): SettlementRecord { return doc.settlement as unknown as SettlementRecord; } async function makeUser(label: string, roleDocIds?: number[]): Promise { const user = await payload.create({ collection: "users", data: { username: `${RUN}-${label}`, discordUsername: `${RUN}-${label}`, displayName: `${RUN} ${label}`, steamId: `7656119${Math.floor(Math.random() * 1e9)}`, password: "Test1234", roles: ["user"], ...(roleDocIds && roleDocIds.length > 0 ? { roleDocs: roleDocIds } : {}), }, overrideAccess: true, depth: 0, }); userIds.push(user.id); return user.id; } async function makeStructure( label: string, blueprintId: number, stock: { resourceId: number; amount: number }[] = [], ): Promise { const site = await payload.create({ collection: "game-structures", data: { name: `${RUN} ${label} ${gameStructureIds.length}`, type: blueprintId, map: mapId, coordinates: [100 + gameStructureIds.length, 100], constructionStatus: "complete", storedResources: stock.map((s, i) => ({ resource: s.resourceId, amount: s.amount, gridX: 0, gridY: i, rotated: false, })), }, overrideAccess: true, depth: 0, }); gameStructureIds.push(site.id); return site.id; } async function storedAmountAt(siteId: number, resourceId: number): Promise { const doc = (await payload.findByID({ collection: "game-structures", id: siteId, depth: 0, overrideAccess: true, })) as unknown as GameStructure; return (doc.storedResources ?? []) .filter((r) => relId(r.resource) === resourceId) .reduce((sum, r) => sum + r.amount, 0); } type JsonObject = { [k: string]: unknown }; async function emitRawEvent(eventId: string, type: string, eventPayload: JsonObject): 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); } async function effectsForOperation(operationId: string) { const effects = await payload.find({ collection: "operation-effects", where: { operationId: { equals: operationId } }, limit: 20, depth: 0, overrideAccess: true, }); for (const doc of effects.docs) { if (!opEffectIds.includes(doc.id)) opEffectIds.push(doc.id); } return effects.docs; } async function notificationsFor(userId: number, type: string) { const found = await payload.find({ collection: "user-notifications", where: { and: [{ user: { equals: userId } }, { type: { equals: type } }] }, limit: 20, depth: 0, overrideAccess: true, }); return found.docs; } async function lockerOf(userId: number): Promise { const found = await payload.find({ collection: "locker-storages", where: { ownerUser: { equals: userId } }, limit: 1, depth: 1, overrideAccess: true, }); return (found.docs[0] as unknown as LockerStorage | undefined) ?? null; } async function findReservation(key: string): Promise { const res = await payload.find({ collection: "operation-reservations", where: { reservationKey: { equals: key } }, limit: 1, depth: 0, overrideAccess: true, }); return (res.docs[0] as OperationReservation | undefined) ?? null; } // --------------------------------------------------------------------------- // Setup / teardown // --------------------------------------------------------------------------- beforeAll(async () => { const payloadConfig = await config; payload = await getPayload({ config: payloadConfig }); const superRole = await payload.create({ collection: "roles", data: { name: `${RUN} Command`, slug: `${RUN}-command`, isSuperuser: true }, overrideAccess: true, depth: 0, }); roleIds.push(superRole.id); commandUserId = await makeUser("command", [superRole.id]); extractorId = await makeUser("extractor"); deathUserId = await makeUser("dead"); fullLockerUserId = await makeUser("packed"); malformedUserId = await makeUser("glitch"); const map = await payload.create({ collection: "maps", data: { name: `${RUN} Map`, worldSizeWidth: 8192, worldSizeHeight: 8192, basemapMode: "image" }, overrideAccess: true, depth: 0, }); mapId = map.id; mapIds.push(map.id); const campaign = await payload.create({ collection: "campaigns", data: { name: `${RUN} Campaign`, summary: "Settlement test campaign", status: "concept", campaignMode: "custom", }, overrideAccess: true, depth: 0, }); campaignId = campaign.id; campaignIds.push(campaign.id); const mission = await payload.create({ collection: "missions", data: { name: `${RUN} Mission`, codeName: `${RUN}-mission`, summary: "Settlement test mission", operationType: "main", classification: { map: mapId, missionType: "PvE", campaign: campaignId, startDateTime: new Date(Date.now() + 86_400_000).toISOString(), estimatedDuration: 60, }, ownershipAndStatus: { authors: [commandUserId], status: "Scheduled", visibility: "unit", }, missionRoles: { maxPlayers: 16 }, gameDetails: { serverDetails: { serverIp: "127.0.0.1", serverPort: 2302 } }, briefing: [], }, overrideAccess: true, depth: 0, }); missionId = mission.id; missionIds.push(mission.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; const resource = await payload.create({ collection: "resources", data: { name: `${RUN} Supplies`, codeName: `${RUN}-supplies`, approvalStatus: "in_progress", type: "physical", baseValue: 1, rarity: "common", unitOfMeasure: "kg", massPerUnit: 1, gridWidth: 1, gridHeight: 1, }, overrideAccess: true, depth: 0, }); resourceAId = resource.id; resourceIds.push(resource.id); const stackableAsset = await payload.create({ collection: "assets", data: { name: `${RUN} Ammo Box`, className: `${RUN}_ammo_box`, assetType: "magazine", approvalStatus: "in_progress", crafting: { craftable: false, craftingData: { craftingTimePerUnit: 1, batchSize: 1 } }, storageDimensions: { gridWidth: 1, gridHeight: 1, stackable: true, maxStackSize: 10 }, }, overrideAccess: true, depth: 0, }); stackableAssetId = stackableAsset.id; assetIds.push(stackableAsset.id); const fillerAsset = await payload.create({ collection: "assets", data: { name: `${RUN} Rifle`, className: `${RUN}_rifle`, assetType: "weapon", approvalStatus: "in_progress", crafting: { craftable: false, craftingData: { craftingTimePerUnit: 1, batchSize: 1 } }, storageDimensions: { gridWidth: 1, gridHeight: 1, stackable: false }, }, overrideAccess: true, depth: 0, }); fillerAssetId = fillerAsset.id; assetIds.push(fillerAsset.id); const hqBlueprint = await payload.create({ collection: "structures", data: { name: `${RUN} HQ Depot`, codeName: `${RUN}-hq-depot`, approvalStatus: "in_progress", description: LEXICAL_EMPTY as unknown as Structure["description"], category: "logistics", materials: [{ resource: resourceAId, amount: 1 }], constructionDurationMinutes: 1, terrainType: "land", maxHealth: 100, }, overrideAccess: true, depth: 0, }); hqBlueprintId = hqBlueprint.id; blueprintIds.push(hqBlueprint.id); const cappedBlueprint = await payload.create({ collection: "structures", data: { name: `${RUN} Cramped Shed`, codeName: `${RUN}-cramped-shed`, approvalStatus: "in_progress", description: LEXICAL_EMPTY as unknown as Structure["description"], category: "logistics", materials: [{ resource: resourceAId, amount: 1 }], constructionDurationMinutes: 1, terrainType: "land", maxHealth: 100, maxCapacityMass: 5, }, overrideAccess: true, depth: 0, }); cappedBlueprintId = cappedBlueprint.id; blueprintIds.push(cappedBlueprint.id); }); afterAll(async () => { if (!payload) return; for (const id of reservationIds) { await payload.delete({ collection: "operation-reservations", id, overrideAccess: true }).catch(() => {}); } for (const id of opEffectIds) { await payload.delete({ collection: "operation-effects", 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(() => {}); } for (const id of gameStructureIds) { await payload.delete({ collection: "game-structures", id, overrideAccess: true }).catch(() => {}); } for (const id of blueprintIds) { await payload.delete({ collection: "structures", id, overrideAccess: true }).catch(() => {}); } for (const id of assetIds) { await payload.delete({ collection: "assets", id, overrideAccess: true }).catch(() => {}); } for (const id of resourceIds) { await payload.delete({ collection: "resources", id, overrideAccess: true }).catch(() => {}); } for (const id of missionIds) { await payload.delete({ collection: "missions", id, overrideAccess: true }).catch(() => {}); } for (const id of campaignIds) { await payload.delete({ collection: "campaigns", id, overrideAccess: true }).catch(() => {}); } for (const id of mapIds) { await payload.delete({ collection: "maps", id, overrideAccess: true }).catch(() => {}); } const eventLogs = await payload.find({ collection: "game-event-logs", where: { actor: { in: userIds } }, limit: 200, depth: 0, overrideAccess: true, }); for (const doc of eventLogs.docs) { await payload.delete({ collection: "game-event-logs", id: doc.id, overrideAccess: true }).catch(() => {}); } for (const userId of userIds) { const notifications = await payload.find({ collection: "user-notifications", where: { user: { equals: userId } }, limit: 50, depth: 0, overrideAccess: true, }); for (const doc of notifications.docs) { await payload.delete({ collection: "user-notifications", id: doc.id, overrideAccess: true }).catch(() => {}); } const lockers = await payload.find({ collection: "locker-storages", where: { ownerUser: { equals: userId } }, limit: 5, depth: 0, overrideAccess: true, }); for (const doc of lockers.docs) { await payload.delete({ collection: "locker-storages", id: doc.id, overrideAccess: true }).catch(() => {}); } const profiles = await payload.find({ collection: "profiles", where: { user: { equals: userId } }, limit: 5, depth: 0, overrideAccess: true, }); for (const profile of profiles.docs) { await payload.delete({ collection: "profiles", id: profile.id, overrideAccess: true }).catch(() => {}); } await payload.delete({ collection: "users", id: userId, overrideAccess: true }).catch(() => {}); } for (const id of roleIds) { await payload.delete({ collection: "roles", id, overrideAccess: true }).catch(() => {}); } if (serverId) { await payload.delete({ collection: "game-servers", id: serverId, overrideAccess: true }).catch(() => {}); } }); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("Operation settlement", () => { it("settles a logistics extraction into one provenance-linked HQ deposit and settles its reservation", async () => { const operationId = `${RUN}-op-log`; const originId = await makeStructure("origin", hqBlueprintId, [ { resourceId: resourceAId, amount: 100 }, ]); const hqId = await makeStructure("hq", hqBlueprintId); const reservationKey = `${RUN}-res-log`; const reservation = await reserveOperationAssets(payload, { reservationKey, operationId, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 40 }], originId, }); reservationIds.push(reservation.id); await emitRawEvent(`${RUN}-evt-log`, "operation.extraction", { operationId, version: 1, actorId: extractorId, kind: "logistics", hqStructureId: hqId, reservationKey, items: [{ resourceId: resourceAId, amount: 25 }], consumed: { cargo: [{ resourceId: resourceAId, amount: 15 }] }, }); const effects = await effectsForOperation(operationId); expect(effects).toHaveLength(1); const effect = effects[0]; expect(effect.effectType).toBe("operation.extraction-recorded"); expect(effect.status).toBe("applied"); expect(effect.appliedAt).toBeTruthy(); // Exactly one deposit: 25 units at the designated HQ, none at the origin. expect(await storedAmountAt(hqId, resourceAId)).toBe(25); expect(await storedAmountAt(originId, resourceAId)).toBe(100); // The linked reservation settled exactly once from the effect. const settled = await findReservation(reservationKey); expect(settled?.status).toBe("settled"); const record = settlementOf(settled as OperationReservation); expect(record.reason).toBe("extracted"); expect(record.consumed.cargo).toEqual([{ resource: resourceAId, amount: 15 }]); expect(record.returned.cargo).toEqual([{ resource: resourceAId, amount: 25 }]); expect(record.effectId).toBe(effect.id); expect(relId(settled?.settledByEffect)).toBe(effect.id); const notifications = await notificationsFor(extractorId, "operation:extraction"); expect(notifications.length).toBeGreaterThanOrEqual(1); }); it("settles a player extraction into one provenance-linked locker credit and settles its reservation", async () => { const operationId = `${RUN}-op-player`; const reservationKey = `${RUN}-res-player`; const reservation = await reserveOperationAssets(payload, { reservationKey, operationId, actorId: commandUserId, missionId, personnel: [{ userId: extractorId }], }); reservationIds.push(reservation.id); await emitRawEvent(`${RUN}-evt-player`, "operation.extraction", { operationId, version: 1, actorId: extractorId, kind: "player", reservationKey, items: [{ assetId: stackableAssetId, quantity: 3 }], }); const effects = await effectsForOperation(operationId); expect(effects).toHaveLength(1); expect(effects[0].status).toBe("applied"); const locker = await lockerOf(extractorId); expect(locker).not.toBeNull(); const entries = (locker?.items ?? []).filter((e) => relId(e.asset) === stackableAssetId); expect(entries).toHaveLength(1); expect(entries[0].quantity).toBe(3); const settled = await findReservation(reservationKey); expect(settled?.status).toBe("settled"); const record = settlementOf(settled as OperationReservation); expect(record.reason).toBe("extracted"); expect(record.consumed).toEqual({ personnel: [], vehicles: [], cargo: [], budget: 0 }); expect(record.returned.personnel).toEqual([extractorId]); expect(record.effectId).toBe(effects[0].id); const notifications = await notificationsFor(extractorId, "operation:extraction"); expect(notifications.length).toBeGreaterThanOrEqual(1); }); it("applies a duplicate extraction event as a no-op with no second deposit", async () => { const operationId = `${RUN}-op-dup`; const hqId = await makeStructure("hq-dup", hqBlueprintId); const syncDoc = await payload.create({ collection: "arma-sync-events", data: { eventId: `${RUN}-evt-dup`, server: serverId, type: "operation.extraction", occurredAt: new Date().toISOString(), payload: { operationId, version: 1, actorId: extractorId, kind: "logistics", hqStructureId: hqId, items: [{ resourceId: resourceAId, amount: 10 }], }, }, 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); // A replay of the same raw event is idempotent by sourceEvent. await processOperationEvent(payload, { id: syncDoc.id, eventId: syncDoc.eventId, server: serverId, type: syncDoc.type, occurredAt: syncDoc.occurredAt, payload: syncDoc.payload, }); const effects = await effectsForOperation(operationId); expect(effects).toHaveLength(1); expect(effects[0].status).toBe("applied"); expect(await storedAmountAt(hqId, resourceAId)).toBe(10); }); it("records death loss evidence for unextracted inventory only, with no reward", async () => { const operationId = `${RUN}-op-death`; await emitRawEvent(`${RUN}-evt-death`, "operation.inventory", { operationId, version: 1, actorId: deathUserId, reason: "death", items: [{ assetId: stackableAssetId, quantity: 2 }], }); const effects = await effectsForOperation(operationId); expect(effects).toHaveLength(1); expect(effects[0].effectType).toBe("operation.inventory-recorded"); expect(effects[0].status).toBe("applied"); // No inventory was credited anywhere. const locker = await lockerOf(deathUserId); expect(locker).toBeNull(); // Loss evidence: notification + combat:death event log entry. const notifications = await notificationsFor(deathUserId, "operation:death"); expect(notifications).toHaveLength(1); const deathEvents = await payload.find({ collection: "game-event-logs", where: { and: [{ type: { equals: "combat:death" } }, { targetId: { equals: deathUserId } }] }, limit: 10, depth: 0, overrideAccess: true, }); expect(deathEvents.docs).toHaveLength(1); // No scoreboard write: the XP hook would turn any scoreboard key into XP. const profiles = await payload.find({ collection: "profiles", where: { user: { equals: deathUserId } }, limit: 1, depth: 0, overrideAccess: true, }); const scoreboard = (profiles.docs[0]?.progression?.scoreboard ?? {}) as Record; expect(scoreboard.deaths ?? 0).toBe(0); expect(scoreboard.infantryKills ?? 0).toBe(0); }); it("fails recoverably on a full HQ with no partial deposit", async () => { const operationId = `${RUN}-op-full-hq`; const hqId = await makeStructure("hq-capped", cappedBlueprintId); await emitRawEvent(`${RUN}-evt-full-hq`, "operation.extraction", { operationId, version: 1, actorId: extractorId, kind: "logistics", hqStructureId: hqId, items: [{ resourceId: resourceAId, amount: 10 }], }); const effects = await effectsForOperation(operationId); expect(effects).toHaveLength(1); expect(effects[0].status).toBe("failed"); expect(effects[0].reconciliationNotes).toContain("hq-capacity"); expect(await storedAmountAt(hqId, resourceAId)).toBe(0); }); it("fails recoverably on a full locker with no partial credit", async () => { const operationId = `${RUN}-op-full-locker`; const locker = await ensureLockerStorage(payload, fullLockerUserId); const grid = await getLockerGridDimensions(payload); const fill = []; for (let y = 0; y < grid.height; y++) { for (let x = 0; x < grid.width; x++) { fill.push({ asset: fillerAssetId, quantity: 1, gridX: x, gridY: y, rotated: false }); } } await payload.update({ collection: "locker-storages", id: locker.id, data: { items: fill }, overrideAccess: true, depth: 0, }); await emitRawEvent(`${RUN}-evt-full-locker`, "operation.extraction", { operationId, version: 1, actorId: fullLockerUserId, kind: "player", items: [{ assetId: stackableAssetId, quantity: 1 }], }); const effects = await effectsForOperation(operationId); expect(effects).toHaveLength(1); expect(effects[0].status).toBe("failed"); expect(effects[0].reconciliationNotes).toContain("locker-full"); const after = await lockerOf(fullLockerUserId); expect(after?.items ?? []).toHaveLength(grid.width * grid.height); }); it("rejects malformed and unresolved asset references without guessing", async () => { const unresolvedOp = `${RUN}-op-unresolved`; await emitRawEvent(`${RUN}-evt-unresolved`, "operation.extraction", { operationId: unresolvedOp, version: 1, actorId: malformedUserId, kind: "player", items: [{ assetId: 99999999, quantity: 1 }], }); const unresolvedEffects = await effectsForOperation(unresolvedOp); expect(unresolvedEffects).toHaveLength(1); expect(unresolvedEffects[0].status).toBe("failed"); expect(unresolvedEffects[0].reconciliationNotes).toContain("unresolved-asset"); const malformedOp = `${RUN}-op-malformed`; await emitRawEvent(`${RUN}-evt-malformed`, "operation.extraction", { operationId: malformedOp, version: 1, actorId: malformedUserId, kind: "player", items: [{ assetId: "abc", quantity: 1 }], }); const malformedEffects = await effectsForOperation(malformedOp); expect(malformedEffects).toHaveLength(1); expect(malformedEffects[0].status).toBe("failed"); expect(malformedEffects[0].reconciliationNotes).toContain("malformed-asset"); // Nothing was credited; validation failed before the locker was even created. expect(await lockerOf(malformedUserId)).toBeNull(); }); it("preserves an extraction deposit after a later mission failure", async () => { const operationId = `${RUN}-op-fail`; const originId = await makeStructure("origin-fail", hqBlueprintId, [ { resourceId: resourceAId, amount: 100 }, ]); const hqId = await makeStructure("hq-fail", hqBlueprintId); const extractedKey = `${RUN}-fail-extracted`; const extracted = await reserveOperationAssets(payload, { reservationKey: extractedKey, operationId, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 10 }], originId, }); reservationIds.push(extracted.id); const pendingKey = `${RUN}-fail-pending`; const pending = await reserveOperationAssets(payload, { reservationKey: pendingKey, operationId, actorId: commandUserId, cargo: [{ resourceId: resourceAId, amount: 10 }], originId, }); reservationIds.push(pending.id); await emitRawEvent(`${RUN}-evt-fail-extract`, "operation.extraction", { operationId, version: 1, actorId: extractorId, kind: "logistics", hqStructureId: hqId, reservationKey: extractedKey, items: [{ resourceId: resourceAId, amount: 4 }], consumed: { cargo: [{ resourceId: resourceAId, amount: 6 }] }, }); await emitRawEvent(`${RUN}-evt-fail-end`, "operation.end", { operationId, version: 1, actorId: commandUserId, outcome: "failed", }); const effects = await effectsForOperation(operationId); expect(effects).toHaveLength(2); const extractionEffect = effects.find((e) => e.effectType === "operation.extraction-recorded"); const endEffect = effects.find((e) => e.effectType === "operation.ended"); expect(extractionEffect?.status).toBe("applied"); expect(endEffect?.status).toBe("applied"); expect(endEffect?.reconciliationNotes).toContain("mission-failed"); // The deposit is final: mission failure never claws it back. expect(await storedAmountAt(hqId, resourceAId)).toBe(4); // The extraction-settled reservation is untouched by the failure sweep. const extractedDoc = await findReservation(extractedKey); expect(extractedDoc?.status).toBe("settled"); expect(settlementOf(extractedDoc as OperationReservation).reason).toBe("extracted"); // The still-reserved row settles as mission-failed via the end effect. const failedDoc = await findReservation(pendingKey); expect(failedDoc?.status).toBe("settled"); const failedRecord = settlementOf(failedDoc as OperationReservation); expect(failedRecord.reason).toBe("mission-failed"); expect(failedRecord.returned.cargo).toEqual([{ resource: resourceAId, amount: 10 }]); expect(failedRecord.effectId).toBe(endEffect?.id); // The extraction effect was never reversed. const freshExtraction = await payload.findByID({ collection: "operation-effects", id: extractionEffect?.id as number, depth: 0, overrideAccess: true, }); expect(freshExtraction.status).toBe("applied"); expect(freshExtraction.reversedBy).toBeNull(); }); it("updates the AAR for an objective-only operation without touching inventory", async () => { const operationId = `${RUN}-op-obj`; const hqId = await makeStructure("hq-obj", hqBlueprintId); const lockerBefore = await lockerOf(extractorId); const countBefore = (lockerBefore?.items ?? []).reduce((sum, e) => sum + e.quantity, 0); await emitRawEvent(`${RUN}-evt-obj`, "operation.objective", { operationId, version: 1, actorId: extractorId, objective: { id: "obj-1", name: "Secure the depot", status: "complete" }, }); const effects = await effectsForOperation(operationId); expect(effects).toHaveLength(1); expect(effects[0].effectType).toBe("operation.objective-updated"); expect(effects[0].status).toBe("applied"); const notifications = await notificationsFor(extractorId, "operation:objective"); expect(notifications).toHaveLength(1); // No inventory movement anywhere. expect(await storedAmountAt(hqId, resourceAId)).toBe(0); const lockerAfter = await lockerOf(extractorId); const countAfter = (lockerAfter?.items ?? []).reduce((sum, e) => sum + e.quantity, 0); expect(countAfter).toBe(countBefore); }); });