1
0
Fork 0
polaris-task-force/tests/int/operation-economy.int.spec.ts
Z8MB1E 67adaa301c feat(operations): bounded economy effects and market repair
Link operation loot to the supply/demand ledger with faucet/drain caps and provenance-marked notes; repair buyListing to preflight locker credit before payment with a compensating refund on credit failure.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-20 02:43:53 -04:00

667 lines
22 KiB
TypeScript

import { getPayload, Payload } from "payload";
import config from "@/payload.config";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { applyEconomyEffect, DEFAULT_ECONOMY_CAPS } from "@/lib/operations/economy";
import { ECONOMY_NOTE_PREFIX, type EconomyRecord } from "@/lib/operations/economyConfig";
import { resetMarketStateDocs } from "@/lib/economy/consumption";
import { applyTransaction, ensurePersonalAccount } from "@/lib/banking";
import { ensureLockerStorage } from "@/lib/locker";
import type { Asset, GameStructure, LockerStorage, MarketListing, Structure } from "@/payload-types";
let payload: Payload;
const RUN = `econ-${Date.now().toString(36)}`;
const LEXICAL_EMPTY = {
root: {
children: [{ text: "" }],
direction: null,
format: "" as const,
indent: 0,
type: "text",
version: 1,
},
};
interface EconomyNote {
applied: { itemType: string; itemId: number; supply: number }[];
rejected: { itemType: string; itemId: number; supply: number }[];
provenance: { itemType: string; itemId: number }[];
error?: string;
}
function economyRecordFrom(effect: { reconciliationNotes: string | null }): EconomyRecord | null {
const notes = effect.reconciliationNotes ?? "";
const line = notes.split("\n").find((l) => l.startsWith(ECONOMY_NOTE_PREFIX));
if (!line) return null;
return JSON.parse(line.slice(ECONOMY_NOTE_PREFIX.length)) as EconomyRecord;
}
// ---------------------------------------------------------------------------
// Fixture tracking for cleanup
// ---------------------------------------------------------------------------
const syncEventIds: number[] = [];
const opEventIds: number[] = [];
const opEffectIds: number[] = [];
const userIds: number[] = [];
const accountIds: number[] = [];
const gameStructureIds: number[] = [];
const blueprintIds: number[] = [];
const resourceIds: number[] = [];
const assetIds: number[] = [];
const marketStateIds: number[] = [];
const listingIds: number[] = [];
const lockerIds: number[] = [];
let serverId: number;
let mapId: number;
let actorId: number;
let sellerId: number;
let buyerId: number;
let resourceAlphaId: number;
let resourceBetaId: number;
let tradeableAssetId: number;
let fillerAssetId: number;
let hqId: number;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
async function supplyOf(itemType: "resource" | "asset", itemId: number): Promise<number | null> {
const docs = await payload.find({
collection: "market-state",
where: { and: [{ itemType: { equals: itemType } }, { itemId: { equals: itemId } }] },
limit: 1,
depth: 0,
overrideAccess: true,
});
if (docs.docs.length === 0) return null;
return (docs.docs[0] as { currentSupply: number }).currentSupply;
}
async function makeMarketState(itemType: "resource" | "asset", itemId: number): Promise<number> {
const existing = await payload.find({
collection: "market-state",
where: { and: [{ itemType: { equals: itemType } }, { itemId: { equals: itemId } }] },
limit: 1,
depth: 0,
overrideAccess: true,
});
if (existing.docs.length > 0) return existing.docs[0].id;
const doc = await payload.create({
collection: "market-state",
data: {
itemType,
itemId,
gmBaselineSupply: 100,
gmBaselineDemand: 50,
currentSupply: 100,
artificialDemand: 50,
realDemand: 0,
priceModifier: 1,
},
overrideAccess: true,
depth: 0,
});
marketStateIds.push(doc.id);
return doc.id;
}
/**
* Full hook chain: creating the raw sync event runs the afterChange hook,
* which validates it into an operation-event, derives the pending effect, and
* applies settlement. Returns the applied effect.
*/
async function hookExtraction(
label: string,
payloadManifest: Record<string, unknown>,
): Promise<{ effectId: number; eventId: string }> {
const sync = await payload.create({
collection: "arma-sync-events",
data: {
eventId: `${RUN}-${label}`,
server: serverId,
type: "operation.extraction",
occurredAt: new Date().toISOString(),
payload: { operationId: RUN, version: 1, ...payloadManifest },
},
overrideAccess: true,
depth: 0,
});
syncEventIds.push(sync.id);
const events = await payload.find({
collection: "operation-events",
where: { sourceEvent: { equals: sync.id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
expect(events.docs).toHaveLength(1);
opEventIds.push(events.docs[0].id);
const effects = await payload.find({
collection: "operation-effects",
where: { operationEvent: { equals: events.docs[0].id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
expect(effects.docs).toHaveLength(1);
const effect = effects.docs[0] as { id: number; status: string };
opEffectIds.push(effect.id);
expect(effect.status).toBe("applied");
return { effectId: effect.id, eventId: sync.eventId as string };
}
/** Create an applied extraction effect for adapter-level tests. The afterChange
* hook already consumes every raw sync event (unique sourceEvent), so instead
* of creating a second operation-event, the hook-derived effect row is
* repurposed with an extraction type and manifest. */
async function manualExtractionEffect(label: string, resourceId: number, amount: number): Promise<number> {
const sync = await payload.create({
collection: "arma-sync-events",
data: {
eventId: `${RUN}-${label}`,
server: serverId,
type: "operation.start",
occurredAt: new Date().toISOString(),
payload: { operationId: RUN, version: 1 },
},
overrideAccess: true,
depth: 0,
});
syncEventIds.push(sync.id);
const events = await payload.find({
collection: "operation-events",
where: { sourceEvent: { equals: sync.id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
expect(events.docs).toHaveLength(1);
opEventIds.push(events.docs[0].id);
const effects = await payload.find({
collection: "operation-effects",
where: { operationEvent: { equals: events.docs[0].id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
expect(effects.docs).toHaveLength(1);
const effect = effects.docs[0] as { id: number };
opEffectIds.push(effect.id);
const updated = await payload.update({
collection: "operation-effects",
id: effect.id,
data: {
effectType: "operation.extraction-recorded",
payload: {
operationId: RUN,
kind: "logistics",
hqStructureId: hqId,
actorId,
items: [{ resourceId, amount }],
},
status: "applied",
appliedAt: new Date().toISOString(),
},
overrideAccess: true,
depth: 0,
});
return updated.id;
}
// ---------------------------------------------------------------------------
// Setup / teardown
// ---------------------------------------------------------------------------
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,
});
serverId = server.id;
for (const label of ["actor", "seller", "buyer"] as const) {
const user = await payload.create({
collection: "users",
data: {
username: `${RUN}-${label}`,
discordUsername: `${RUN}-${label}-discord`,
displayName: `${RUN} ${label}`,
steamId: `${RUN}-steam-${label}`,
password: "Test1234",
},
overrideAccess: true,
depth: 0,
});
userIds.push(user.id);
if (label === "actor") actorId = user.id;
if (label === "seller") sellerId = user.id;
if (label === "buyer") buyerId = user.id;
}
const map = await payload.create({
collection: "maps",
data: { name: `${RUN} Map`, worldSizeWidth: 8192, worldSizeHeight: 8192, basemapMode: "image" },
overrideAccess: true,
depth: 0,
});
mapId = map.id;
for (const [label, setter] of [
["alpha", (id: number) => (resourceAlphaId = id)],
["beta", (id: number) => (resourceBetaId = id)],
] as const) {
const resource = await payload.create({
collection: "resources",
data: {
name: `${RUN} ${label}`,
codeName: `${RUN}-${label}`,
approvalStatus: "in_progress",
type: "physical",
baseValue: 1,
rarity: "common",
unitOfMeasure: "kg",
massPerUnit: 1,
gridWidth: 1,
gridHeight: 1,
},
overrideAccess: true,
depth: 0,
});
resourceIds.push(resource.id);
setter(resource.id);
}
const tradeableAsset = await payload.create({
collection: "assets",
data: {
name: `${RUN} Tradeable Rifle`,
className: `${RUN}_tradeable_rifle`,
assetType: "weapon",
approvalStatus: "in_progress",
tradeable: true,
isLive: true,
crafting: { craftable: false, craftingData: { craftingTimePerUnit: 1, batchSize: 1 } },
storageDimensions: { gridWidth: 1, gridHeight: 1, stackable: false },
},
overrideAccess: true,
depth: 0,
});
assetIds.push(tradeableAsset.id);
tradeableAssetId = tradeableAsset.id;
const fillerAsset = await payload.create({
collection: "assets",
data: {
name: `${RUN} Filler Rifle`,
className: `${RUN}_filler_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,
});
assetIds.push(fillerAsset.id);
fillerAssetId = fillerAsset.id;
const blueprint = 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: resourceAlphaId, amount: 1 }],
constructionDurationMinutes: 1,
terrainType: "land",
maxHealth: 100,
},
overrideAccess: true,
depth: 0,
});
blueprintIds.push(blueprint.id);
const hq = await payload.create({
collection: "game-structures",
data: {
name: `${RUN} HQ`,
type: blueprint.id,
map: mapId,
coordinates: [100, 100],
constructionStatus: "complete",
},
overrideAccess: true,
depth: 0,
});
gameStructureIds.push(hq.id);
hqId = hq.id;
});
afterAll(async () => {
if (!payload) return;
for (const id of listingIds) {
await payload.delete({ collection: "market-listings", id, overrideAccess: true }).catch(() => {});
}
for (const id of marketStateIds) {
await payload.delete({ collection: "market-state", 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 lockerIds) {
await payload.delete({ collection: "locker-storages", id, overrideAccess: true }).catch(() => {});
}
const deleteAccountChain = async (accountId: number) => {
const txns = await payload.find({
collection: "bank-transactions",
where: { or: [{ fromAccount: { equals: accountId } }, { toAccount: { equals: accountId } }] },
limit: 100,
depth: 0,
overrideAccess: true,
});
for (const txn of txns.docs) {
await payload.delete({ collection: "bank-transactions", id: txn.id, overrideAccess: true }).catch(() => {});
}
await payload.delete({ collection: "bank-accounts", id: accountId, overrideAccess: true }).catch(() => {});
};
for (const id of accountIds) await deleteAccountChain(id);
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 userIds) {
await payload.delete({ collection: "users", id, overrideAccess: true }).catch(() => {});
}
await payload.delete({ collection: "maps", id: mapId, overrideAccess: true }).catch(() => {});
});
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("Operation economy adapter", () => {
it("feeds a logistics extraction into the resource market supply exactly once", async () => {
await makeMarketState("resource", resourceAlphaId);
const before = (await supplyOf("resource", resourceAlphaId)) as number;
const { effectId } = await hookExtraction("logi", {
kind: "logistics",
hqStructureId: hqId,
actorId,
items: [{ resourceId: resourceAlphaId, amount: 5 }],
});
const result = await applyEconomyEffect(payload, effectId);
const record = economyRecordFrom(result as { reconciliationNotes: string | null });
expect(record).not.toBeNull();
expect(record?.applied).toEqual([
{ itemType: "resource", itemId: resourceAlphaId, supply: 5 },
]);
expect(record?.rejected).toEqual([]);
expect(await supplyOf("resource", resourceAlphaId)).toBe(before + 5);
});
it("is a no-op when the same effect is applied twice", async () => {
const effectId = await manualExtractionEffect("dup", resourceAlphaId, 5);
const before = (await supplyOf("resource", resourceAlphaId)) as number;
await applyEconomyEffect(payload, effectId);
const afterFirst = (await supplyOf("resource", resourceAlphaId)) as number;
const second = await applyEconomyEffect(payload, effectId);
expect(await supplyOf("resource", resourceAlphaId)).toBe(afterFirst);
expect(await supplyOf("resource", resourceAlphaId)).toBe(before + 5);
// The marker is present exactly once: the retry did not append a second record.
expect((second as { reconciliationNotes: string }).reconciliationNotes.split(ECONOMY_NOTE_PREFIX).length - 1).toBe(1);
});
it("records player extractions as provenance without touching the market", async () => {
await makeMarketState("asset", tradeableAssetId);
const before = (await supplyOf("asset", tradeableAssetId)) as number;
const { effectId } = await hookExtraction("player", {
kind: "player",
actorId: buyerId,
items: [{ assetId: tradeableAssetId, quantity: 2 }],
});
const result = await applyEconomyEffect(payload, effectId);
const record = economyRecordFrom(result as { reconciliationNotes: string | null });
expect(record?.applied).toEqual([]);
expect(record?.provenance).toEqual([{ itemType: "asset", itemId: tradeableAssetId }]);
expect(await supplyOf("asset", tradeableAssetId)).toBe(before);
});
it("drains the market supply for inventory losses", async () => {
await makeMarketState("asset", tradeableAssetId);
const before = (await supplyOf("asset", tradeableAssetId)) as number;
const sync = await payload.create({
collection: "arma-sync-events",
data: {
eventId: `${RUN}-loss`,
server: serverId,
type: "operation.inventory",
occurredAt: new Date().toISOString(),
payload: { operationId: RUN, version: 1, reason: "loss", items: [{ assetId: tradeableAssetId, quantity: 3 }] },
},
overrideAccess: true,
depth: 0,
});
syncEventIds.push(sync.id);
const events = await payload.find({
collection: "operation-events",
where: { sourceEvent: { equals: sync.id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
opEventIds.push(events.docs[0].id);
const effects = await payload.find({
collection: "operation-effects",
where: { operationEvent: { equals: events.docs[0].id } },
limit: 1,
depth: 0,
overrideAccess: true,
});
opEffectIds.push(effects.docs[0].id);
const effectId = effects.docs[0].id;
const result = await applyEconomyEffect(payload, effectId);
const record = economyRecordFrom(result as { reconciliationNotes: string | null });
expect(record?.applied).toEqual([
{ itemType: "asset", itemId: tradeableAssetId, supply: -3 },
]);
expect(await supplyOf("asset", tradeableAssetId)).toBe(before - 3);
});
it("caps excess supply and records the rejected remainder", async () => {
const effectId = await manualExtractionEffect("cap", resourceAlphaId, 5000);
const before = (await supplyOf("resource", resourceAlphaId)) as number;
const result = await applyEconomyEffect(payload, effectId, { faucetCapPerItem: 100 });
const record = economyRecordFrom(result as { reconciliationNotes: string | null });
expect(record?.applied).toEqual([
{ itemType: "resource", itemId: resourceAlphaId, supply: 100 },
]);
expect(record?.rejected).toEqual([
{ itemType: "resource", itemId: resourceAlphaId, supply: 4900 },
]);
expect(await supplyOf("resource", resourceAlphaId)).toBe(before + 100);
});
it("keeps items without a market-state ledger provenance-eligible only", async () => {
const effectId = await manualExtractionEffect("nol", resourceBetaId, 7);
const result = await applyEconomyEffect(payload, effectId);
const record = economyRecordFrom(result as { reconciliationNotes: string | null });
expect(record?.applied).toEqual([]);
expect(record?.provenance).toEqual([{ itemType: "resource", itemId: resourceBetaId }]);
const docs = await payload.find({
collection: "market-state",
where: { itemId: { equals: resourceBetaId } },
limit: 1,
depth: 0,
overrideAccess: true,
});
expect(docs.docs).toHaveLength(0);
});
it("rejects a purchase into a full locker before any payment moves", async () => {
const listing = await payload.create({
collection: "market-listings",
data: {
asset: tradeableAssetId,
seller: sellerId,
quantity: 2,
price: 100,
status: "active",
listedAt: new Date().toISOString(),
},
overrideAccess: true,
depth: 0,
});
listingIds.push(listing.id);
const buyerAccount = await ensurePersonalAccount(payload, buyerId);
accountIds.push(buyerAccount.id);
await applyTransaction(payload, {
type: "deposit",
toAccountId: buyerAccount.id,
amount: 10000,
memo: `${RUN} buyer funding`,
});
const buyerAccountAfterFunding = (await payload.findByID({
collection: "bank-accounts",
id: buyerAccount.id,
depth: 0,
overrideAccess: true,
})) as { balance: number };
expect(buyerAccountAfterFunding.balance).toBe(10000);
const buyerLocker = await ensureLockerStorage(payload, buyerId);
lockerIds.push(buyerLocker.id);
const grid = await payload.findByID({
collection: "locker-storages",
id: buyerLocker.id,
depth: 0,
overrideAccess: true,
});
const entries = Array.from({ length: 60 }, (_, i) => ({
asset: fillerAssetId,
quantity: 1,
gridX: i % 10,
gridY: Math.floor(i / 10),
rotated: false,
}));
await payload.update({
collection: "locker-storages",
id: buyerLocker.id,
data: { items: entries },
overrideAccess: true,
depth: 0,
});
expect((grid as unknown as LockerStorage).items).toBeDefined();
const buyerUser = (await payload.findByID({
collection: "users",
id: buyerId,
depth: 0,
overrideAccess: true,
})) as { username: string };
await expect(
(async () => {
const { buyListing } = await import("@/lib/market");
const listingDoc = (await payload.findByID({
collection: "market-listings",
id: listing.id,
depth: 1,
overrideAccess: true,
})) as unknown as MarketListing;
const buyerDoc = (await payload.findByID({
collection: "users",
id: buyerId,
depth: 0,
overrideAccess: true,
})) as never;
await buyListing(payload, listingDoc, buyerDoc, { quantity: 1 });
})(),
).rejects.toThrow();
const after = (await payload.findByID({
collection: "bank-accounts",
id: buyerAccount.id,
depth: 0,
overrideAccess: true,
})) as { balance: number };
expect(after.balance).toBe(10000);
const stillActive = (await payload.findByID({
collection: "market-listings",
id: listing.id,
depth: 0,
overrideAccess: true,
})) as { status: string };
expect(stillActive.status).toBe("active");
expect(buyerUser.username).toContain(RUN);
expect((await payload.findByID({ collection: "game-structures", id: hqId, depth: 0, overrideAccess: true })) as GameStructure).toBeDefined();
});
it("rebuilds market baselines on reset while operation provenance survives", async () => {
const before = (await supplyOf("resource", resourceAlphaId)) as number;
const result = await resetMarketStateDocs(payload, {
items: [{ itemType: "resource", itemId: resourceAlphaId }],
});
expect(result.reset).toBe(1);
const after = (await supplyOf("resource", resourceAlphaId)) as number;
expect(after).toBe(100);
expect(before).toBeGreaterThanOrEqual(100);
const effects = await payload.find({
collection: "operation-effects",
where: { operationId: { equals: RUN } },
limit: 10,
depth: 0,
overrideAccess: true,
});
expect(effects.docs.length).toBeGreaterThan(0);
const withNote = effects.docs.filter((d) =>
((d as { reconciliationNotes: string | null }).reconciliationNotes ?? "").includes(ECONOMY_NOTE_PREFIX),
);
expect(withNote.length).toBeGreaterThan(0);
});
it("exposes the default caps constant for surfaces", () => {
expect(DEFAULT_ECONOMY_CAPS.faucetCapPerItem).toBeGreaterThan(0);
expect(DEFAULT_ECONOMY_CAPS.drainCapPerItem).toBeGreaterThan(0);
});
});