157 lines
5.4 KiB
TypeScript
157 lines
5.4 KiB
TypeScript
// 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");
|
|
});
|
|
});
|