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
+21 -15
View File
@@ -20,6 +20,9 @@ const HUB = 'EDDF'; // must match HUB_ICAO in lib/game/seed.ts
// the 6300 L tank, so a full-tank departure always makes it home.
const DEST = 'LEMD';
const SCALE = 1000; // test rate for fast-forwarding legs
// Pure arrival waits and relocation-chain hops (no mid-leg observation): a lagged tick can only finish
// the leg sooner, never skip an observed state. Max hop ≤3400 km lands in <1 s at this rate.
const FAST = 50_000;
const WORLD = 24; // the fixed world rate (ISS-117) — must match lib/worldClock.ts
interface PlaneState {
@@ -106,15 +109,15 @@ async function setFuelFull(page: Page, planeId: string): Promise<void> {
expect((await page.request.post('/api/test/set-fuel', { data: { planeId, fuel: p.maxFuel } })).status()).toBe(200);
}
// Load a package onto plane-starter and fly it from → to via the API, then poll at
// scale 1000 until the plane lands at `to`. Same pattern as e2e/actions.spec.ts.
// Load a package onto plane-starter and fly it from → to via the API, then fast-forward at FAST
// (pure arrival wait — no mid-leg observation) until the plane lands at `to`. Same pattern as e2e/actions.spec.ts.
async function loadAndFly(page: Page, fromIcao: string, toIcao: string): Promise<PackageState> {
const pkg = await createPackage(page, fromIcao, toIcao);
const load = await page.request.post(`/api/packages/${pkg.id}/load`, { data: { planeId: 'plane-starter' } });
expect(load.status()).toBe(200);
const fly = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: toIcao } });
expect(fly.status()).toBe(200);
await setScale(page, SCALE);
await setScale(page, FAST);
await expect
.poll(async () => {
const body = await getState(page);
@@ -204,7 +207,7 @@ test('airport popup shows name, ICAO, local time and generated weather', async (
});
test('plane markers move while flying and the plane popup shows status + route', async ({ page }) => {
test.setTimeout(600_000); // self-heal from a far airport (~20 s worst case at 1000×) + main flight + cleanup
test.setTimeout(600_000); // self-heal from a far airport (~3 s worst case at 50_000×) + main flight + cleanup
await page.goto('/');
await waitForAirportCount(page);
@@ -215,7 +218,7 @@ test('plane markers move while flying and the plane popup shows status + route',
const foundStarter = (await getState(page)).planes.find((p) => p.id === 'plane-starter');
expect(foundStarter).toBeDefined();
if (foundStarter!.status === 'flying') {
await setScale(page, SCALE);
await setScale(page, FAST); // pure arrival wait — no mid-leg observation needed here
await expect
.poll(async () => (await getState(page)).planes.find((p) => p.id === 'plane-starter')?.status === 'available', {
timeout: 240_000,
@@ -348,8 +351,8 @@ test('plane markers move while flying and the plane popup shows status + route',
// The popup's route line is the plane's own leg: origin name → destination name.
await expect(page.getByTestId('plane-route')).toContainText(`${live.locationName} → ${live.destName}`);
// Restore SCALE before step 5 — the landing wait needs a fast clock.
await setScale(page, SCALE);
// Fast-forward to arrival (pure wait — no mid-leg observation) at FAST: ~0.2 s instead of ~4 s.
await setScale(page, FAST);
// 5) Let the flight finish, then fly home so the live instance stays in its
// onboarding state (starter grounded at EDDF) for the other specs. The loaded
@@ -478,7 +481,7 @@ async function fitToAirports(page: Page, icaos: string[]): Promise<void> {
}
test('plane marker stays continuous across the antimeridian (no 360° teleport)', async ({ page }) => {
test.setTimeout(900_000); // relocation chain (~3 min at 1000×) + crossing leg + return chain + margin
test.setTimeout(900_000); // relocation chain (~15 s per direction at 50_000×) + crossing leg + return chain + margin
await page.goto('/');
await waitForAirportCount(page);
@@ -489,7 +492,7 @@ test('plane marker stays continuous across the antimeridian (no 360° teleport)'
// Self-heal: full health, let any in-flight leg finish, then relocate to ORIGIN via the chain.
expect((await page.request.post('/api/test/set-health', { data: { planeId: 'plane-starter', health: 100, repairUntilMs: 0 } })).status()).toBe(200);
if ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.status === 'flying') {
await setScale(page, SCALE);
await setScale(page, FAST); // pure arrival wait — no mid-leg observation needed here
await expect.poll(async () => (await getState(page)).planes.find((p) => p.id === 'plane-starter')?.status ?? '', { timeout: 240_000 }).toBe('available');
}
// Relocate + crossing leg. The finally ALWAYS runs — even when a poll below fails, the plane must end
@@ -500,7 +503,7 @@ test('plane marker stays continuous across the antimeridian (no 360° teleport)'
for (const next of await chainTo(page, loc, ORIGIN)) {
await setFuelFull(page, 'plane-starter'); // full tank → auto-refuel tops up 0 L and costs nothing
expect((await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: next } })).status()).toBe(200);
await setScale(page, SCALE);
await setScale(page, FAST); // pure arrival wait per hop — a lagged tick can only land it sooner
await expect.poll(async () => (await getState(page)).planes.find((p) => p.id === 'plane-starter')?.locationIcao ?? '', { timeout: 240_000 }).toBe(next);
}
}
@@ -521,9 +524,12 @@ test('plane marker stays continuous across the antimeridian (no 360° teleport)'
const pkg = await createPackage(page, ORIGIN, CROSS_DEST);
expect((await page.request.post(`/api/packages/${pkg.id}/load`, { data: { planeId: 'plane-starter' } })).status()).toBe(200);
console.log(`[antimeridian] pkg=${pkg.id} leg ${ORIGIN}->${CROSS_DEST}; BEFORE dispatch: ${fmtPlane((await getState(page)).planes.find((x) => x.id === 'plane-starter')!)}`);
// The crossing leg must run at SCALE (1000×): the freshness gate (~700 km ≈ 5 s of flight) and the
// >3 fresh samples both assume ~14 s of real-time flight. Set it BEFORE dispatch — a leftover FAST
// rate would let the dispatch tick itself consume most of the leg before this setScale lands.
await setScale(page, SCALE);
const fly = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: CROSS_DEST } });
if (fly.status() !== 200) throw new Error(`dispatch POST /api/planes/plane-starter/fly → ${fly.status()} (expected 200)`);
await setScale(page, SCALE);
console.log(`[antimeridian] AFTER dispatch: ${fmtPlane((await getState(page)).planes.find((x) => x.id === 'plane-starter')!)}`);
// querySourceFeatures only sees LOADED tiles — the v0.70.0 default view ([0,0] z2) doesn't cover this
@@ -591,16 +597,16 @@ test('plane marker stays continuous across the antimeridian (no 360° teleport)'
expect(lons.some((lon) => Math.abs(lon) > 180), `sampled lons (n=${lons.length}): ${JSON.stringify(lons)}`).toBe(true); // crossed onto a world copy, either side of ±180
expect(Math.max(...lons) - Math.min(...lons), `lon span: min=${Math.min(...lons).toFixed(4)} max=${Math.max(...lons).toFixed(4)} over ${JSON.stringify(lons)}`).toBeGreaterThan(4); // traversed the crossing region, not a static window
} finally {
// Cleanup: let any in-flight leg land, then chain back home so the other specs start from the canonical
// state. The package was delivered on arrival (generous deadline).
await setScale(page, SCALE);
// Cleanup: let any in-flight leg land (pure arrival wait → FAST), then chain back home so the other
// specs start from the canonical state. The package was delivered on arrival (generous deadline).
await setScale(page, FAST);
await expect.poll(async () => (await getState(page)).planes.find((p) => p.id === 'plane-starter')?.status ?? '', { timeout: 240_000 }).toBe('available');
const endLoc = (await getState(page)).planes.find((p) => p.id === 'plane-starter')!.locationIcao;
if (endLoc !== HUB) {
for (const next of await chainTo(page, endLoc, HUB)) {
await setFuelFull(page, 'plane-starter');
expect((await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: next } })).status()).toBe(200);
await setScale(page, SCALE);
await setScale(page, FAST); // pure arrival wait per hop — a lagged tick can only land it sooner
await expect.poll(async () => (await getState(page)).planes.find((p) => p.id === 'plane-starter')?.locationIcao ?? '', { timeout: 240_000 }).toBe(next);
}
}