test: harden full-core-loop against Load/Fly precondition races
CI / build + unit + e2e (pull_request) Successful in 6m32s

This commit is contained in:
2026-09-19 13:36:27 +00:00
parent 687e4f2169
commit ae1cb93a4f
2 changed files with 28 additions and 6 deletions
+27 -6
View File
@@ -100,15 +100,21 @@ test.describe.configure({ mode: 'serial' }); // shared live DB — order matters
// (repositioning needs no package — v0.12.0 manual flight), and abandon any leftover cargo so
// exact cash/rep assertions start from a clean baseline even after an interrupted previous run.
async function flyHomeViaApi(page: Page): Promise<void> {
const s0 = await getState(page);
const plane = s0.planes.find((p) => p.id === 'plane-starter')!;
let s = await getState(page);
let plane = s.planes.find((p) => p.id === 'plane-starter')!;
if (plane.status === 'flying' || plane.locationIcao !== HUB) {
// Fast-forward any leftover leg / repositioning flight (~20 s real time worst case at 1000×).
await setScale(page, SCALE);
}
if (plane.status === 'flying') {
// Let the in-flight leg finish before repositioning.
// Let the in-flight leg finish before repositioning. While flying, locationIcao stays at the
// DEPARTURE airport, so the pre-landing snapshot can't tell us where the plane ends up — re-read
// state after landing before deciding whether a repositioning flight is needed (run #41 retry: a
// leftover EDDF→EDFH leg landed away from home but the helper still believed it was home and
// skipped the fly-home).
await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.status), { timeout: 240_000 }).toBe('available');
s = await getState(page);
plane = s.planes.find((p) => p.id === 'plane-starter')!;
}
if (plane.locationIcao !== HUB) {
const res = await page.request.post(`/api/planes/${plane.id}/fly`, { data: { destIcao: HUB } });
@@ -143,6 +149,14 @@ test('full core loop via UI: load → fly to destination → exact reward + rep
expect(pkg.originIcao).toBe(HUB);
expect(pkg.destIcao).toBe(nearest.icao);
// Precondition (server truth) before clicking Load: the starter must be grounded at the package's
// origin — that is exactly what makes it load-eligible, and a stale pane snapshot can't prove it.
await expect.poll(async () => {
const b = await getState(page);
const p = b.planes.find((x) => x.id === 'plane-starter')!;
return p.status === 'available' && p.locationIcao === pkg.originIcao;
}, { timeout: 30_000, message: 'waiting for plane-starter to be grounded at the package origin (self-heal incomplete?)' }).toBe(true);
// LOAD via the UI: exactly one eligible plane (the starter at EDDF) → direct button.
await page.goto('/');
const loadBtn = page.getByTestId(`load-${pkg.id}`);
@@ -159,10 +173,17 @@ test('full core loop via UI: load → fly to destination → exact reward + rep
await expect(flyBtn).toBeEnabled();
await flyBtn.click();
let s = await getState(page);
const planeId = 'plane-starter';
expect(s.planes.find((p) => p.id === planeId)!.status).toBe('flying');
expect(s.planes.find((p) => p.id === planeId)!.destIcao).toBe(nearest.icao);
// The click fires the fly POST asynchronously (runAction is fire-and-forget from the test's view),
// so a getState issued immediately can race ahead of it and still see 'available' — poll until the
// fly actually took effect on the server (the run #36/#41 flake).
await expect.poll(async () => {
const b = await getState(page);
const p = b.planes.find((x) => x.id === planeId)!;
return p.status === 'flying' && p.destIcao === nearest.icao;
}, { timeout: 15_000, message: 'waiting for the fly click to take effect (status "flying" + dest set)' }).toBe(true);
let s = await getState(page);
// Fast-forward to arrival (a few seconds of real time at 1000×).
await setScale(page, SCALE);