1
0
Fork 0
polaris-task-force/tests/int/shipment-lifecycle.int.spec.ts
Z8MB1E f8ce3f3dcc feat(logistics): timestamp shipment lifecycle with client relay
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

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

339 lines
10 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import config from "@payload-config";
import { getPayload, type Payload } from "payload";
import type {
GameStructure,
GameVehicle,
Map as MapDoc,
Resource,
Shipment,
Vehicle,
} from "@/payload-types";
import { advanceShipment } from "@/lib/shipping/lifecycle";
import { emitGameEvent } from "@/utils/event-log/emit";
import { EventTypes } from "@/utils/event-log/eventTypes";
const LEXICAL_EMPTY = {
root: {
children: [
{
type: "paragraph",
children: [
{ type: "text", detail: 0, format: 0, mode: "normal", style: "", text: "", version: 1 },
],
},
{
type: "paragraph",
children: [
{ type: "text", detail: 0, format: 0, mode: "normal", style: "", text: "", version: 1 },
],
},
],
direction: null,
format: "",
indent: 0,
type: "root",
version: 1,
},
} as const;
describe("shipment lifecycle (advanceShipment)", () => {
let payload: Payload;
let origin: GameStructure;
let destination: GameStructure;
let blueprint: Vehicle;
let fuel: Resource;
const makeShipment = async (data: {
dispatchedAt: Date;
estimatedArrival: Date;
fuelCost?: number;
vehicleRef: GameVehicle;
}): Promise<Shipment> => {
return (await payload.create({
collection: "shipments",
data: {
name: "SHP-test",
origin: origin.id,
destination: destination.id,
map: (origin.map as number) ?? null,
routePath: null,
transportVehicle: data.vehicleRef.id,
distance: 10000,
cargo: [
{
item: { relationTo: "resources", value: fuel.id },
amount: 25,
},
],
fuelCost: data.fuelCost ?? 60,
fuelConsumed: 0,
status: "dispatched",
dispatchedAt: data.dispatchedAt.toISOString(),
estimatedArrival: data.estimatedArrival.toISOString(),
autoReturn: false,
createdBy: null,
},
overrideAccess: true,
depth: 0,
})) as unknown as Shipment;
};
const makeVehicle = async (fuelAmount = 100): Promise<GameVehicle> =>
(await payload.create({
collection: "game-vehicles",
data: {
name: `Lifecycle Truck ${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
type: blueprint.id,
deployedAt: origin.id,
status: "in_transit",
currentFuel: fuelAmount,
currentHealth: 100,
},
overrideAccess: true,
depth: 0,
})) as unknown as GameVehicle;
const loadActive = async (id: number) =>
payload.findByID({ collection: "shipments", id, depth: 2, overrideAccess: true });
const getShipment = (id: number) =>
payload.findByID({
collection: "shipments",
id,
depth: 0,
overrideAccess: true,
}) as Promise<Shipment>;
const getVehicle = (id: number) =>
payload.findByID({
collection: "game-vehicles",
id,
depth: 0,
overrideAccess: true,
}) as Promise<GameVehicle>;
beforeAll(async () => {
payload = await getPayload({ config });
const rules = await payload.findGlobal({ slug: "game-rules" });
expect(rules).toBeTruthy();
const map = (await payload.create({
collection: "maps",
data: {
name: `Lifecycle Map ${Date.now()}`,
worldSizeWidth: 20480,
worldSizeHeight: 20480,
basemapMode: "tiles",
tileUrlTemplate: "http://localhost/{z}/{x}/{y}.png",
isActive: false,
},
overrideAccess: true,
depth: 0,
})) as unknown as MapDoc;
fuel = (await payload.create({
collection: "resources",
data: {
name: `Diesel ${Date.now()}`,
codeName: `lifecycle_diesel_${Date.now()}`,
approvalStatus: "in_progress",
type: "physical",
baseValue: 1,
rarity: "common",
unitOfMeasure: "kg",
massPerUnit: 1,
gridWidth: 1,
gridHeight: 1,
},
overrideAccess: true,
depth: 0,
})) as unknown as Resource;
const blueprintType = (await payload.create({
collection: "structures",
data: {
name: "Lifecycle Depot",
codeName: "lifecycle_depot",
approvalStatus: "in_progress",
description: LEXICAL_EMPTY as unknown as never,
category: "logistics",
materials: [{ resource: fuel.id, amount: 1 }],
constructionDurationMinutes: 60,
terrainType: "land",
maxHealth: 100,
},
overrideAccess: true,
depth: 0,
})) as unknown as { id: number };
origin = (await payload.create({
collection: "game-structures",
data: {
name: "Lifecycle Origin",
type: blueprintType.id,
map: map.id,
coordinates: [2000, 2000],
},
overrideAccess: true,
depth: 0,
})) as unknown as GameStructure;
destination = (await payload.create({
collection: "game-structures",
data: {
name: "Lifecycle Destination",
type: blueprintType.id,
map: map.id,
coordinates: [18000, 18000],
},
overrideAccess: true,
depth: 0,
})) as unknown as GameStructure;
blueprint = (await payload.create({
collection: "vehicles",
data: {
name: "Lifecycle Truck",
approvalStatus: "in_progress",
transportMode: "ground",
fuel: { fuelType: fuel.id, fuelCapacity: 100000, fuelConsumptionRate: 0.01 },
maxSpeedOnRoad: 60,
},
overrideAccess: true,
depth: 0,
})) as unknown as Vehicle;
});
afterAll(async () => {
await emitGameEvent(payload, {
type: EventTypes.ShipmentComplete,
message: "lifecycle test teardown marker",
targetCollection: "shipments",
targetId: 0,
}).catch(() => undefined);
});
it("flips dispatched to in_transit with continuous fuel burn derived from elapsed time", async () => {
const now = new Date();
const truck = await makeVehicle();
const shipment = await makeShipment({
dispatchedAt: new Date(now.getTime() - 30 * 60 * 1000),
estimatedArrival: new Date(now.getTime() + 30 * 60 * 1000),
vehicleRef: truck,
});
await advanceShipment(payload, (await loadActive(shipment.id)) as never, now);
const after = await getShipment(shipment.id);
expect(after.status).toBe("in_transit");
expect(after.fuelConsumed).toBeCloseTo(30, 5);
const afterVehicle = await getVehicle(truck.id);
expect(afterVehicle.currentFuel).toBeCloseTo(70, 5);
});
it("is idempotent: advancing twice at the same instant does not double-burn fuel", async () => {
const now = new Date();
const truck = await makeVehicle();
const shipment = await makeShipment({
dispatchedAt: new Date(now.getTime() - 30 * 60 * 1000),
estimatedArrival: new Date(now.getTime() + 30 * 60 * 1000),
vehicleRef: truck,
});
await advanceShipment(payload, (await loadActive(shipment.id)) as never, now);
await advanceShipment(payload, (await loadActive(shipment.id)) as never, now);
const after = await getShipment(shipment.id);
expect(after.fuelConsumed).toBeCloseTo(30, 5);
const afterVehicle = await getVehicle(truck.id);
expect(afterVehicle.currentFuel).toBeCloseTo(70, 5);
});
it("completes a due shipment on demand: cargo credited, vehicle idle at destination", async () => {
const now = new Date();
const truck = await makeVehicle();
const shipment = await makeShipment({
dispatchedAt: new Date(now.getTime() - 61 * 60 * 1000),
estimatedArrival: new Date(now.getTime() - 60 * 1000),
vehicleRef: truck,
});
await advanceShipment(payload, (await loadActive(shipment.id)) as never, now);
const after = await getShipment(shipment.id);
expect(after.status).toBe("completed");
expect(after.fuelConsumed).toBeCloseTo(60, 5);
expect(after.completedAt).toBeTruthy();
const afterVehicle = await getVehicle(truck.id);
expect(afterVehicle.status).toBe("idle");
expect(afterVehicle.deployedAt).toBe(destination.id);
const dest = (await payload.findByID({
collection: "game-structures",
id: destination.id,
depth: 0,
overrideAccess: true,
})) as unknown as GameStructure;
const credited = (dest.storedResources ?? []).find(
(r) => (typeof r.resource === "object" ? r.resource.id : r.resource) === fuel.id,
);
expect(credited?.amount).toBe(25);
});
it("is a no-op for shipments that already reached a terminal state", async () => {
const now = new Date();
const truck = await makeVehicle();
const shipment = await makeShipment({
dispatchedAt: new Date(now.getTime() - 61 * 60 * 1000),
estimatedArrival: new Date(now.getTime() - 60 * 1000),
vehicleRef: truck,
});
await advanceShipment(payload, (await loadActive(shipment.id)) as never, now);
const completed = await getShipment(shipment.id);
await advanceShipment(payload, completed as never, now);
const after = await getShipment(shipment.id);
expect(after.status).toBe("completed");
expect(after.fuelConsumed).toBeCloseTo(60, 5);
const afterVehicle = await getVehicle(truck.id);
expect(afterVehicle.currentFuel).toBeCloseTo(40, 5);
});
it("strands the shipment when the vehicle cannot cover the elapsed burn", async () => {
const now = new Date();
const dryVehicle = (await payload.create({
collection: "game-vehicles",
data: {
name: "Lifecycle Dry Truck",
type: blueprint.id,
deployedAt: origin.id,
status: "in_transit",
currentFuel: 5,
currentHealth: 100,
},
overrideAccess: true,
depth: 0,
})) as unknown as GameVehicle;
const shipment = await makeShipment({
dispatchedAt: new Date(now.getTime() - 30 * 60 * 1000),
estimatedArrival: new Date(now.getTime() + 30 * 60 * 1000),
vehicleRef: dryVehicle,
});
await advanceShipment(payload, (await loadActive(shipment.id)) as never, now);
const after = await getShipment(shipment.id);
expect(after.status).toBe("stranded");
expect(after.failureReason).toContain("fuel");
const afterVehicle = await getVehicle(dryVehicle.id);
expect(afterVehicle.status).toBe("damaged");
});
});