e2e: per-test timescale speedup — FAST/CHAIN rates for pure arrival waits (v1.8.0)
CI / build + unit + e2e (pull_request) Successful in 6m3s

- actions/hover/two-planes: FAST=10_000 for expiry fast-forwards, landing
  waits and fly-home cleanups; queue: + CHAIN=4_000 for auto-chain
  observation, refuel test at explicit 2_000 with relaxed fuel bound
- map-interactions: FAST=50_000 chain hops (~3 min -> ~15 s per direction);
  crossing leg stays at SCALE and now sets it BEFORE dispatch (a leftover
  fast rate would let the dispatch tick consume most of the leg)
- hover fleet-row test asserts flying from the fly endpoint's own snapshot
  and freezes immediately instead of speeding up then freezing mid-leg
- two-planes parallel-flight: both-grounded poll after cleanup dispatches so
  afterEach heal no longer pays the range-edge return leg at SCALE
This commit is contained in:
2026-09-27 20:18:56 +00:00
parent b60006a949
commit 664906b8ce
7 changed files with 98 additions and 57 deletions
+26 -15
View File
@@ -20,6 +20,12 @@ test.beforeEach(async ({ page }) => { await authenticate(page); });
// the next queued leg server-side (part 1); test 3 exercises that chain end-to-end through the API.
const SCALE = 1000; // test rate for fast-forwarding legs — keeps flights to seconds of real time
// Pure arrival waits (no mid-leg observation): LEMD-length legs land in ~1 s. Safe at any scale —
// a lagged tick can only finish the leg sooner, never skip an observed state.
const FAST = 10_000;
// Chained-leg observation: max per-tick advance (2 s × S) must stay below the LEMD-length leg's
// sim-duration (~1e7 sim-ms) so one lagged tick can't complete it before a poll sees the chain.
const CHAIN = 4_000;
const WORLD = 24; // the fixed world rate (ISS-117) — must match lib/worldClock.ts
const HUB = 'EDDF'; // must match HUB_ICAO in lib/game/seed.ts
const DEST = 'LEMD'; // Madrid–Barajas — the typical long leg (~10 s at scale 1000)
@@ -285,14 +291,15 @@ test('queue display + cancel via UI (ISS-043 part 2)', async ({ page }) => {
await expect(page.getByTestId('queue-leg-plane-starter-0'), 'the cancelled leg is removed from the queue').toHaveCount(0, { timeout: 15_000 });
// Cleanup: resume + fast-forward to arrival at DEST (empty queue → plain landing), then fly home.
await setScale(page, SCALE);
// Both waits are pure arrivals — FAST is safe and ~10× quicker than SCALE here.
await setScale(page, FAST);
await expect.poll(async () => {
const p = (await getState(page)).planes.find((x) => x.id === 'plane-starter');
return !!p && p.status === 'available' && p.locationIcao === DEST;
}, { timeout: 120_000, message: `waiting for plane-starter to land at ${DEST}` }).toBe(true);
// Re-set SCALE for the return leg.
await setScale(page, SCALE);
// Re-set FAST for the return leg.
await setScale(page, FAST);
const home = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
expect(home.status()).toBe(200);
await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')?.locationIcao ?? ''), {
@@ -366,14 +373,15 @@ test('dotted queue line renders the queued path on the map (ISS-043 part 2)', as
// to arrival, then fly home.
const cancelRes = await page.request.post('/api/planes/plane-starter/queue/cancel', { data: { index: 0 } });
expect(cancelRes.status()).toBe(200);
await setScale(page, SCALE);
// Pure arrival waits — FAST is safe and ~10× quicker than SCALE here.
await setScale(page, FAST);
await expect.poll(async () => {
const p = (await getState(page)).planes.find((x) => x.id === 'plane-starter');
return !!p && p.status === 'available' && p.locationIcao === DEST;
}, { timeout: 120_000, message: `waiting for plane-starter to land at ${DEST}` }).toBe(true);
// Re-set SCALE for the return leg.
await setScale(page, SCALE);
// Re-set FAST for the return leg.
await setScale(page, FAST);
const home = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
expect(home.status()).toBe(200);
await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')?.locationIcao ?? ''), {
@@ -457,9 +465,9 @@ test('landing auto-starts the next queued leg (ISS-043 chain end-to-end)', async
expect(flyRes.status()).toBe(200);
// Fast-forward (also clears any pause). On landing at DEST the server must AUTO-START the queued
// return leg — no manual dispatch. The chained state persists for the whole ~10 s return leg, so a
// poll can't miss it: status 'flying' with destIcao = the first (only) queued ICAO.
await setScale(page, SCALE);
// return leg — no manual dispatch. The chained state persists for the whole ~2.5 s return leg at
// CHAIN, so a poll can't miss it: status 'flying' with destIcao = the first (only) queued ICAO.
await setScale(page, CHAIN);
await expect.poll(async () => {
const p = (await getState(page)).planes.find((x) => x.id === 'plane-starter');
return !!p && p.status === 'flying' && p.destIcao === HUB;
@@ -566,8 +574,10 @@ test('a drained tank is topped up before a chained leg at exactly the rep-discou
const rate = repRate(s0.account.reputation, FUEL_DISCOUNT_PER_REP, MAX_FUEL_DISCOUNT);
const expectedCost = Math.round(liters * FUEL_PRICE_PER_L * (1 - rate) * 100) / 100;
// Landing at DEST auto-chains QUEUE with the top-up…
await setScale(page, SCALE);
// Landing at DEST auto-chains QUEUE with the top-up… 2_000× (not SCALE): leg 2's chained state must
// stay observable — max per-tick advance (2 s × S) stays below its ~1e7 sim-ms duration, and the
// snapshot window below is sized for this rate.
await setScale(page, 2_000);
await expect.poll(async () => {
const x = (await getState(page)).planes.find((y) => y.id === 'plane-starter');
return !!x && x.status === 'flying' && x.destIcao === QUEUE;
@@ -577,9 +587,9 @@ test('a drained tank is topped up before a chained leg at exactly the rep-discou
// Exact fuel equality is impossible — this snapshot lands after leg 2's departure burn; the exact top-up level × rep-discounted price is pinned by the cash assertion below.
const fuel = s.planes.find((x) => x.id === 'plane-starter')!.fuel;
expect(fuel).toBeLessThanOrEqual(target); // can only have burned since departure…
// 1.3× not 1.4×: under dind load the snapshot can land ~0.1×required2 into leg 2's departure burn;
// a wrong top-up level (≤ required2, or none at all) still lands far below this bound.
expect(fuel).toBeGreaterThan(1.3 * required2);
// > required2 (not 1.3×): at 2_000× the snapshot can land ~0.4×required2 into leg 2's burn under dind
// load; a wrong top-up level (≤ required2, or none at all) still lands far below this bound.
expect(fuel).toBeGreaterThan(required2);
expect(s.account.cash).toBeCloseTo(cashBeforeChain - expectedCost, 6); // at exactly the rep-discounted cost
// Let leg 2 finish so the next test starts from a known airport.
@@ -603,7 +613,8 @@ test('cancelling the only queued leg mid-flight leaves a plain landing (v0.46.0)
const cancel = await page.request.post('/api/planes/plane-starter/queue/cancel', { data: { index: 0 } });
expect(cancel.status()).toBe(200);
await setScale(page, SCALE);
// Pure arrival wait (the queue is empty — nothing to observe mid-leg) → FAST.
await setScale(page, FAST);
await expect.poll(async () => {
const x = (await getState(page)).planes.find((y) => y.id === 'plane-starter');
return !!x && x.status === 'available' && x.locationIcao === DEST;