idle fleet slows world clock to real time instead of freezing (ISS-128)
CI / build + unit + e2e (pull_request) Successful in 10m32s
CI / build + unit + e2e (pull_request) Successful in 10m32s
This commit is contained in:
+18
-9
@@ -272,19 +272,28 @@ test('on-board list: fly-to-destination + abandon with penalty preview; flying p
|
||||
const onboardRow = page.getByTestId(`onboard-package-${pkg.id}`);
|
||||
await expect(onboardRow, 'loaded package appears in the on-board list').toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// The abandon button previews the exact penalty at the snapshot's clock (same formula as the server).
|
||||
// The abandon button previews the penalty at its snapshot's clock (same formula as the server).
|
||||
// The fleet is grounded here (the starter just loaded; workers:1 and every spec's cleanup leave no
|
||||
// other planes), so idle-pause (ISS-117) freezes the clock — verify it actually did, so every
|
||||
// snapshot (mine and the UI's last poll) shares one clock value instead of racing the 1 s poll loop.
|
||||
const frozenClock = (await getState(page)).simState.clockMs;
|
||||
// other planes), so the world runs at the idle real-time rate (ISS-128) — verify it actually did,
|
||||
// then bound the UI's staleness instead of racing a frozen clock.
|
||||
const c0 = (await getState(page)).simState.clockMs;
|
||||
const t0 = Date.now();
|
||||
await new Promise((r) => setTimeout(r, 1_500)); // > one background-tick interval
|
||||
expect(
|
||||
(await getState(page)).simState.clockMs,
|
||||
'world should be frozen while the fleet is idle (ISS-117)',
|
||||
).toBe(frozenClock);
|
||||
const state = await getState(page);
|
||||
const idleDelta = state.simState.clockMs - c0;
|
||||
expect(idleDelta, 'world should advance at the idle real-time rate while grounded (ISS-128)').toBeGreaterThan(0);
|
||||
expect(idleDelta).toBeLessThan((Date.now() - t0) * WORLD); // clearly below the full world rate
|
||||
|
||||
// The UI renders the penalty from its last snapshot — up to one poll interval (~1 s, useGameState)
|
||||
// older than `state`, i.e. ≤ ~2 game-seconds of clock drift at the idle real-time rate (ISS-128).
|
||||
// Allow the formula's slope over that window instead of an exact string match.
|
||||
const DRIFT_MS = 3_000; // a couple of poll intervals at the idle rate (= 1), margin for slow CI boxes
|
||||
const span = pkg.deadlineSimTime - pkg.createdSimTime;
|
||||
const penaltyText = ((await page.getByTestId(`abandon-penalty-${pkg.id}`).textContent()) ?? '').replace(/[^0-9.]/g, '');
|
||||
expect(Number.isFinite(Number(penaltyText)), `penalty text: ${penaltyText}`).toBe(true);
|
||||
const expectedPenalty = penaltyFor(pkg.reward, pkg.createdSimTime, pkg.deadlineSimTime, state.simState.clockMs);
|
||||
await expect(page.getByTestId(`abandon-penalty-${pkg.id}`)).toContainText(eur(expectedPenalty));
|
||||
const slopePerGameMs = (pkg.reward * 0.45) / span; // abandonPenaltyPct's SPAN across the package lifetime
|
||||
expect(Math.abs(Number(penaltyText) - expectedPenalty)).toBeLessThanOrEqual(slopePerGameMs * DRIFT_MS + 0.01);
|
||||
|
||||
// Fly to the destination through the UI — the button is the destination's name.
|
||||
const flyBtn = page.getByTestId(`fly-to-${pkg.id}`);
|
||||
|
||||
Reference in New Issue
Block a user