import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; // Real helpers (v0.54.0) — the expiry floor must match lib/ exactly; Playwright resolves the repo's // tsconfig paths, so importing beats mirroring a third copy of the constants/formulas. import { haversineKm } from '../lib/geo'; import { travelDurationMs } from '../lib/sim/movement'; import { FASTEST_CATALOG_SPEED_KMH } from '../lib/game/catalog'; import { EXPIRY_BUFFER_MS } from '../lib/game/seed'; // v0.54.0 (ISS-119) — long priority jobs must be finishable: a package's deadline is clamped up to // now + travel time at the fastest catalog speed + buffer, so no generated job can expire before even // the best plane could land. The EXISTING POST /api/test/create-package hook (FLIGHT_TEST_HOOKS=1) // forces a far-apart endpoint pair (~9000 km, at MAX_JOB_LEG_KM); this spec asserts the invariant on // every returned package — no new server hooks needed. No scale changes, plane interaction, polling or // map queries are involved (the assertion is self-contained in the hook response), so none of those // conventions apply here. const FROM = 'EDDB'; // Berlin Brandenburg const TO = 'KSMF'; // Sacramento International — ~9000 km from EDDB, at MAX_JOB_LEG_KM const COUNT = 10; // priority packages created and checked interface Pkg { id: string; originIcao: string; destIcao: string; createdSimTime: number; deadlineSimTime: number; } async function createPriorityPackage(page: Page, from: string, to: string): Promise { const res = await page.request.post('/api/test/create-package', { data: { originIcao: from, destIcao: to, special: 'priority' } }); expect(res.status()).toBe(201); return (await res.json()) as Pkg; } test('long-leg priority packages always expire after travel time at max speed + buffer', async ({ page }) => { // The pair's real coordinates from the live dataset — the floor is computed on the same km the server uses. const res = await page.request.get('/api/airports'); expect(res.ok()).toBeTruthy(); const fc = (await res.json()) as { features: Array<{ properties: { icao: string; lat: number; lon: number } }> }; const from = fc.features.find((f) => f.properties.icao === FROM)?.properties; const to = fc.features.find((f) => f.properties.icao === TO)?.properties; expect(from, `${FROM} missing from the airport dataset`).toBeTruthy(); expect(to, `${TO} missing from the airport dataset`).toBeTruthy(); if (!from || !to) return; // guarded above — keeps the math below total const km = haversineKm(from.lat, from.lon, to.lat, to.lon); expect(km).toBeGreaterThan(7_000); // guard: the pair must still be a long leg or this test is vacuous const floorMs = travelDurationMs(km, FASTEST_CATALOG_SPEED_KMH) + EXPIRY_BUFFER_MS; const created: Pkg[] = []; try { for (let i = 0; i < COUNT; i++) { const pkg = await createPriorityPackage(page, FROM, TO); expect(pkg.originIcao).toBe(FROM); expect(pkg.destIcao).toBe(TO); // The invariant (ISS-119): the deadline must leave at least travel time + buffer after creation. // deadlineSimTime = round(now + offset) with offset ≥ floorMs, so this holds exactly. const offsetMs = pkg.deadlineSimTime - pkg.createdSimTime; expect(offsetMs).toBeGreaterThanOrEqual(Math.floor(floorMs)); // And the clamp must not blow past the priority ceiling (36 h) — the deadline stays sane. expect(offsetMs).toBeLessThanOrEqual(36 * 3_600_000); created.push(pkg); } } finally { // Cleanup: dismiss every package this test created so it leaves no trace on the shared board. for (const pkg of created) { const r = await page.request.post(`/api/packages/${pkg.id}/ignore`); expect(r.ok(), `ignoring ${pkg.id}`).toBeTruthy(); } } });