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:
+21
-45
@@ -10,14 +10,15 @@ import { windBurnFactor } from '../lib/sim/movement';
|
||||
// if both planes are on the same airport") was UX-only: LoadControls' multi-plane path rendered a
|
||||
// greyed-out Load button until a plane was picked from the dropdown, while the single-plane path
|
||||
// is one-click. v0.31.0 preselects the first eligible plane (components/RightPane.tsx), and this
|
||||
// spec covers every two-plane flow: load (preselect + picker), parallel flights with the ISS-057
|
||||
// slow/resume semantics, relocate, per-plane queues (scoped testids) and sell.
|
||||
// spec covers every two-plane flow: load (preselect + picker), parallel flights, relocate,
|
||||
// per-plane queues (scoped testids) and sell.
|
||||
//
|
||||
// Each test buys its own second plane via the UI (after a cash grant from the new
|
||||
// POST /api/test/add-cash hook — ATR €200k vs the €20k starting balance); beforeEach/afterEach
|
||||
// guarantee the fleet is exactly one plane (the starter, grounded at EDDF) before and after.
|
||||
|
||||
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 STARTER = 'plane-starter'; // the seeded plane's id (lib/game/seed.ts)
|
||||
const BUY_MODEL = 'atr72'; // cheapest catalog entry — the second plane every test buys
|
||||
@@ -54,7 +55,7 @@ interface CatalogEntry {
|
||||
|
||||
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[];
|
||||
catalog: CatalogEntry[];
|
||||
@@ -71,18 +72,12 @@ 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. Also records lastSelectedTimeScale
|
||||
// (the ISS-057 resume target) and clears pause — so any fast-forward after a slowed-to-1× landing
|
||||
// proceeds at full speed.
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Mirrors haversineKm in lib/geo.ts (same formula → identical floats).
|
||||
@@ -230,17 +225,14 @@ async function healPlaneHome(page: Page, planeId: string): Promise<void> {
|
||||
await clearQueueViaApi(page, planeId);
|
||||
let p = (await getState(page)).planes.find((x) => x.id === planeId);
|
||||
if (!p) return; // already sold — nothing to heal
|
||||
// ISS-057: a prior landing may have slowed the sim to 1× — restore 1000× first so any in-flight leg
|
||||
// completes fast. Re-applied inside the poll below, since a sibling's landing mid-wait slows it again.
|
||||
// Make sure the world rate is at SCALE so any in-flight leg completes fast.
|
||||
await setScale(page, SCALE);
|
||||
if (p.status === 'flying') {
|
||||
// While flying, locationIcao stays at the DEPARTURE airport — re-read state after landing.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const s = await getState(page);
|
||||
const plane = s.planes.find((x) => x.id === planeId);
|
||||
if (plane?.status === 'flying' && s.simState.timeScale !== SCALE) await setScale(page, SCALE); // sibling landing re-slowed to 1×
|
||||
return plane?.status ?? '';
|
||||
return s.planes.find((x) => x.id === planeId)?.status ?? '';
|
||||
}, {
|
||||
timeout: 240_000,
|
||||
message: `waiting for an in-flight ${planeId} to land`,
|
||||
@@ -249,9 +241,8 @@ async function healPlaneHome(page: Page, planeId: string): Promise<void> {
|
||||
p = (await getState(page)).planes.find((x) => x.id === planeId)!;
|
||||
}
|
||||
if (p.locationIcao !== HUB) {
|
||||
// ISS-057: the landing above slowed the sim to 1× — setScale restores 1000× for the return leg.
|
||||
// ISS-082: dispatch auto-refuels, so a grounded plane can always start any full-tank-flyable
|
||||
// leg when cash allows — no top-up retry needed.
|
||||
// Make sure the world rate is back at SCALE for the return leg. ISS-082: dispatch auto-refuels,
|
||||
// so a grounded plane can always start any full-tank-flyable leg when cash allows — no top-up retry needed.
|
||||
await setScale(page, SCALE);
|
||||
const flyHome = await page.request.post(`/api/planes/${planeId}/fly`, { data: { destIcao: HUB } });
|
||||
expect(flyHome.status()).toBe(200);
|
||||
@@ -295,8 +286,7 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await sellNonStarters(page); // a failure mid-test may have left the second plane behind (even in flight)
|
||||
const res = await postSim(page, { timeScale: 1, paused: false });
|
||||
expect(res.status()).toBe(200);
|
||||
await setScale(page, WORLD);
|
||||
});
|
||||
|
||||
test('two grounded planes: Load is one-click with a preselected plane; the picker routes to the other', async ({ page }) => {
|
||||
@@ -348,7 +338,7 @@ test('two grounded planes: Load is one-click with a preselected plane; the picke
|
||||
await expect(page.getByTestId(`plane-${secondId}`), 'the second plane row renders').toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test('two planes fly in parallel; the last landing slows to 1× and the next dispatch resumes', async ({ page }) => {
|
||||
test('two planes fly in parallel and both deliver their packages', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal + one long leg each way at scale 1000
|
||||
const secondId = await buySecondPlaneViaUi(page);
|
||||
|
||||
@@ -358,7 +348,7 @@ test('two planes fly in parallel; the last landing slows to 1× and the next dis
|
||||
|
||||
// Two very different leg lengths: the starter takes the SHORTEST hop, the second plane the longest
|
||||
// full-tank-flyable one — their landings are separated by hours of game time and can never fall on
|
||||
// the same tick (ISS-057 slows to 1× on ANY landing, so a shared-tick arrival would race the asserts).
|
||||
// the same tick (a shared-tick arrival would race the first-landing assert).
|
||||
const [nearest] = await twoNearestToHub(page);
|
||||
const farthest = await farthestFlyableToHub(page);
|
||||
|
||||
@@ -388,8 +378,6 @@ test('two planes fly in parallel; the last landing slows to 1× and the next dis
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
// …restore speed (that landing slowed the sim to 1× — ISS-057) so the long leg fast-forwards too.
|
||||
await setScale(page, SCALE);
|
||||
await expect
|
||||
.poll(async () => ((await getState(page)).planes.every((p) => p.status === 'available')), {
|
||||
timeout: 240_000,
|
||||
@@ -397,10 +385,7 @@ test('two planes fly in parallel; the last landing slows to 1× and the next dis
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
// ISS-057: the LAST landing left a grounded plane → the sim slowed to 1× WITHOUT pausing.
|
||||
const landed = await getState(page);
|
||||
expect(landed.simState.paused).toBe(false);
|
||||
expect(landed.simState.timeScale).toBe(1);
|
||||
// Both packages delivered on arrival (in deadline — 24h window vs hours of flight time).
|
||||
expect(landed.packages.find((p) => p.id === pkgA.id)?.status).toBe('delivered');
|
||||
expect(landed.packages.find((p) => p.id === pkgB.id)?.status).toBe('delivered');
|
||||
@@ -408,17 +393,9 @@ test('two planes fly in parallel; the last landing slows to 1× and the next dis
|
||||
// The outbound to the range edge left the tank nearly empty — top it back up (ISS-082 hook).
|
||||
await setFuelFull(page, secondId);
|
||||
|
||||
// Resume semantics: with the starter still grounded, dispatching the second plane does NOT resume…
|
||||
const flyB2 = await page.request.post(`/api/planes/${secondId}/fly`, { data: { destIcao: HUB } });
|
||||
expect(flyB2.status()).toBe(200);
|
||||
let body = (await flyB2.json()) as StateBody;
|
||||
expect(body.simState.timeScale, 'another plane still grounded → stays at 1×').toBe(1);
|
||||
|
||||
// …but dispatching the LAST grounded plane restores the last manually selected speed.
|
||||
const flyA2 = await page.request.post(`/api/planes/${STARTER}/fly`, { data: { destIcao: HUB } });
|
||||
expect(flyA2.status()).toBe(200);
|
||||
body = (await flyA2.json()) as StateBody;
|
||||
expect(body.simState.timeScale, 'the last grounded plane resumes lastSelectedTimeScale').toBe(SCALE); // setScale posted 1000
|
||||
// Cleanup: fly both home so the live instance stays in its onboarding state for later specs.
|
||||
expect((await page.request.post(`/api/planes/${secondId}/fly`, { data: { destIcao: HUB } })).status()).toBe(200);
|
||||
expect((await page.request.post(`/api/planes/${STARTER}/fly`, { data: { destIcao: HUB } })).status()).toBe(200);
|
||||
});
|
||||
|
||||
// ISS-082 (v0.42.0): the "refuel section lists both planes" test is gone with the manual refuel UI —
|
||||
@@ -503,9 +480,8 @@ test('per-plane queues are independent; landing auto-chains the next queued leg'
|
||||
const flyBody = (await flyRes.json()) as StateBody;
|
||||
expect(flyBody.planes.find((x) => x.id === STARTER)!.queue, 'queue[0] consumed on dispatch').toEqual([b.icao]);
|
||||
|
||||
// …fast-forward; on landing at a the server AUTO-CHAINS queue[0] (b) in the same tick — but with
|
||||
// the second plane still grounded, ISS-057 slows the sim to 1× on that landing. Wait for the chain…
|
||||
await setScale(page, SCALE);
|
||||
// …the outbound leg flies at the fixed world rate (24×) — slow enough that polls can observe the
|
||||
// landing and the server's AUTO-CHAIN of queue[0] (b) in the same tick. Wait for the chain…
|
||||
const seen: Array<{ locationIcao: string; destIcao: string | null }> = [];
|
||||
await expect
|
||||
.poll(async () => {
|
||||
@@ -519,7 +495,7 @@ test('per-plane queues are independent; landing auto-chains the next queued leg'
|
||||
// is either the outbound leg (HUB→a) or the chained leg (a→b).
|
||||
expect(seen.filter((s) => s.locationIcao === a.icao && s.destIcao === a.icao), 'phantom self-hop a → a').toEqual([]);
|
||||
|
||||
// …then restore speed so the chained leg fast-forwards too.
|
||||
// …fast-forward the chained leg to its landing.
|
||||
await setScale(page, SCALE);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
|
||||
Reference in New Issue
Block a user