diff --git a/CHANGELOG.md b/CHANGELOG.md index b669695..41fc2b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## v0.55.0 — 2026-09-22 15:38 - **Favicon (ISS-122).** The app shipped with no favicon at all (`app/layout.tsx` set only title/description; no `app/icon.svg`, no `public/favicon.ico`). New hand-written single-file `app/icon.svg` — a bold white top-down airplane silhouette (fuselage + swept wings + tail, one straight-line path) on a pastel blue (#93c5fd) rounded square, 64×64 viewBox. Next App Router's file convention auto-serves it at `/icon.svg` and injects `` into the head; no layout/metadata changes needed. - **Tests.** E2E +1 in `e2e/panes.spec.ts`: page load asserts exactly one `head link[rel~="icon"]` pointing at `/icon.svg`, and a direct `GET /icon.svg` returns 200 with content-type `image/svg+xml`. No unit test — static asset. +- **E2E hardening (CI run #167).** Two e2e-only fixes: the favicon assertion now tolerates the content-hash query string Next App Router production builds append to the auto-injected icon `` (`/icon.svg?icon..svg` — matched with `/^\/icon\.svg/`, canonical `GET /icon.svg` check unchanged), and `e2e/hover.spec.ts`'s local `flyHomeViaApi` self-heal is replaced by the established `e2e/panes.spec.ts` pattern (set-health repair-gate reset + re-read state AFTER an in-flight leg lands) — the old version checked `locationIcao` against the stale pre-landing snapshot, so a retry after a mid-flight interruption left plane-starter grounded at the destination and its package load 409'd ("plane not at the origin"). ## v0.54.0 — 2026-09-22 15:20 - **Long priority jobs are now always finishable (ISS-119).** `generatePackage` in `lib/game/seed.ts` drew a flat 6–72 h expiry window (priority ×0.5 → 3–36 h) independent of leg distance, so some jobs' deadlines were shorter than the flight itself and could never be completed (legs go up to MAX_JOB_LEG_KM = 9000 km; the fastest catalog plane is ~920 km/h ≈ 10 h for a max leg). The drawn window is now clamped UP after special scaling: deadline ≥ now + `travelDurationMs(legKm, FASTEST_CATALOG_SPEED_KMH)` + `EXPIRY_BUFFER_MS` (flat 1 h), reusing the existing `travelDurationMs` from `lib/sim/movement.ts`. New constant `FASTEST_CATALOG_SPEED_KMH` in `lib/game/catalog.ts` — data-driven max of `PLANE_TYPES` speeds, so a job must be finishable with the best available aircraft (NOT the starter's 510 km/h, which would wreck long-job economics); adding a faster plane tightens the floor automatically. `EXPIRY_BUFFER_MS` lives next to the deadline tunables in seed.ts. The window is a single uniform draw, so clamping up covers both requirements: short legs (drawn × scale already above the floor) are byte-identical to before, and when the raw draw falls below the floor the deadline lands exactly on it — which stays ≤ the scaled ceiling for any pair of airports on Earth (max leg ~20000 km → floor ≈ 22.8 h < the 36 h priority ceiling), so no separate ceiling raise is needed. Forced `deadlineOffsetMs` skips the floor, same as it skips special scaling (the e2e expiry hook keeps working). diff --git a/e2e/hover.spec.ts b/e2e/hover.spec.ts index 64c8d85..bbfb94a 100644 --- a/e2e/hover.spec.ts +++ b/e2e/hover.spec.ts @@ -84,33 +84,30 @@ async function setFuelFull(page: Page, planeId: string): Promise { expect((await page.request.post('/api/test/set-fuel', { data: { planeId, fuel: p.maxFuel } })).status()).toBe(200); } -// Self-heal: let any in-flight leg finish, then reposition plane-starter home via the API. +// Self-heal (same pattern as e2e/panes.spec.ts): restore full health / clear any repair gate from an +// interrupted run, let any in-flight leg finish, then reposition plane-starter home via the API. async function flyHomeViaApi(page: Page): Promise { - const s0 = await getState(page); - const starter = s0.planes.find((p) => p.id === 'plane-starter')!; + // 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 acting on the starter. + expect((await page.request.post('/api/test/set-health', { data: { planeId: 'plane-starter', health: 100, repairUntilMs: 0 } })).status()).toBe(200); + let s = await getState(page); // ticks — a shortened gate flips the plane back to available here + let starter = s.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); + // While flying, locationIcao stays at the DEPARTURE airport — re-read state after landing. + await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')?.status ?? ''), { timeout: 240_000 }).toBe('available'); + s = await getState(page); + starter = s.planes.find((p) => p.id === 'plane-starter')!; } if (starter.locationIcao !== HUB) { // Make sure the world rate is back at SCALE so the return leg fast-forwards. await setScale(page, SCALE); const res = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } }); expect(res.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); + await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')?.locationIcao ?? ''), { timeout: 240_000 }).toBe(HUB); } } diff --git a/e2e/panes.spec.ts b/e2e/panes.spec.ts index 6e82f92..ca21d26 100644 --- a/e2e/panes.spec.ts +++ b/e2e/panes.spec.ts @@ -198,7 +198,8 @@ test('favicon ships as /icon.svg (ISS-122)', async ({ page }) => { // App Router file convention: app/icon.svg is auto-served and injected into the head. const link = page.locator('head link[rel~="icon"]'); await expect(link).toHaveCount(1); - expect(await link.getAttribute('href')).toBe('/icon.svg'); + // Production builds append a content-hash query string to the auto-injected icon href. + expect(await link.getAttribute('href')).toMatch(/^\/icon\.svg/); const res = await page.request.get('/icon.svg'); expect(res.status()).toBe(200);