import { describe, expect, it } from "vitest"; import { withPlayerLock } from "../src/game/lock"; import { resolveArrivedDispatches } from "../src/game/resolve"; import { makeOwner, setCredits, useTestDatabase } from "./db"; import { getState, insertDispatch, signUp, testContract, useTestApp } from "./game"; const database = useTestDatabase(); const testApp = useTestApp(database); /** An owner (one Courier at Earth, crewed by the avatar) with 1,000 credits. */ async function owner() { const { app } = testApp(); const guest = await signUp(app); const { shipId } = await makeOwner(database().db, guest.playerId, guest.avatarId); await setCredits(database().db, guest.playerId, 1000); return { guest, shipId }; } describe("lazy resolution on read", () => { it("applies a past arrival's credits, XP, ship location and discovery", async () => { const { app, clock } = testApp(); const { guest, shipId } = await owner(); // A delivery to Mars, which the player has not discovered yet. const contract = testContract({ destinationId: "mars", payout: 5000 }); const record = await insertDispatch(database().db, { playerId: guest.playerId, shipId, pilotId: guest.avatarId, departAt: clock.now, contract, }); clock.now = record.arrivalAt + 60_000; const state = await getState(app, guest); // The avatar flew it, so no pilot pay is deducted. expect(state.company.credits).toBe(1000 + 5000); expect(state.pilots[0]!.xp).toBeGreaterThan(0); expect(state.pilots[0]!.status).toBe("idle"); expect(state.ships[0]).toMatchObject({ status: "idle", locationId: "mars", dispatchId: null }); expect(state.locations.find((l) => l.id === "mars")!.discovered).toBe(true); expect(state.activeDispatches).toHaveLength(0); expect(state.summaries).toEqual([ expect.objectContaining({ dispatchId: record.id, credits: 5000, discoveredLocationId: "mars", }), ]); }); it("leaves dispatches that have not arrived yet in flight", async () => { const { app, clock } = testApp(); const { guest, shipId } = await owner(); const record = await insertDispatch(database().db, { playerId: guest.playerId, shipId, pilotId: guest.avatarId, departAt: clock.now, }); clock.now = record.arrivalAt - 1; const state = await getState(app, guest); expect(state.company.credits).toBe(1000); expect(state.ships[0]).toMatchObject({ status: "in-flight", locationId: null, dispatchId: record.id, }); expect(state.activeDispatches.map((d) => d.id)).toEqual([record.id]); }); it("does not apply an arrival twice on a second fetch", async () => { const { app, clock } = testApp(); const { guest, shipId } = await owner(); const record = await insertDispatch(database().db, { playerId: guest.playerId, shipId, pilotId: guest.avatarId, departAt: clock.now, }); clock.now = record.arrivalAt; const first = await getState(app, guest); const second = await getState(app, guest); expect(second.company.credits).toBe(first.company.credits); expect(second.pilots[0]!.xp).toBe(first.pilots[0]!.xp); expect(first.company.credits).toBe(1000 + 2000); }); it("applies an arrival exactly once under two concurrent fetches", async () => { const { app, clock } = testApp(); const { guest, shipId } = await owner(); const record = await insertDispatch(database().db, { playerId: guest.playerId, shipId, pilotId: guest.avatarId, departAt: clock.now, }); clock.now = record.arrivalAt; const [a, b] = await Promise.all([getState(app, guest), getState(app, guest)]); expect(a.company.credits).toBe(3000); expect(b.company.credits).toBe(3000); }); it("resolves exactly once when two resolvers race outside HTTP", async () => { const { clock } = testApp(); const { guest, shipId } = await owner(); const { db } = database(); const record = await insertDispatch(db, { playerId: guest.playerId, shipId, pilotId: guest.avatarId, departAt: clock.now, }); const run = () => withPlayerLock(db, guest.playerId, (tx) => resolveArrivedDispatches(tx, guest.playerId, record.arrivalAt), ); const counts = await Promise.all([run(), run(), run()]); expect(counts.reduce((a, b) => a + b, 0)).toBe(1); }); it("resolves several arrivals in arrival order", async () => { const { app, clock } = testApp(); const { guest, shipId } = await owner(); const { db } = database(); // Fly to the Moon, then (already resolved in the DB as of arrival) on to Earth. const first = await insertDispatch(db, { playerId: guest.playerId, shipId, pilotId: guest.avatarId, departAt: clock.now, }); clock.now = first.arrivalAt; await getState(app, guest); const second = await insertDispatch(db, { playerId: guest.playerId, shipId, pilotId: guest.avatarId, departAt: first.arrivalAt, contract: testContract({ originId: "moon", destinationId: "earth", payout: 700 }), }); clock.now = second.arrivalAt + 1; const state = await getState(app, guest); expect(state.company.credits).toBe(1000 + 2000 + 700); expect(state.ships[0]!.locationId).toBe("earth"); expect(state.summaries).toHaveLength(2); }); });