feat: v0.12.0 — contracts → packages: load/fly/abandon gameplay (ATR 72-600 capacity 6, manual flight to any airport, time-scaled abandon penalty 15–60%), one-time DB migration preserving wallet/clock; new endpoints /api/packages/:id/load|abandon + /api/planes/:planeId/fly
This commit is contained in:
+19
-19
@@ -1,14 +1,14 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
// v0.7.0 sim engine against the live origin: tick-on-read clock advance at a
|
||||
// raised time scale, pause freezing, contract expiry (via the FLIGHT_TEST_HOOKS
|
||||
// raised time scale, pause freezing, package expiry (via the FLIGHT_TEST_HOOKS
|
||||
// hook enabled in docker-compose.yml), and market top-up.
|
||||
|
||||
// Must match lib/sim/engine.ts — kept in sync manually (same pattern as MIN_ZOOM/MAX_ZOOM in map.spec.ts).
|
||||
const MAX_TICK_ELAPSED_MS = 2_000;
|
||||
const SCALE = 60;
|
||||
const MAX_DELTA_PER_TICK = MAX_TICK_ELAPSED_MS * SCALE; // 120_000 game ms per tick at scale 60
|
||||
// Must match MARKET_TARGET_OPEN_CONTRACTS in lib/game/seed.ts.
|
||||
// Must match MARKET_TARGET_OPEN_PACKAGES in lib/game/seed.ts.
|
||||
const MARKET_TARGET = 10;
|
||||
|
||||
interface SimState {
|
||||
@@ -21,7 +21,7 @@ interface StateBody {
|
||||
account: { cash: number; reputation: number };
|
||||
simState: SimState;
|
||||
planes: Array<Record<string, unknown>>;
|
||||
contracts: Array<{ id: string; status: string; deadlineSimTime: number }>;
|
||||
packages: Array<{ id: string; status: string; deadlineSimTime: number }>;
|
||||
}
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
@@ -74,16 +74,16 @@ test('paused freezes the clock exactly across polls', async ({ page }) => {
|
||||
}
|
||||
});
|
||||
|
||||
test('an open contract past its short deadline expires and the market stays topped up', async ({ page }) => {
|
||||
test('an open package past its short deadline expires and the market stays topped up', async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
|
||||
const before = await getState(page);
|
||||
const victim = before.contracts.find((c) => c.status === 'open');
|
||||
expect(victim).toBeDefined(); // steady state always has open contracts
|
||||
const victim = before.packages.find((p) => p.status === 'open');
|
||||
expect(victim).toBeDefined(); // steady state always has open packages
|
||||
|
||||
// Test-only hook (FLIGHT_TEST_HOOKS=1 in docker-compose): move an existing open
|
||||
// contract's deadline ~10 game-seconds out so expiry is observable at scale 60.
|
||||
const hook = await page.request.post('/api/test/expire-contract', { data: { contractId: victim!.id } });
|
||||
// package's deadline ~10 game-seconds out so expiry is observable at scale 60.
|
||||
const hook = await page.request.post('/api/test/expire-package', { data: { packageId: victim!.id } });
|
||||
expect(hook.status()).toBe(200);
|
||||
const moved = (await hook.json()) as { id: string; deadlineSimTime: number };
|
||||
expect(moved.id).toBe(victim!.id);
|
||||
@@ -94,24 +94,24 @@ test('an open contract past its short deadline expires and the market stays topp
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const body = await getState(page);
|
||||
if (body.contracts.find((c) => c.id === moved.id)?.status === 'expired') expiredBody = body;
|
||||
if (body.packages.find((p) => p.id === moved.id)?.status === 'expired') expiredBody = body;
|
||||
return !!expiredBody;
|
||||
}, { timeout: 30_000, message: 'waiting for the short-deadline contract to expire' })
|
||||
}, { timeout: 30_000, message: 'waiting for the short-deadline package to expire' })
|
||||
.toBe(true);
|
||||
|
||||
const after = expiredBody!;
|
||||
expect(after.contracts.find((c) => c.id === moved.id)?.status).toBe('expired');
|
||||
// Expired contracts leave the available set…
|
||||
expect(after.contracts.filter((c) => c.status === 'open').some((c) => c.id === moved.id)).toBe(false);
|
||||
expect(after.packages.find((p) => p.id === moved.id)?.status).toBe('expired');
|
||||
// Expired packages leave the available set…
|
||||
expect(after.packages.filter((p) => p.status === 'open').some((c) => c.id === moved.id)).toBe(false);
|
||||
// …and spawn tops the market back up to the target — never below it.
|
||||
const openCount = after.contracts.filter((c) => c.status === 'open').length;
|
||||
const openCount = after.packages.filter((p) => p.status === 'open').length;
|
||||
expect(openCount).toBeGreaterThanOrEqual(MARKET_TARGET);
|
||||
});
|
||||
|
||||
test('the hook can also create a fresh short-deadline contract, which then expires', async ({ page }) => {
|
||||
test('the hook can also create a fresh short-deadline package, which then expires', async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
|
||||
const created = await page.request.post('/api/test/expire-contract');
|
||||
const created = await page.request.post('/api/test/expire-package');
|
||||
expect(created.status()).toBe(201);
|
||||
const c = (await created.json()) as { id: string; status: string };
|
||||
expect(c.status).toBe('open');
|
||||
@@ -122,12 +122,12 @@ test('the hook can also create a fresh short-deadline contract, which then expir
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const body = await getState(page);
|
||||
if (body.contracts.find((x) => x.id === c.id)?.status === 'expired') expiredBody = body;
|
||||
if (body.packages.find((p) => p.id === c.id)?.status === 'expired') expiredBody = body;
|
||||
return !!expiredBody;
|
||||
}, { timeout: 30_000, message: 'waiting for the hook-created contract to expire' })
|
||||
}, { timeout: 30_000, message: 'waiting for the hook-created package to expire' })
|
||||
.toBe(true);
|
||||
|
||||
const openCount = expiredBody!.contracts.filter((x) => x.status === 'open').length;
|
||||
const openCount = expiredBody!.packages.filter((p) => p.status === 'open').length;
|
||||
expect(openCount).toBeGreaterThanOrEqual(MARKET_TARGET); // top-up held after this expiry too
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user