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>
667 lines
22 KiB
TypeScript
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);
|
|
});
|
|
});
|