// Ported from apps/server/test (CR-014 Step 14.5): now runs on the engine. import { content } from "@deadhead/content"; import { dispatchResponseSchema, errorResponseSchema, pilotResponseSchema, type PlayerState, } from "@deadhead/schema"; import { restEndsAt } from "@deadhead/sim"; import { describe, expect, it } from "vitest"; import { useTestApp, signUp, call, getState, setCredits, testContract, insertDispatch, putBoard, signUpOwner, type Guest, } from "../support"; const testApp = useTestApp(); const errorCode = (response: { json(): unknown }) => errorResponseSchema.parse(response.json()).error.code; const avatar = (state: PlayerState) => state.pilots.find((p) => p.isAvatar)!; const FAR_FUTURE = Date.parse("2200-01-01T00:00:00Z"); /** Flies the owner's Courier Earth↔Moon until a trip ends in mandatory rest. */ async function chainUntilRest(owner: Guest & { shipId: string }) { const { clock } = testApp(); let at = "earth"; for (let trip = 0; trip < 20; trip++) { const to = at === "earth" ? "moon" : "earth"; const record = await insertDispatch(testApp().app, { shipId: owner.shipId, pilotId: owner.avatarId, departAt: clock.now, contract: testContract({ originId: at, destinationId: to, expiresAt: FAR_FUTURE }), }); clock.now = record.arrivalAt; const state = await getState(testApp().app, owner); at = to; if (avatar(state).resting) return { state, record, at }; expect(avatar(state).dutyHours).toBeGreaterThan(0); } throw new Error("never needed rest"); } describe("pilot duty and rest on the server (Step 10D.5)", () => { it("rests a pilot after chained runs, refuses them until rest ends, then dispatches", async () => { const { app, clock } = testApp(); const owner = await signUpOwner(app); await setCredits(testApp().app, 50_000); const { state, record, at } = await chainUntilRest(owner); const rested = avatar(state); expect(rested).toMatchObject({ dutyHours: 0, resting: true }); // Rest starts at the arrival time (decision 0.5.45). expect(rested.restEndsAt).toBe(restEndsAt(record.arrivalAt, content.config)); expect(state.summaries.at(-1)!.restEndsAt).toBe(rested.restEndsAt); const job = testContract({ originId: at, destinationId: at === "earth" ? "moon" : "earth", expiresAt: FAR_FUTURE, }); await putBoard(testApp().app, [job], clock.now, at); const dispatch = () => call(app, owner, "POST", "/api/dispatches", { kind: "contract", contractId: job.id, shipId: owner.shipId, pilotId: owner.avatarId, }); const refused = await dispatch(); expect(refused.statusCode).toBe(409); expect(errorCode(refused)).toBe("pilot-resting"); clock.now = rested.restEndsAt!; const accepted = await dispatch(); expect(accepted.statusCode).toBe(200); dispatchResponseSchema.parse(accepted.json()); }); it("rests an idle pilot by hand, and refuses resting one who is flying or already resting", async () => { const { app, clock } = testApp(); const owner = await signUpOwner(app); const response = await call(app, owner, "POST", `/api/pilots/${owner.avatarId}/rest`, {}); expect(response.statusCode).toBe(200); expect(pilotResponseSchema.parse(response.json()).pilot).toMatchObject({ dutyHours: 0, resting: true, restEndsAt: restEndsAt(clock.now, content.config), }); const again = await call(app, owner, "POST", `/api/pilots/${owner.avatarId}/rest`, {}); expect(again.statusCode).toBe(409); expect(errorCode(again)).toBe("pilot-resting"); const other = await signUpOwner(app); await insertDispatch(testApp().app, { shipId: other.shipId, pilotId: other.avatarId, departAt: clock.now, }); const flying = await call(app, other, "POST", `/api/pilots/${other.avatarId}/rest`, {}); expect(flying.statusCode).toBe(409); expect(errorCode(flying)).toBe("pilot-not-idle"); }); it("lets a resting pilot leave the ship at a bay and a fresh pilot fly it at once", async () => { const { app, clock } = testApp(); const owner = await signUpOwner(app); await setCredits(testApp().app, 50_000); await call(app, owner, "POST", `/api/pilots/${owner.avatarId}/rest`, {}); const unassign = await call(app, owner, "POST", `/api/pilots/${owner.avatarId}/unassign`, {}); expect(unassign.statusCode).toBe(200); const hired = pilotResponseSchema.parse( (await call(app, owner, "POST", "/api/pilots", { locationId: "earth" })).json(), ).pilot; const assign = await call(app, owner, "POST", `/api/pilots/${hired.id}/assign`, { shipId: owner.shipId, }); expect(assign.statusCode).toBe(200); const job = testContract({ expiresAt: FAR_FUTURE }); await putBoard(testApp().app, [job], clock.now); const response = await call(app, owner, "POST", "/api/dispatches", { kind: "contract", contractId: job.id, shipId: owner.shipId, pilotId: hired.id, }); expect(response.statusCode).toBe(200); }); it("does not rest the avatar during the tutorial", async () => { const guest = await signUp(testApp().app); const response = await call( testApp().app, guest, "POST", `/api/pilots/${guest.avatarId}/rest`, {}, ); expect(response.statusCode).toBe(409); expect(errorCode(response)).toBe("tutorial-active"); }); });