// Ported from apps/server/test (CR-014 Step 14.5): now runs on the engine. import { content } from "@deadhead/content"; import { contractBoardResponseSchema, errorResponseSchema } from "@deadhead/schema"; import { boardPeriodMs, createDispatchRecord, type Contract } from "@deadhead/sim"; import { describe, expect, it } from "vitest"; import { useTestApp, signUp, call, edit, readSave, makeOwner, type Guest } from "../support"; const randomUUID = () => globalThis.crypto.randomUUID(); const testApp = useTestApp(); async function board(guest: Guest) { const response = await call(testApp().app, guest, "GET", "/api/tutorial/board"); expect(response.statusCode).toBe(200); return contractBoardResponseSchema.parse(response.json()); } /** Records a contract as accepted, with the tutorial dispatch it would create. */ async function accept(guest: Guest, contract: Contract, now: number) { const id = randomUUID(); const record = createDispatchRecord( { id, ship: { id: "loaner", modelId: "courier", locationId: contract.originId, inFlight: false }, pilot: { id: guest.avatarId, isAvatar: true, assignedShipId: null, inFlight: false, skills: {}, restEndsAt: null, }, contract, tutorial: true, now, worldSeed: 1, }, content, ); await edit(testApp().app, (save) => { save.dispatches.push({ id, shipId: null, pilotId: guest.avatarId, tutorial: true, record, arrivalAt: record.arrivalAt, status: "in-flight", resolution: null, resolvedAt: null, acknowledgedAt: null, }); save.acceptedContracts.push({ contractId: contract.id, periodIndex: 0, dispatchId: id, acceptedAt: now, }); }); } describe("tutorial board endpoint", () => { it("gives a tutorial player at Earth a full board of Earth→Moon contracts", async () => { const { app, clock } = testApp(); const guest = await signUp(app); const { contracts, periodEndsAt } = await board(guest); expect(contracts).toHaveLength(content.config.tutorialBoardSize); for (const c of contracts) { expect([c.originId, c.destinationId]).toEqual(["earth", "moon"]); expect(c.cargoTypeId).toBe("standard"); } expect(periodEndsAt).toBeGreaterThan(clock.now); expect(periodEndsAt - clock.now).toBeLessThanOrEqual(boardPeriodMs(content.config)); }); it("offers Moon→Earth when the loaner is at the Moon", async () => { const { app } = testApp(); const guest = await signUp(app); await edit(app, (save) => { save.player.loanerLocationId = "moon"; }); for (const c of (await board(guest)).contracts) { expect([c.originId, c.destinationId]).toEqual(["moon", "earth"]); } }); it("stores the board: repeated requests in a period return the same contracts", async () => { const { clock } = testApp(); const guest = await signUp(testApp().app); const first = await board(guest); clock.advance(60_000); expect((await board(guest)).contracts).toEqual(first.contracts); expect((await readSave(testApp().app)).contractBoards).toHaveLength(1); }); it("hides contracts the player has already accepted", async () => { const { clock } = testApp(); const guest = await signUp(testApp().app); const before = await board(guest); await accept(guest, before.contracts[0]!, clock.now); const after = await board(guest); expect(after.contracts).toHaveLength(before.contracts.length - 1); expect(after.contracts.map((c) => c.id)).not.toContain(before.contracts[0]!.id); }); it("refuses players who are past the tutorial", async () => { const { app } = testApp(); const guest = await signUp(app); await makeOwner(testApp().app, guest.avatarId); const response = await call(app, guest, "GET", "/api/tutorial/board"); expect(response.statusCode).toBe(409); expect(errorResponseSchema.parse(response.json()).error.code).toBe("not-in-tutorial"); }); });