Pause on landing + auto-unpause on dispatch (ISS-041)
CI / build + unit + e2e (pull_request) Successful in 7m43s
CI / build + unit + e2e (pull_request) Successful in 7m43s
Replace the ISS-011 auto-slow with pause-on-landing: when the last plane
in flight lands, tickOnce sets sim_state.paused=true WITHOUT touching
timeScale (speed preserved). flyPlane() now clears paused before
persisting so starting a flight resumes a paused sim — inside runAction(),
tick + unpause + dispatch are atomic under the sim lock.
Tests: engine.test.ts auto-slow block rewritten for pause semantics; new
flyPlane test asserts dispatch clears paused with speed untouched. E2E:
auto-slow.spec.ts renamed to pause-on-landing.spec.ts asserting the new
behavior (paused=true AND timeScale still 1000 after landing; /fly clears
paused). Cross-cutting fix: every per-spec setScale helper now POSTs
{timeScale, paused:false} — a scale-only fast-forward after a landing would
otherwise leave the clock frozen. Deliberate mid-test pauses untouched.
This commit is contained in:
+11
-9
@@ -66,8 +66,10 @@ 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.
|
||||
async function setScale(page: Page, scale: number): Promise<void> {
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale } });
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale, paused: false } });
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as { timeScale: number };
|
||||
expect(body.timeScale, `timeScale should be ${scale} after POST /api/sim`).toBe(scale);
|
||||
@@ -118,7 +120,7 @@ async function flyHomeViaApi(page: Page): Promise<void> {
|
||||
plane = s.planes.find((p) => p.id === 'plane-starter')!;
|
||||
}
|
||||
if (plane.locationIcao !== HUB) {
|
||||
// ISS-011: the landing above auto-slown the sim to 1× — re-set SCALE so the return leg
|
||||
// ISS-041: the landing above paused the sim — setScale clears pause 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 } });
|
||||
@@ -225,8 +227,8 @@ test('full core loop via UI: load → fly to destination → exact reward + rep
|
||||
const refuelCost = Math.round((afterRefuel.maxFuel - landed.fuel) * FUEL_PRICE_PER_L * 100) / 100;
|
||||
expect(s.account.cash).toBeCloseTo(before.account.cash + pkg.reward - refuelCost, 6);
|
||||
|
||||
// Restore: fly home (repositioning — no package needed in the v0.12.0 model). ISS-011: the
|
||||
// arrival above auto-slown the sim to 1×, so re-set SCALE for the return leg.
|
||||
// Restore: fly home (repositioning — no package needed in the v0.12.0 model). ISS-041: the
|
||||
// arrival above paused the sim, so re-set SCALE for the return leg (setScale clears pause).
|
||||
await setScale(page, SCALE);
|
||||
const homeRes = await page.request.post(`/api/planes/${planeId}/fly`, { data: { destIcao: HUB } });
|
||||
expect(homeRes.ok()).toBeTruthy();
|
||||
@@ -314,8 +316,8 @@ test('expired packages deliver and abandon with no reward and no penalty', async
|
||||
expect(s.packages.find((p) => p.id === pkgB.id)!.status).toBe('abandoned');
|
||||
expect(s.account.cash).toBe(cashBeforeAbandon); // free — expired abandons cost nothing
|
||||
|
||||
// Restore: refuel + fly home. ISS-011: the LEMD arrival above auto-slown the sim to 1×, so
|
||||
// re-set SCALE for the return leg (the expiry wait's scale no longer persists past a landing).
|
||||
// Restore: refuel + fly home. ISS-041: the LEMD arrival above paused the sim, so
|
||||
// re-set SCALE for the return leg (setScale clears pause; the expiry wait's scale is kept).
|
||||
await page.request.post('/api/planes/plane-starter/refuel');
|
||||
await setScale(page, SCALE);
|
||||
const homeRes = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
|
||||
@@ -355,8 +357,8 @@ test('load and fly validation paths (404 / 409 / 400)', async ({ page }) => {
|
||||
res = await page.request.post(`/api/packages/${pkg.id}/abandon`);
|
||||
expect(res.status()).toBe(409);
|
||||
|
||||
// Fly home from the hop so the fuel section starts at the hub. ISS-011: the hop arrival above
|
||||
// auto-slown the sim to 1×, so re-set SCALE for this leg.
|
||||
// Fly home from the hop so the fuel section starts at the hub. ISS-041: the hop arrival above
|
||||
// paused the sim, so re-set SCALE for this leg (setScale clears pause).
|
||||
await setScale(page, SCALE);
|
||||
res = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
|
||||
expect(res.ok()).toBeTruthy();
|
||||
@@ -380,7 +382,7 @@ test('load and fly validation paths (404 / 409 / 400)', async ({ page }) => {
|
||||
const atFar = sFar.planes.find((p) => p.id === 'plane-starter')!;
|
||||
expect(atFar.locationIcao).toBe(FAR);
|
||||
// The return leg fits: the weighted burn over it is less than what's left in the tank.
|
||||
// ISS-011: the FAR arrival above auto-slown the sim to 1×, so re-set SCALE for this leg.
|
||||
// ISS-041: the FAR arrival above paused the sim, so re-set SCALE for this leg (setScale clears pause).
|
||||
await setScale(page, SCALE);
|
||||
const backRes = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
|
||||
expect(backRes.ok()).toBeTruthy();
|
||||
|
||||
+6
-5
@@ -55,9 +55,10 @@ async function postSim(page: Page, body: unknown) {
|
||||
}
|
||||
|
||||
// Set the time scale and VERIFY from the response that it stuck — a silent reset would
|
||||
// otherwise surface as an unexplained landing-wait timeout.
|
||||
// otherwise surface as an unexplained landing-wait timeout. ISS-041 also clears pause: a
|
||||
// landing pauses the sim, so any fast-forward after one must resume it or the clock stays frozen.
|
||||
async function setScale(page: Page, scale: number): Promise<void> {
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale } });
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale, paused: false } });
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as { timeScale: number };
|
||||
expect(body.timeScale, `timeScale should be ${scale} after POST /api/sim`).toBe(scale);
|
||||
@@ -95,7 +96,7 @@ async function flyHomeViaApi(page: Page): Promise<void> {
|
||||
.toBe(true);
|
||||
}
|
||||
if (starter.locationIcao !== HUB) {
|
||||
// ISS-011: the landing above auto-slown the sim to 1× — re-set SCALE so the return leg
|
||||
// ISS-041: the landing above paused the sim — setScale clears pause 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-starter/fly', { data: { destIcao: HUB } });
|
||||
@@ -487,7 +488,7 @@ test('hovering an on-board block of a flying plane highlights its route', async
|
||||
}, { timeout: 120_000, message: `waiting for plane-starter to land at ${DEST}` })
|
||||
.toBe(true);
|
||||
|
||||
// ISS-011: the arrival above auto-slown the sim to 1×, so re-set SCALE for this leg.
|
||||
// ISS-041: the arrival above paused the sim, so re-set SCALE for this leg (setScale clears pause).
|
||||
await setScale(page, SCALE);
|
||||
const home = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
|
||||
expect(home.status()).toBe(200);
|
||||
@@ -579,7 +580,7 @@ test('hovering an on-board package while its carrier is mid-flight traces from c
|
||||
}, { timeout: 120_000, message: `waiting for plane-starter to land at ${DEST}` })
|
||||
.toBe(true);
|
||||
|
||||
// ISS-011: the arrival above auto-slown the sim to 1×, so re-set SCALE for this leg.
|
||||
// ISS-041: the arrival above paused the sim, so re-set SCALE for this leg (setScale clears pause).
|
||||
await setScale(page, SCALE);
|
||||
const home = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
|
||||
expect(home.status()).toBe(200);
|
||||
|
||||
@@ -69,9 +69,10 @@ async function postSim(page: Page, body: unknown) {
|
||||
}
|
||||
|
||||
// Set the time scale and VERIFY from the response that it stuck — a silent reset would
|
||||
// otherwise surface as an unexplained landing-wait timeout.
|
||||
// otherwise surface as an unexplained landing-wait timeout. ISS-041 also clears pause: a
|
||||
// landing pauses the sim, so any fast-forward after one must resume it or the clock stays frozen.
|
||||
async function setScale(page: Page, scale: number): Promise<void> {
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale } });
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale, paused: false } });
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as { timeScale: number };
|
||||
expect(body.timeScale, `timeScale should be ${scale} after POST /api/sim`).toBe(scale);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
// v0.19.0 — ISS-011: when the LAST plane lands (no plane remains in flight) the sim auto-slows
|
||||
// to 1×, server-side and persisted on the sim_state row so the UI slider reflects it on the next
|
||||
// snapshot. Arrival-triggered by design: a grounded fleet at high scale is left alone, there is no
|
||||
// auto-restore — the player speeds up manually again (asserted below).
|
||||
// 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.
|
||||
|
||||
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)
|
||||
@@ -33,8 +34,10 @@ 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.
|
||||
async function setScale(page: Page, scale: number): Promise<void> {
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale } });
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale, paused: false } });
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as SimState;
|
||||
expect(body.timeScale, `timeScale should be ${scale} after POST /api/sim`).toBe(scale);
|
||||
@@ -75,7 +78,7 @@ async function flyHomeViaApi(page: Page): Promise<void> {
|
||||
plane = s.planes.find((p) => p.id === 'plane-starter')!;
|
||||
}
|
||||
if (plane.locationIcao !== HUB) {
|
||||
// ISS-011: the landing above auto-slown the sim to 1× — re-set SCALE so the return leg
|
||||
// ISS-041: the landing above paused the sim — setScale clears pause 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 } });
|
||||
@@ -93,8 +96,8 @@ test.afterEach(async ({ page }) => {
|
||||
|
||||
test.describe.configure({ mode: 'serial' }); // shared live DB — order matters within this file
|
||||
|
||||
test('auto-slow to 1× when the last plane lands; manual speed-up still works (ISS-011)', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal (worst case ~20 s at 1000×) + one short leg
|
||||
test('last landing pauses the sim without changing speed; dispatch auto-resumes (ISS-041)', 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);
|
||||
@@ -117,14 +120,19 @@ test('auto-slow to 1× when the last plane lands; manual speed-up still works (I
|
||||
}, { timeout: 120_000, message: `waiting for plane-starter to land at ${nearest.icao}` })
|
||||
.toBe(true);
|
||||
|
||||
// …and the server has auto-slown to 1× — persisted on sim_state, so this very snapshot shows it.
|
||||
expect(landedBody!.simState.timeScale).toBe(1);
|
||||
// …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);
|
||||
|
||||
// Manual override still works: POST /api/sim {timeScale: 1000} → confirmed by the response…
|
||||
const post = await page.request.post('/api/sim', { data: { timeScale: SCALE } });
|
||||
expect(post.status()).toBe(200);
|
||||
expect(((await post.json()) as SimState).timeScale).toBe(SCALE);
|
||||
// …and by a fresh snapshot (no auto-restore of the previous scale — and no re-slow either,
|
||||
// since nothing is in flight: the trigger is arrival-based, not idle-enforcement).
|
||||
expect((await getState(page)).simState.timeScale).toBe(SCALE);
|
||||
// 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.
|
||||
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
|
||||
|
||||
// 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');
|
||||
});
|
||||
@@ -31,8 +31,10 @@ 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.
|
||||
async function setScale(page: Page, scale: number): Promise<void> {
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale } });
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale, paused: false } });
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as { timeScale: number };
|
||||
expect(body.timeScale, `timeScale should be ${scale} after POST /api/sim`).toBe(scale);
|
||||
@@ -72,7 +74,7 @@ async function flyHomeViaApi(page: Page): Promise<void> {
|
||||
plane = s.planes.find((p) => p.id === 'plane-starter')!;
|
||||
}
|
||||
if (plane.locationIcao !== HUB) {
|
||||
// ISS-011: the landing above auto-slown the sim to 1× — re-set SCALE so the return leg
|
||||
// ISS-041: the landing above paused the sim — setScale clears pause 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 } });
|
||||
|
||||
@@ -69,9 +69,10 @@ async function postSim(page: Page, body: unknown) {
|
||||
}
|
||||
|
||||
// Set the time scale and VERIFY from the response that it stuck — a silent reset would
|
||||
// otherwise surface as an unexplained landing-wait timeout.
|
||||
// otherwise surface as an unexplained landing-wait timeout. ISS-041 also clears pause: a
|
||||
// landing pauses the sim, so any fast-forward after one must resume it or the clock stays frozen.
|
||||
async function setScale(page: Page, scale: number): Promise<void> {
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale } });
|
||||
const res = await page.request.post('/api/sim', { data: { timeScale: scale, paused: false } });
|
||||
expect(res.status()).toBe(200);
|
||||
const body = (await res.json()) as { timeScale: number };
|
||||
expect(body.timeScale, `timeScale should be ${scale} after POST /api/sim`).toBe(scale);
|
||||
@@ -176,7 +177,7 @@ async function flyHomeViaApi(page: Page): Promise<void> {
|
||||
.toBe(true);
|
||||
}
|
||||
if (starter.locationIcao !== HUB) {
|
||||
// ISS-011: the landing above auto-slown the sim to 1× — re-set SCALE so the return leg
|
||||
// ISS-041: the landing above paused the sim — setScale clears pause 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-starter/fly', { data: { destIcao: HUB } });
|
||||
@@ -289,7 +290,7 @@ test('on-board list: fly-to-destination + abandon with penalty preview; flying p
|
||||
await expect(onboardRow).not.toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Restore: fly home so the live instance stays in its onboarding state for the next run.
|
||||
// ISS-011: the arrival above auto-slown the sim to 1×, so re-set SCALE for this leg.
|
||||
// ISS-041: the arrival above paused the sim, so re-set SCALE for this leg (setScale clears pause).
|
||||
await setScale(page, SCALE);
|
||||
const homeRes = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
|
||||
expect(homeRes.status()).toBe(200);
|
||||
@@ -411,8 +412,8 @@ test('on-board row Refuel tops up just enough for the package leg; left-pane but
|
||||
await expect(page.getByTestId('market-refuel')).toHaveText('Refuel to full');
|
||||
|
||||
// Restore: fly home — pkg2's destination IS the hub, so it delivers on arrival. The tank holds
|
||||
// exactly the leg requirement, so this also proves "just enough" is enough. ISS-011: the far-end
|
||||
// arrival above auto-slown the sim to 1×, so re-set SCALE for this leg.
|
||||
// exactly the leg requirement, so this also proves "just enough" is enough. ISS-041: the far-end
|
||||
// arrival above paused the sim, so re-set SCALE for this leg (setScale clears pause).
|
||||
await setScale(page, SCALE);
|
||||
expect((await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } })).status()).toBe(200);
|
||||
await expect
|
||||
|
||||
Reference in New Issue
Block a user