v0.62.0: fixed 24x world clock — frozen when fully grounded (#135)
CI / build + unit + e2e (pull_request) Successful in 14m3s

This commit is contained in:
2026-09-23 08:09:29 +00:00
parent e970f95036
commit bb4e3b96f3
14 changed files with 122 additions and 124 deletions
+9 -21
View File
@@ -1,15 +1,14 @@
import { test, expect, type Page } from '@playwright/test';
// v0.52.0 (ISS-117) fixed world clock against the live origin: a fully grounded fleet slowing to
// real time instead of freezing (ISS-128), background-tick advance at a raised rate while
// something is active, package expiry via the FLIGHT_TEST_HOOKS hooks (enabled in docker-compose.yml),
// and market top-up.
// v0.52.0 (ISS-117), finalized in #135: fixed world clock against the live origin — a fully
// grounded fleet freezes the clock, background-tick advance at a raised rate while something is
// active, package expiry via the FLIGHT_TEST_HOOKS hooks (enabled in docker-compose.yml), and
// market top-up.
// Must match lib/sim/engine.ts — kept in sync manually (same pattern as MIN_ZOOM/MAX_ZOOM in map.spec.ts).
const MAX_TICK_ELAPSED_MS = 2_000;
const SCALE = 1000; // test rate for fast-forwarding legs
const WORLD = 24; // the fixed world rate (ISS-117) — must match lib/worldClock.ts
const IDLE = 1; // the idle real-time rate (ISS-128) — must match IDLE_WORLD_TIME_SCALE in lib/worldClock.ts (= WORLD / 24)
const MAX_DELTA_PER_TICK = MAX_TICK_ELAPSED_MS * SCALE; // 2_000_000 game ms per tick at scale 1000
// Must match MARKET_TARGET_OPEN_PACKAGES in lib/game/seed.ts.
const MARKET_TARGET = 10;
@@ -48,10 +47,9 @@ async function setHealth(page: Page, planeId: string, fields: Record<string, unk
expect(res.status()).toBe(200);
}
// A fully grounded fleet only advances at the idle real-time rate (ISS-128) — put the starter in
// repair so the clock runs at FULL scale for tests that need fast-forwarding. The €1.5k repair
// cost is irrelevant to every spec's relative cash assertions; deactivateWorld restores full health
// and no gate afterwards.
// A fully grounded fleet freezes the world clock (#135) — put the starter in repair so the clock
// runs at FULL scale for tests that need fast-forwarding. The €1.5k repair cost is irrelevant to
// every spec's relative cash assertions; deactivateWorld restores full health and no gate afterwards.
async function activateWorld(page: Page): Promise<void> {
await setHealth(page, STARTER, { health: 50 });
expect((await page.request.post(`/api/planes/${STARTER}/repair`)).status()).toBe(200);
@@ -73,25 +71,15 @@ test.afterEach(async ({ page }) => {
await setScale(page, WORLD);
});
test('a fully grounded fleet slows the clock to real time instead of freezing it (ISS-128)', async ({ page }) => {
test('a fully grounded fleet freezes the world clock (#135)', async ({ page }) => {
const s = await getState(page);
expect(s.planes.every((p) => p.status === 'available'), 'fleet should be grounded at spec start').toBe(true);
const t0 = Date.now();
const c0 = s.simState.clockMs;
await sleep(2_500); // ~2.5 s of real idleness — several background ticks plus the reads below
const after = await getState(page);
const elapsedReal = Date.now() - t0;
const delta = after.simState.clockMs - c0;
expect(delta, 'the idle clock must advance (no longer frozen)').toBeGreaterThan(0);
// ≈ real time: each tick counts min(gap, MAX_TICK_ELAPSED_MS) × IDLE rate, so the total can only fall
// short of the wall span when a tick gap was clamped — worst case is one clamped idle-rate tick.
expect(delta).toBeGreaterThanOrEqual(Math.min(elapsedReal * IDLE * 0.5, MAX_TICK_ELAPSED_MS * IDLE));
// …and never more than the wall span itself (plus one clamp of pre-read downtime for slow boxes)…
expect(delta).toBeLessThanOrEqual(elapsedReal + MAX_TICK_ELAPSED_MS);
// …which is clearly below the full world rate: at ×24 this window would be ≥ ~60 game seconds.
expect(delta).toBeLessThan(elapsedReal * WORLD);
expect(after.simState.clockMs - c0, 'the grounded clock must stay frozen').toBe(0);
});
test('timeScale 1000 advances the clock by roughly real-time × 1000 between polls', async ({ page }) => {