// Ported from apps/server/test (CR-014 Step 14.5): now runs on the engine. import { content } from "@deadhead/content"; import { errorResponseSchema, playerStateSchema } from "@deadhead/schema"; import { describe, expect, it } from "vitest"; import { useTestApp, signUp, call, getState, edit, readSave, type Guest } from "../support"; const testApp = useTestApp(); /** Fast-forwards a new guest to "four runs done, loaner back at Earth". */ async function readyForDeal(_guest: Guest) { await edit(testApp().app, (save) => { save.player.tutorialRunsCompleted = content.config.tutorialRunsBeforeDeal; save.player.loanerLocationId = "earth"; }); } const accept = (guest: Guest) => call(testApp().app, guest, "POST", "/api/deal/accept"); /** Everything the deal touches, read straight from the save. */ async function snapshot(_guest: Guest) { const save = await readSave(testApp().app); return { player: save.player, company: save.company, ships: save.ships, bays: save.bays, pilots: save.pilots, }; } describe("accepting the deal", () => { it("hands over a crewed Courier and Clipper at Earth, an Earth bay and the loan, and ends the tutorial", async () => { const guest = await signUp(testApp().app); await readyForDeal(guest); expect((await getState(testApp().app, guest)).tutorial?.dealAvailable).toBe(true); const response = await accept(guest); expect(response.statusCode).toBe(200); const state = playerStateSchema.parse(response.json()); expect(state.tutorial).toBeNull(); // CR-013: the Courier for the avatar and the Clipper for the veteran. expect(state.ships).toHaveLength(2); const [ship, clipper] = state.ships; expect(ship).toMatchObject({ modelId: "courier", status: "idle", locationId: "earth", pilotId: guest.avatarId, }); const veteran = state.pilots.find((p) => p.isVeteran)!; expect(clipper).toMatchObject({ modelId: "clipper", status: "idle", locationId: "earth", pilotId: veteran.id, }); expect(veteran).toMatchObject({ assignedShipId: clipper!.id, locationId: "earth" }); expect(state.bays.map((b) => b.locationId)).toEqual(["earth"]); expect(state.pilots[0]).toMatchObject({ isAvatar: true, assignedShipId: ship!.id, locationId: "earth", }); expect(state.company.loanBalance).toBe(content.dealLoanAmount); const after = await snapshot(guest); expect(after.player).toMatchObject({ tutorialActive: false, dealAccepted: true, loanerLocationId: null, }); }); it("fails with no changes when the deal is not available", async () => { const guest = await signUp(testApp().app); const before = await snapshot(guest); const response = await accept(guest); expect(response.statusCode).toBe(409); expect(errorResponseSchema.parse(response.json()).error.code).toBe("deal-not-available"); expect(await snapshot(guest)).toEqual(before); }); it("is accepted with the loaner at the Moon, and the avatar ends up at Earth (CR-017)", async () => { const guest = await signUp(testApp().app); await readyForDeal(guest); await edit(testApp().app, (save) => { save.player.loanerLocationId = "moon"; }); const response = await accept(guest); expect(response.statusCode).toBe(200); const state = playerStateSchema.parse(response.json()); const avatar = state.pilots.find((p) => p.isAvatar)!; const courier = state.ships.find((s) => s.id === avatar.assignedShipId)!; expect(avatar.locationId).toBe("earth"); expect(courier).toMatchObject({ modelId: "courier", locationId: "earth" }); expect(state.tutorial).toBeNull(); }); it("fails the second time", async () => { const guest = await signUp(testApp().app); await readyForDeal(guest); expect((await accept(guest)).statusCode).toBe(200); const before = await snapshot(guest); const again = await accept(guest); expect(errorResponseSchema.parse(again.json()).error.code).toBe("deal-not-available"); expect(await snapshot(guest)).toEqual(before); }); it("produces exactly one deal from two simultaneous accepts", async () => { const guest = await signUp(testApp().app); await readyForDeal(guest); const responses = await Promise.all([accept(guest), accept(guest)]); expect(responses.map((r) => r.statusCode).sort()).toEqual([200, 409]); const after = await snapshot(guest); expect(after.ships).toHaveLength(2); expect(after.bays).toHaveLength(1); expect(after.company!.loanBalance).toBe(content.dealLoanAmount); }); });