Step 10G.7: server tests for Priority dispatch and goods on boards

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-10-04 17:13:32 -04:00
parent 5be66372d7
commit 4f8ee4a06d
2 changed files with 44 additions and 1 deletions

View File

@ -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);
}
});
});

View File

@ -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.