Fixed world clock: no player time controls (ISS-117)
CI / build + unit + e2e (pull_request) Failing after 12m10s
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:
@@ -14,7 +14,8 @@ const HUB = 'EDDF'; // must match HUB_ICAO in lib/game/seed.ts
|
||||
// land in ~2 s — shorter than the map poll + click pipeline). Round-trip burn (~4970 L) fits
|
||||
// the 6300 L tank, so a full-tank departure always makes it home.
|
||||
const DEST = 'LEMD';
|
||||
const SCALE = 1000; // fastest sim speed (v0.14.0 ladder top)
|
||||
const SCALE = 1000; // test rate for fast-forwarding legs
|
||||
const WORLD = 24; // the fixed world rate (ISS-117) — must match lib/worldClock.ts
|
||||
|
||||
interface PlaneState {
|
||||
id: string;
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -64,24 +65,18 @@ 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 — a silent reset would
|
||||
// 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.
|
||||
// Test hook (FLIGHT_TEST_HOOKS=1): set the world rate and VERIFY from the response that it stuck —
|
||||
// a silent reset would otherwise surface as an unexplained landing-wait timeout.
|
||||
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.
|
||||
@@ -235,7 +230,7 @@ test('plane markers move while flying and the plane popup shows status + route',
|
||||
// 2) Fly the plane and speed up — its current leg must appear as a route line.
|
||||
const fly = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: DEST } });
|
||||
expect(fly.status()).toBe(200);
|
||||
await postSim(page, { timeScale: SCALE });
|
||||
await setScale(page, SCALE);
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
@@ -258,10 +253,11 @@ test('plane markers move while flying and the plane popup shows status + route',
|
||||
// The arc itself stays covered by unit tests: lib/mapData.test.ts.)
|
||||
|
||||
// 4) Click the flying plane → status popup with model, status, fuel and the leg.
|
||||
// Freeze the sim so the click can't miss a fast-moving marker: at scale 1000 the rendered
|
||||
// marker lags our fresh /api/state fetch by up to one map poll (~1 s) — tens of px of drift
|
||||
// on this leg. Pausing makes the position static; we resume right after the popup checks.
|
||||
await postSim(page, { paused: true });
|
||||
// Freeze the sim so the click can't miss a moving marker: at scale 1000 the rendered marker lags
|
||||
// our fresh /api/state fetch by up to one map poll (~1 s). At scale 1 (and this continent-level
|
||||
// view) the per-second drift is sub-pixel, so the position is static for all practical purposes;
|
||||
// we restore SCALE right after the popup checks.
|
||||
await setScale(page, 1);
|
||||
let prevPos: { lat: number; lon: number } | null = null;
|
||||
await expect
|
||||
.poll(async () => {
|
||||
@@ -270,11 +266,11 @@ test('plane markers move while flying and the plane popup shows status + route',
|
||||
if (prevPos && prevPos.lat === p.lat && prevPos.lon === p.lon) return true;
|
||||
prevPos = { lat: p.lat, lon: p.lon };
|
||||
return false;
|
||||
}, { timeout: 15_000, message: 'waiting for the paused clock to freeze the plane position' })
|
||||
}, { timeout: 15_000, message: 'waiting for the scale-1 clock to settle the plane position' })
|
||||
.toBe(true);
|
||||
|
||||
// Click the RENDERED marker position, not a fresh /api/state fetch. With the sim paused
|
||||
// above the server's analytic position is static — but the map renders from its own ~1 s
|
||||
// Click the RENDERED marker position, not a fresh /api/state fetch. With the clock at scale 1
|
||||
// above the server's analytic position barely moves — but the map renders from its own ~1 s
|
||||
// poll, so right after the freeze confirmation the last rendered frame can still be up to
|
||||
// one poll interval stale (tens of px at scale 1000 on this leg) and a click projected
|
||||
// from a fresh fetch misses the small circle layer (CI run #37). Poll for the plane's
|
||||
@@ -314,8 +310,8 @@ test('plane markers move while flying and the plane popup shows status + route',
|
||||
// The popup's route line is the plane's own leg: origin name → destination name.
|
||||
await expect(page.getByTestId('plane-route')).toContainText(`${live.locationName} → ${live.destName}`);
|
||||
|
||||
// Resume before step 5 — the landing wait needs a running clock.
|
||||
await postSim(page, { timeScale: SCALE, paused: false });
|
||||
// Restore SCALE before step 5 — the landing wait needs a fast clock.
|
||||
await setScale(page, SCALE);
|
||||
|
||||
// 5) Let the flight finish, then fly home so the live instance stays in its
|
||||
// onboarding state (starter grounded at EDDF) for the other specs. The loaded
|
||||
|
||||
Reference in New Issue
Block a user