import { eq } from "drizzle-orm"; import { describe, expect, it } from "vitest"; import { buildApp } from "../src/app"; import { companies, dispatches, ships } from "../src/db/schema"; import { RESOLVE_ARRIVALS_JOB, resolveArrivalsJob } from "../src/jobs/definitions"; import { createRecordingQueue } from "../src/jobs/queue"; import { makeOwner, setCredits, useTestDatabase } from "./db"; import { call, getState, putBoard, signUp, testContract, useTestApp } from "./game"; import { testConfig } from "./helpers"; const database = useTestDatabase(); const testApp = useTestApp(database); /** An owner who has just dispatched a 2,000-credit Earth→Moon contract. */ async function dispatched() { const { app } = testApp(); const { db } = database(); const guest = await signUp(app); const { shipId } = await makeOwner(db, guest.playerId, guest.avatarId); await setCredits(db, guest.playerId, 10_000); const contract = testContract(); await putBoard(db, guest.playerId, [contract]); const response = await call(app, guest, "POST", "/api/dispatches", { kind: "contract", contractId: contract.id, shipId, pilotId: guest.avatarId, }); const dispatch = response.json().dispatch as { id: string; arrivalAt: number; fuelCost: number }; return { guest, shipId, dispatch }; } const credits = async (playerId: string) => (await database().db.select().from(companies).where(eq(companies.playerId, playerId)))[0]! .credits; describe("arrival resolution job", () => { it("is scheduled for the dispatch's arrival time", async () => { const { jobs } = testApp(); const { guest, dispatch } = await dispatched(); expect(jobs.sent).toEqual([ { name: RESOLVE_ARRIVALS_JOB, data: { playerId: guest.playerId, dispatchId: dispatch.id }, startAfter: dispatch.arrivalAt, }, ]); }); it("resolves the dispatch at arrival without any player state fetch", async () => { const { clock, jobs } = testApp(); const { db } = database(); const { guest, shipId, dispatch } = await dispatched(); const jobCtx = { db, clock: clock.read }; // Too early: nothing happens. clock.now = dispatch.arrivalAt - 1; await resolveArrivalsJob(jobCtx, jobs.sent[0]!.data); expect((await db.select().from(dispatches))[0]!.status).toBe("in_flight"); clock.now = dispatch.arrivalAt; await resolveArrivalsJob(jobCtx, jobs.sent[0]!.data); const [row] = await db.select().from(dispatches).where(eq(dispatches.id, dispatch.id)); expect(row!.status).toBe("resolved"); expect(await credits(guest.playerId)).toBe(10_000 - dispatch.fuelCost + 2000); const [ship] = await db.select().from(ships).where(eq(ships.id, shipId)); expect(ship).toMatchObject({ status: "idle", locationId: "moon" }); }); it("does nothing if lazy resolution already happened", async () => { const { app, clock, jobs } = testApp(); const { db } = database(); const { guest, dispatch } = await dispatched(); clock.now = dispatch.arrivalAt + 1000; const state = await getState(app, guest); await resolveArrivalsJob({ db, clock: clock.read }, jobs.sent[0]!.data); expect(await credits(guest.playerId)).toBe(state.company.credits); expect((await getState(app, guest)).summaries).toHaveLength(1); }); }); describe("test-only clock offset endpoint", () => { it("shifts the server clock outside production", async () => { const { app, clock } = testApp(); const response = await app.inject({ method: "POST", url: "/api/test/clock-offset", payload: { offsetMs: 3_600_000 }, }); expect(response.statusCode).toBe(200); const time = (await app.inject({ method: "GET", url: "/api/time" })).json(); expect(time.realTime).toBe(clock.now + 3_600_000); }); it("schedules arrival jobs on the real clock while an offset is set", async () => { const { app, jobs } = testApp(); await app.inject({ method: "POST", url: "/api/test/clock-offset", payload: { offsetMs: 60_000 }, }); const { dispatch } = await dispatched(); expect(jobs.sent[0]!.startAfter).toBe(dispatch.arrivalAt - 60_000); }); it("does not exist in production", async () => { const app = buildApp(testConfig({ NODE_ENV: "production" }), { database: database(), jobs: createRecordingQueue(), }); const response = await app.inject({ method: "POST", url: "/api/test/clock-offset", payload: { offsetMs: 1 }, }); expect(response.statusCode).toBe(404); await app.close(); }); });