Fixed world clock: no player time controls (ISS-117)
CI / build + unit + e2e (pull_request) Failing after 12m10s

The world always runs at WORLD_TIME_SCALE = 24 (lib/worldClock.ts). A server
background tick (instrumentation.ts -> startBackgroundTick, 1 s interval)
advances the sim with no open browser; idle-pause freezes the clock exactly
while no plane is flying or in repair. Fresh worlds start year 2000
(STARTING_CLOCK_MS in seed.ts).

- Removed: POST /api/sim + OpenAPI entry, applyTimeControls/clampTimeScale,
  lib/timeScale.ts, LeftPane pause button + scale slider machinery,
  ISS-057 slow-on-landing/resume in flyPlane.
- Snapshot simState is now { clockMs } only; client extrapolates at the fixed
  rate between polls and freezes while idle (useSimClock).
- New test-only hook POST /api/test/set-timescale behind FLIGHT_TEST_HOOKS=1
  (sibling of set-health); all e2e setScale helpers call it, afterEach restores
  the world rate. E2E pause tests replaced by idle-pause assertions; sim.spec.ts
  activates the world via set-health + /repair for advance/expiry tests.
This commit is contained in:
2026-09-22 10:50:21 +00:00
parent d31bc5d60f
commit a0560ec767
46 changed files with 631 additions and 1502 deletions
+14 -19
View File
@@ -14,7 +14,8 @@ import { fuelAtWeighted, requiredFuelFor, windBurnFactor } from '../lib/sim/move
// dest → queue[0] → … — the active leg stays in the solid `routes-legs` layer. Landing auto-starts
// the next queued leg server-side (part 1); test 3 exercises that chain end-to-end through the API.
const SCALE = 1000; // fastest sim speed (v0.14.0 ladder top) — keeps flights to seconds of real time
const SCALE = 1000; // test rate for fast-forwarding legs — keeps flights to seconds of real time
const WORLD = 24; // the fixed world rate (ISS-117) — must match lib/worldClock.ts
const HUB = 'EDDF'; // must match HUB_ICAO in lib/game/seed.ts
const DEST = 'LEMD'; // Madrid–Barajas — the typical long leg (~10 s at scale 1000)
const QUEUE = 'EHAM'; // Amsterdam — a third airport, distinct from hub and destination
@@ -40,7 +41,7 @@ interface PackageState {
interface StateBody {
account: { cash: number; reputation: number };
simState: { clockMs: number; timeScale: number; paused: boolean };
simState: { clockMs: number }; // ISS-117: the snapshot carries only the game clock
planes: PlaneState[];
packages: PackageState[];
}
@@ -51,23 +52,17 @@ async function getState(page: Page): Promise<StateBody> {
return (await res.json()) as StateBody;
}
async function postSim(page: Page, body: unknown) {
return page.request.post('/api/sim', { data: body });
}
// Set the time scale and VERIFY from the response that it stuck. ISS-041 also clears pause: a
// landing pauses the sim, so any fast-forward after one must resume it or the clock stays frozen.
// Test hook (FLIGHT_TEST_HOOKS=1): set the world rate and VERIFY from the response that it stuck.
async function setScale(page: Page, scale: number): Promise<void> {
const res = await page.request.post('/api/sim', { data: { timeScale: scale, paused: false } });
const res = await page.request.post('/api/test/set-timescale', { data: { timeScale: scale } });
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);
expect(body.timeScale, `timeScale should be ${scale} after POST /api/test/set-timescale`).toBe(scale);
}
// Leave the live instance in a sane state for the other specs (scale 1, running).
// Leave the live instance in a sane state for the other specs (the fixed world rate).
test.afterEach(async ({ page }) => {
const res = await postSim(page, { timeScale: 1, paused: false });
expect(res.status()).toBe(200);
await setScale(page, WORLD);
});
// Test hook (FLIGHT_TEST_HOOKS=1): create an open package on a known route.
@@ -119,7 +114,7 @@ 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.
// 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.id}/fly`, { data: { destIcao: HUB } });
expect(res.status()).toBe(200); // repositioning needs no package (v0.12.0 manual flight)
@@ -259,7 +254,7 @@ test('queue display + cancel via UI (ISS-043 part 2)', async ({ page }) => {
const p = (await getState(page)).planes.find((x) => x.id === 'plane-starter');
return !!p && p.status === 'flying' && p.destIcao === DEST;
}, { timeout: 15_000, message: 'waiting for the fly click to take effect (status "flying" + dest set)' }).toBe(true);
await postSim(page, { paused: true }); // pause-sim pattern: deterministic window for UI clicks
await setScale(page, 1); // scale-1 freeze pattern: deterministic window for UI clicks
// ISS-065: re-queuing the CURRENT leg's destination while flying must be rejected with 409.
const dupQueue = await page.request.post('/api/planes/plane-starter/queue', { data: { destIcao: DEST } });
@@ -286,7 +281,7 @@ test('queue display + cancel via UI (ISS-043 part 2)', async ({ page }) => {
return !!p && p.status === 'available' && p.locationIcao === DEST;
}, { timeout: 120_000, message: `waiting for plane-starter to land at ${DEST}` }).toBe(true);
// ISS-041: the arrival above paused the sim — setScale clears pause for the return leg.
// Re-set SCALE for the return leg.
await setScale(page, SCALE);
const home = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
expect(home.status()).toBe(200);
@@ -316,8 +311,8 @@ test('dotted queue line renders the queued path on the map (ISS-043 part 2)', as
const flyBody = (await flyRes.json()) as StateBody;
expect(flyBody.planes.find((p) => p.id === 'plane-starter')?.status).toBe('flying');
// Freeze mid-flight so the ~10 s leg can't land under us while we assert.
await postSim(page, { paused: true });
// Freeze mid-flight (scale 1) so the ~10 s leg can't land under us while we assert.
await setScale(page, 1);
await page.goto('/');
await waitForAirportCount(page);
@@ -367,7 +362,7 @@ test('dotted queue line renders the queued path on the map (ISS-043 part 2)', as
return !!p && p.status === 'available' && p.locationIcao === DEST;
}, { timeout: 120_000, message: `waiting for plane-starter to land at ${DEST}` }).toBe(true);
// ISS-041: the arrival above paused the sim — setScale clears pause for the return leg.
// Re-set SCALE for the return leg.
await setScale(page, SCALE);
const home = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
expect(home.status()).toBe(200);