Step 6.2: test full player state

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-03 01:03:09 -04:00
parent 906fb275d7
commit 2a8f788ce8
3 changed files with 102 additions and 1 deletions

View File

@ -262,3 +262,7 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **Game-rule failures return `409`** with their error code (`gameError`), except `not-found` (`404`).
- **Routes live in `src/routes/*`** and share an `AppContext` (db, auth, clock, route options).
- **Test helpers** (`test/game.ts`): `useTestApp` (fresh app plus a movable `TestClock`), `signUp` (a real guest session via `POST /api/guest`), `call`/`getState`, `insertDispatch` (puts a sim-built dispatch in the database). `makeOwner` / `setCredits` (`test/db.ts`) reshape a signed-up player.
## Step 6.2
- **No new code was needed**: Step 5.5 built the state reader and Step 6.1 added resolution, so this step is the full test. A pilot's `level` and `skillPointsAvailable` are derived from XP and points spent each time state is read, never stored.

View File

@ -0,0 +1,97 @@
import { randomUUID } from "node:crypto";
import { content } from "@deadhead/content";
import { playerStateSchema } from "@deadhead/schema";
import { pilotLevel } from "@deadhead/sim";
import { describe, expect, it } from "vitest";
import { pilots, ships } from "../src/db/schema";
import { makeOwner, setCredits, TEST_NOW, useTestDatabase } from "./db";
import { call, insertDispatch, signUp, useTestApp } from "./game";
const database = useTestDatabase();
const testApp = useTestApp(database);
describe("full player state", () => {
it("reflects one ship in flight and one idle, and validates against its schema", async () => {
const { app, clock } = testApp();
const { db } = database();
const guest = await signUp(app);
const flyingShipId = await makeOwner(db, guest.playerId, guest.avatarId);
await setCredits(db, guest.playerId, 9000);
// A second ship at the Moon crewed by a hired pilot with some XP and a skill.
const idleShipId = randomUUID();
const hiredId = randomUUID();
await db.insert(ships).values({
id: idleShipId,
playerId: guest.playerId,
modelId: "freighter",
name: "Slow Ox",
locationId: "moon",
createdAt: TEST_NOW,
});
await db.insert(pilots).values({
id: hiredId,
playerId: guest.playerId,
name: "Rae Okafor",
xp: 260,
skills: { "heavy-cargo": 1 },
skillPointsSpent: 1,
assignedShipId: idleShipId,
createdAt: TEST_NOW,
});
const record = await insertDispatch(db, {
playerId: guest.playerId,
shipId: flyingShipId,
pilotId: guest.avatarId,
departAt: clock.now,
});
const response = await call(app, guest, "GET", "/api/state");
expect(response.statusCode).toBe(200);
const state = playerStateSchema.parse(response.json());
expect(state.company.credits).toBe(9000);
expect(state.tutorial).toBeNull();
expect(state.bays.map((b) => b.locationId)).toEqual(["earth"]);
const flying = state.ships.find((s) => s.id === flyingShipId)!;
const idle = state.ships.find((s) => s.id === idleShipId)!;
expect(flying).toMatchObject({
status: "in-flight",
locationId: null,
dispatchId: record.id,
pilotId: guest.avatarId,
});
expect(idle).toMatchObject({
status: "idle",
locationId: "moon",
dispatchId: null,
pilotId: hiredId,
modelId: "freighter",
});
const avatar = state.pilots.find((p) => p.id === guest.avatarId)!;
const hired = state.pilots.find((p) => p.id === hiredId)!;
expect(avatar).toMatchObject({
status: "in-flight",
locationId: null,
assignedShipId: flyingShipId,
});
expect(hired).toMatchObject({
status: "idle",
locationId: "moon",
assignedShipId: idleShipId,
xp: 260,
level: pilotLevel(260, content.config),
skills: { "heavy-cargo": 1 },
});
expect(hired.level).toBe(2);
expect(hired.skillPointsAvailable).toBe(2 * content.config.skillPointsPerLevel - 1);
expect(state.activeDispatches).toEqual([
expect.objectContaining({ id: record.id, status: "in-flight", arrivalAt: record.arrivalAt }),
]);
expect(state.summaries).toEqual([]);
expect(state.locations).toHaveLength(content.locations.length);
});
});

View File

@ -55,7 +55,7 @@
## Phase 6 — Server Game Logic
- [x] **Step 6.1 — Lazy resolution on read.** `resolveArrivedDispatches` (`src/game/resolve.ts`) resolves arrived dispatches in arrival order inside `withPlayerLock` (`src/game/lock.ts`, a transaction holding the company row lock); a conditional status update makes each resolve exactly once. `GET /api/state` runs it before reading. Routes moved to `src/routes/`.
- [ ] Step 6.2 — Get full player state
- [x] **Step 6.2 — Get full player state.** Endpoint from Step 5.5 plus lazy resolution; test covers a player with one ship in flight and one idle (crewed by a hired pilot), checking ships, pilots (XP, level, skills, points, assignment, status, location), bays, dispatches, summaries and schema validity.
- [ ] Step 6.3 — Tutorial board endpoint
- [ ] Step 6.4 — Tutorial dispatch
- [ ] Step 6.5 — Buy a ship