diff --git a/e2e/phases.spec.ts b/e2e/phases.spec.ts index 25ddb37..805ec9c 100644 --- a/e2e/phases.spec.ts +++ b/e2e/phases.spec.ts @@ -105,7 +105,7 @@ async function fitToAirports(page: Page, icaos: string[]): Promise { // fetch can be stale by tens of px) and wait for the popup. `flyingOnly` skips grounded frames — // used right after dispatch, when the last rendered frame may predate it. async function clickPlane(page: Page, flyingOnly: boolean): Promise { - const pt = await page.evaluate(async (onlyFlying: boolean) => { + const pt = await page.evaluate(async ({ onlyFlying, starterId }: { onlyFlying: boolean; starterId: string }) => { const m = (window as any).__flightMap; if (!m) return null; const deadline = Date.now() + 10_000; @@ -116,7 +116,7 @@ async function clickPlane(page: Page, flyingOnly: boolean): Promise { } catch { // map not ready yet — keep polling } - const f = feats.find((x) => x.properties?.id === STARTER && (!onlyFlying || x.properties?.flying === true)); + const f = feats.find((x) => x.properties?.id === starterId && (!onlyFlying || x.properties?.flying === true)); if (f && f.geometry) { const projected = m.project(f.geometry.coordinates); const rect = m.getContainer().getBoundingClientRect(); @@ -125,7 +125,7 @@ async function clickPlane(page: Page, flyingOnly: boolean): Promise { if (Date.now() >= deadline) return null; await new Promise((r) => setTimeout(r, 100)); } - }, flyingOnly); + }, { onlyFlying: flyingOnly, starterId: STARTER }); expect(pt, 'plane marker not rendered').not.toBeNull(); // Popups are created per click and never auto-close — remove any stale ones so the testids below diff --git a/e2e/right-pane.spec.ts b/e2e/right-pane.spec.ts index 4c3093d..d17633d 100644 --- a/e2e/right-pane.spec.ts +++ b/e2e/right-pane.spec.ts @@ -154,39 +154,29 @@ async function createPackage(page: Page, fromIcao: string, toIcao: string, deadl return (await created.json()) as PackageState; } -// Self-heal: let any in-flight leg finish, then reposition plane-starter home via the API. +// Self-heal: let any in-flight leg finish, then reposition plane-starter home via the API. The end +// state is verified by ONE poll on FRESH snapshots (v1.12.0): legs now include takeoff/landing holds, +// so a leftover leg can land at its destination after an earlier snapshot was taken — deciding from a +// stale pre-flight snapshot let tests load cargo onto a plane that wasn't actually at the hub (409). async function flyHomeViaApi(page: Page): Promise { // v0.39.0 (ISS-069): wear accumulates across the shared CI DB — restore full health and clear any // active repair gate from an interrupted run before the steps below act on the starter, so the // farthest-flyable picker and fuel pins never see a worn or broken plane. expect((await page.request.post('/api/test/set-health', { data: { planeId: 'plane-starter', health: 100, repairUntilMs: 0 } })).status()).toBe(200); - const s0 = await getState(page); // ticks — a shortened gate flips the plane back to available here - const starter = s0.planes.find((p) => p.id === 'plane-starter')!; - if (starter.status === 'flying' || starter.locationIcao !== HUB) { - // Fast-forward any leftover leg / repositioning flight (~20 s real time worst case at 1000×). - await setScale(page, SCALE); - } - if (starter.status === 'flying') { - await expect - .poll(async () => (await getState(page)).planes.find((p) => p.id === 'plane-starter')?.status === 'available', { - timeout: 240_000, - message: 'waiting for an in-flight plane-starter to land', - }) - .toBe(true); - } - if (starter.locationIcao !== HUB) { - // Make sure the world rate is back at SCALE so the return leg fast-forwards. ISS-082: dispatch - // auto-refuels, so a grounded plane can always start any full-tank-flyable leg when cash allows — no top-up retry needed. - await setScale(page, SCALE); - const flyHome = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } }); - expect(flyHome.status()).toBe(200); // repositioning needs no package (v0.12.0 manual flight) - await expect - .poll(async () => (await getState(page)).planes.find((p) => p.id === 'plane-starter')?.locationIcao === HUB, { - timeout: 240_000, - message: 'waiting for plane-starter to land back at the hub', - }) - .toBe(true); - } + // Fast-forward any leftover leg / repositioning flight (a few seconds real time worst case at 1000×). + await setScale(page, SCALE); + await expect.poll(async () => { + const p = (await getState(page)).planes.find((x) => x.id === 'plane-starter')!; + if (p.status === 'available' && p.locationIcao === HUB) return 'ok'; + if (p.status === 'available') { + // ISS-082: dispatch auto-refuels, so a grounded plane can always start any full-tank-flyable leg + // when cash allows — no top-up retry needed. Fail loud with the server's reason otherwise. + const flyHome = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } }); + if (flyHome.status() !== 200) throw new Error(`reposition home failed (${flyHome.status()}): ${await flyHome.text()}`); + return `flying home from ${p.locationIcao}`; + } + return p.status; // flying or repairing — keep waiting (SCALE makes both finish quickly) + }, { timeout: 240_000, message: 'waiting for plane-starter to be grounded at the hub' }).toBe('ok'); // Abandon leftover loaded/expired packages (the baseline is captured AFTER this cleanup). const s1 = await getState(page); for (const pkg of s1.packages) {