From 4f8ee4a06d5c38de88d8e4d6d9520fcb2c341a77 Mon Sep 17 00:00:00 2001 From: fireshaper Date: Sun, 4 Oct 2026 17:13:32 -0400 Subject: [PATCH] Step 10G.7: server tests for Priority dispatch and goods on boards Co-Authored-By: Claude Opus 5.5 --- apps/server/test/contract-dispatch.test.ts | 44 +++++++++++++++++++++- memory-bank/progress.md | 1 + 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/apps/server/test/contract-dispatch.test.ts b/apps/server/test/contract-dispatch.test.ts index 1fc9c39..3535a39 100644 --- a/apps/server/test/contract-dispatch.test.ts +++ b/apps/server/test/contract-dispatch.test.ts @@ -5,7 +5,7 @@ import { dispatchResponseSchema, errorResponseSchema, } from "@deadhead/schema"; -import { boardPeriodMs } from "@deadhead/sim"; +import { boardPeriodMs, contractTripGameHours } from "@deadhead/sim"; import { eq } from "drizzle-orm"; import { describe, expect, it } from "vitest"; import { acceptedContracts, companies, dispatches, pilots, ships } from "../src/db/schema"; @@ -205,3 +205,45 @@ describe("contract dispatch", () => { expect(errorCode(await dispatch(o, contract.id))).toBe("contract-already-accepted"); }); }); + +describe("Priority contracts (CR-007)", () => { + const courier = content.shipModel("courier")!; + + /** A Priority Earth→Moon mail job whose window is the Courier's trip plus `slackHours`. */ + async function priorityJob(o: Owner, id: string, slackHours: number) { + const { clock } = testApp(); + const base = testContract({ id, goodId: "mail", massTonnes: 4, priority: true }); + const trip = contractTripGameHours(courier, base, clock.now, content); + const contract = { ...base, deliveryWindowGameHours: trip + slackHours }; + await putBoard(database().db, o.guest.playerId, [contract], clock.now); + return contract; + } + + it("refuses a ship too slow for the window with too-slow and changes nothing", async () => { + const o = await owner(); + const job = await priorityJob(o, "earth:1:0", -0.25); + await expectRejected(o, await dispatch(o, job.id), "too-slow"); + }); + + it("dispatches a ship fast enough for the window", async () => { + const o = await owner(); + const job = await priorityJob(o, "earth:1:0", 0.25); + const response = await dispatch(o, job.id); + expect(response.statusCode).toBe(200); + const { dispatch: stored } = dispatchResponseSchema.parse(response.json()); + expect(stored.contract).toMatchObject({ priority: true, goodId: "mail" }); + }); + + it("serves generated boards whose contracts all name a known good and a shipper", async () => { + const o = await owner(); + const response = await call(testApp().app, o.guest, "GET", "/api/locations/earth/board"); + const { contracts } = contractBoardResponseSchema.parse(response.json()); + expect(contracts.length).toBeGreaterThan(0); + for (const c of contracts) { + const good = content.good(c.goodId); + expect(good?.name).toBeTruthy(); + expect(good!.cargoClassId).toBe(c.cargoTypeId); + expect(content.location("earth")!.shippers).toContain(c.shipper); + } + }); +}); diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 4d42f20..a4321a8 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -208,3 +208,4 @@ Spec: `memory-bank/change-requests/CR-007-cargo-goods.md` (supplied as "CR-005 / - [x] **Step 10G.4 — Sim: Priority validation.** `too-slow` joins `DISPATCH_FAILURES` after the route checks: a Priority contract fails when `contractTripGameHours` (cruise + overhead + handling, distance at departure; exported) exceeds `deliveryWindowGameHours`. Server message and the dispatch sheet's short reason added (the API error code follows from the sim's list). `priorityWindow` exported. Tests: `priority.test.ts` (Priority mail Earth→Moon with the Freighter reference: Courier and Freighter pass, Bulk Hauler fails `too-slow`; the trip is measured like the window; a normal contract never fails as too slow); `board-fleet.test.ts` now also requires fit Priority jobs to be within the crew's reach. - [x] **Step 10G.5 — Sim: tutorial goods.** Each tutorial slot carries a Standard good made at the loaner's location and wanted at the other end, chosen (seeded from location, run and period) among goods whose range holds the CR-001 tier mass, else clamped to a good's range; a random shipper from the origin; never Priority. On both routes some good holds 5, 12 and 20 t, so tutorial masses are unchanged; payouts now include the good's pay multiplier (e.g. Mail ×1.2). "Unmarked crates" is unchanged. Test: `tutorial.test.ts` (goods per direction over 6 runs × 20 periods, never Priority, shippers, masses 5/12/20; the existing scripted-run test still passes). - [x] **Step 10G.6 — Schema and database.** API `Contract` (since 10G.2) carries `goodId`, `shipper`, `priority`, `deliveryWindowGameHours`; `ResolutionSummary` gains `goodId`, `massTonnes`, `shipper` (null for a deadhead); `too-slow` is an API error code via the sim's list. Contracts are JSONB, so no columns change: migration `0005_cr007_goods` (custom SQL) backfills stored boards and dispatch snapshots without a good (default good of the class, origin's first shipper, no Priority) and skips rows that have one. Dev and screenshot databases migrated (all 48 stored dev contracts now carry a good). `architecture.md` updated. Tests: `goods-backfill.test.ts` (legacy board and in-flight contract are filled, a second run changes nothing, the API then serves them), `migrations.test.ts` unchanged, schema cases for Priority windows, empty goods and summary goods. +- [x] **Step 10G.7 — Server: pass-through and validation.** No server code changes were needed beyond 10G.4's message and 10G.6's summary fields: boards come from the sim and dispatch validation returns the sim's `too-slow`. Tests (`contract-dispatch.test.ts`): a Priority job whose window is shorter than the Courier's trip is refused with `too-slow` and changes nothing; with a window the Courier makes, it dispatches and the stored contract keeps its Priority good; every generated Earth contract names a catalog good of its class and a shipper from Earth's list.