Slow sim to 1× on landing and resume last selected speed (ISS-057, v0.30.0)
CI / build + unit + e2e (pull_request) Successful in 8m57s

This commit is contained in:
2026-09-20 20:27:59 +00:00
parent 5e7ec903d3
commit c9e9113a45
13 changed files with 151 additions and 73 deletions
@@ -1,11 +1,10 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
// v0.24.0 — ISS-041: when the LAST plane lands (no plane remains in flight) the sim pauses
// server-side WITHOUT touching timeScale (speed is preserved), and starting a flight
// (POST /api/planes/{id}/fly) auto-resumes a paused sim. The cycle: dispatch unpauses →
// last landing pauses → next dispatch unpauses again. Arrival-triggered by design: a grounded
// fleet at high scale is left alone, there is no idle-enforcement or auto-restore of pause.
// v0.30.0 — ISS-057: when a landing leaves any plane on the ground the sim slows to 1× server-side
// WITHOUT pausing (pause is manual-only now), and starting a flight (POST /api/planes/{id}/fly)
// restores the last MANUALLY selected speed once no plane remains grounded. The cycle: dispatch at
// 1000× → landing slows to 1× → next dispatch (last grounded plane airborne) resumes 1000×.
const SCALE = 1000; // fastest sim speed — top of the v0.14.0 ladder (same convention as e2e/sim.spec.ts)
const HUB = 'EDDF'; // Frankfurt Main — the seeded starter plane's home base (lib/game/seed.ts)
@@ -34,8 +33,8 @@ async function postSim(page: Page, body: Record<string, unknown>): Promise<void>
// Set the time scale and VERIFY from the response that it stuck — if something external ever
// resets the live instance's controls mid-test, fail loudly here instead of timing out later.
// ISS-041 also clears pause: a landing pauses the sim, so any fast-forward after one must
// resume it or the clock stays frozen and the next wait hangs.
// ISS-057 also records this as lastSelectedTimeScale (the resume target) and clears pause, so any
// fast-forward after a manual pause or a slowed-to-1× landing proceeds at full speed.
async function setScale(page: Page, scale: number): Promise<void> {
const res = await page.request.post('/api/sim', { data: { timeScale: scale, paused: false } });
expect(res.status()).toBe(200);
@@ -78,8 +77,8 @@ async function flyHomeViaApi(page: Page): Promise<void> {
plane = s.planes.find((p) => p.id === 'plane-starter')!;
}
if (plane.locationIcao !== HUB) {
// ISS-041: the landing above paused the sim — setScale clears pause so the return leg
// fast-forwards instead of taking real-time hours.
// ISS-057: the landing above slowed the sim to 1× — setScale restores 1000× (and records it as
// lastSelectedTimeScale) so the return leg fast-forwards instead of taking real-time hours.
await setScale(page, SCALE);
const res = await page.request.post(`/api/planes/${plane.id}/fly`, { data: { destIcao: HUB } });
expect(res.ok()).toBeTruthy(); // repositioning needs no package (v0.12.0 manual flight)
@@ -96,13 +95,13 @@ test.afterEach(async ({ page }) => {
test.describe.configure({ mode: 'serial' }); // shared live DB — order matters within this file
test('last landing pauses the sim without changing speed; dispatch auto-resumes (ISS-041)', async ({ page }) => {
test('landing slows the sim to 1× without pausing; dispatch restores the last selected speed (ISS-057)', async ({ page }) => {
test.setTimeout(300_000); // self-heal (worst case ~20 s at 1000×) + two short legs
await flyHomeViaApi(page); // deterministic: the starter sits grounded at EDDF
const nearest = await nearestToHub(page);
// Set 1000× via the API and verify it stuck.
// Set 1000× via the API and verify it stuck (also records lastSelectedTimeScale=1000).
await setScale(page, SCALE);
// Dispatch plane-starter on the shortest leg from its hub (repositioning — no package needed).
@@ -120,18 +119,20 @@ test('last landing pauses the sim without changing speed; dispatch auto-resumes
}, { timeout: 120_000, message: `waiting for plane-starter to land at ${nearest.icao}` })
.toBe(true);
// …and the server paused the sim — persisted on sim_state, so this very snapshot shows it…
expect(landedBody!.simState.paused).toBe(true);
// …with timeScale UNCHANGED (no auto-slow anymore): speed is preserved for the next dispatch.
expect(landedBody!.simState.timeScale).toBe(SCALE);
// …and the server slowed the sim to 1× WITHOUT pausing — persisted on sim_state, so this very
// snapshot shows it (the just-landed plane is on ground → slow; ISS-057).
expect(landedBody!.simState.paused).toBe(false);
expect(landedBody!.simState.timeScale).toBe(1);
// A subsequent /fly dispatch auto-resumes a paused sim — asserted on the endpoint's OWN full
// post-action snapshot, so no poll race with the client's next tick.
// A subsequent /fly dispatch restores the last selected speed — asserted on the endpoint's OWN
// full post-action snapshot, so no poll race with the client's next tick. setScale posted 1000
// above, so lastSelectedTimeScale is 1000 and this dispatch (sole grounded plane → airborne)
// resumes it.
const fly2 = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
expect(fly2.status()).toBe(200);
const resumedBody = (await fly2.json()) as StateBody;
expect(resumedBody.simState.paused).toBe(false); // ISS-041 auto-unpause…
expect(resumedBody.simState.timeScale).toBe(SCALE); // …with the preserved speed still in effect
expect(resumedBody.simState.paused).toBe(false); // dispatch still clears a manual pause…
expect(resumedBody.simState.timeScale).toBe(SCALE); // …and restores the last selected speed
// Let the return leg finish so cleanup starts from a grounded plane.
await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.status), { timeout: 120_000 }).toBe('available');