93 lines
5.1 KiB
TypeScript
93 lines
5.1 KiB
TypeScript
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<Pkg> {
|
|
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: age out 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/test/expire-package', { data: { packageId: pkg.id } });
|
|
expect(r.ok(), `expiring ${pkg.id}`).toBeTruthy();
|
|
}
|
|
}
|
|
});
|
|
|
|
// v0.65.0: open jobs live on the WALL clock — JOB_LIFETIME_MS after createdRealMs they expire
|
|
// regardless of time scale or fleet state. The age hook (FLIGHT_TEST_HOOKS=1) stamps a job past its
|
|
// lifetime and runs a tick, so expiry is observable in one round-trip; self-contained like the test
|
|
// above (the assertion lives in the hook response, no /api/state polling).
|
|
test('an open job aged past its wall-clock lifetime expires on the next tick', async ({ page }) => {
|
|
const pkg = await createPriorityPackage(page, FROM, TO);
|
|
|
|
const res = await page.request.post('/api/test/expire-package', { data: { packageId: pkg.id } });
|
|
expect(res.status()).toBe(200);
|
|
const aged = (await res.json()) as Pkg & { status: string };
|
|
expect(aged.id).toBe(pkg.id);
|
|
expect(aged.status, 'the hook runs a tick after aging the job, so it is expired by response time').toBe('expired');
|
|
|
|
// Unknown ids and non-open jobs still 404.
|
|
const missing = await page.request.post('/api/test/expire-package', { data: { packageId: 'no-such-id' } });
|
|
expect(missing.status()).toBe(404);
|
|
const alreadyExpired = await page.request.post('/api/test/expire-package', { data: { packageId: pkg.id } });
|
|
expect(alreadyExpired.status()).toBe(404); // expired is not open — the hook only ages live jobs
|
|
});
|