idle fleet slows world clock to real time instead of freezing (ISS-128)
CI / build + unit + e2e (pull_request) Successful in 10m32s

This commit is contained in:
2026-09-22 18:37:06 +00:00
parent 5268a39fe9
commit 0a40143497
12 changed files with 142 additions and 72 deletions
+26 -12
View File
@@ -1,13 +1,15 @@
import { test, expect, type Page } from '@playwright/test';
// v0.52.0 (ISS-117) fixed world clock against the live origin: idle-pause freezing a grounded
// fleet, 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) 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.
// 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;
@@ -46,9 +48,10 @@ async function setHealth(page: Page, planeId: string, fields: Record<string, unk
expect(res.status()).toBe(200);
}
// Idle-pause (ISS-117) freezes a fully grounded fleet — put the starter in repair so the clock
// advances for tests that need it. 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 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.
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);
@@ -70,14 +73,25 @@ test.afterEach(async ({ page }) => {
await setScale(page, WORLD);
});
test('a fully grounded fleet freezes the clock exactly across polls (idle-pause)', async ({ page }) => {
test('a fully grounded fleet slows the clock to real time instead of freezing it (ISS-128)', 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 frozen = s.simState.clockMs;
for (let i = 0; i < 3; i++) {
await sleep(500);
expect((await getState(page)).simState.clockMs, 'clock must not advance while the fleet is idle').toBe(frozen);
}
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);
});
test('timeScale 1000 advances the clock by roughly real-time × 1000 between polls', async ({ page }) => {