Slow sim to 1× on landing and resume last selected speed (ISS-057, v0.30.0)
CI / build + unit + e2e (pull_request) Successful in 8m57s

This commit is contained in:
2026-09-20 20:27:59 +00:00
parent 5e7ec903d3
commit c9e9113a45
13 changed files with 151 additions and 73 deletions
+27 -23
View File
@@ -301,12 +301,13 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
});
});
describe('pause on landing when the last plane lands (ISS-041)', () => {
describe('slow to 1× when a landing leaves any plane on the ground (ISS-057)', () => {
const FROM = 'EDDF';
const TO = 'LEMD';
// Starter flying EDDF→LEMD with departureSimTime set so its arrival is EXACTLY T0 —
// any advancing tick past T0 finalizes it. timeScale is the value under test.
// any advancing tick past T0 finalizes it. timeScale is the value under test;
// lastSelectedTimeScale mirrors what a manual POST /api/sim would have stored.
async function setupLanding(name: string, timeScale: number) {
const inst = await freshDb(name);
const fc = await loadAirportsGeoJson();
@@ -315,7 +316,7 @@ describe('pause on landing when the last plane lands (ISS-041)', () => {
const km = haversineKm(from.lat, from.lon, to.lat, to.lon);
const type = PLANE_TYPES.atr72;
await inst.db.update(schema.simState).set({ timeScale });
await inst.db.update(schema.simState).set({ timeScale, lastSelectedTimeScale: timeScale });
await inst.db.update(schema.planes)
.set({ status: 'flying', destIcao: TO, departureSimTime: T0 - travelMs(km, type.speedKmh), fuel: type.maxFuel })
.where(eq(schema.planes.id, 'plane-starter'));
@@ -323,20 +324,21 @@ describe('pause on landing when the last plane lands (ISS-041)', () => {
return { inst, from, km, type };
}
it('last plane lands → sim pauses and stored timeScale is UNCHANGED', async () => {
const { inst } = await setupLanding('pause-last.db', 1000);
it('last plane lands → sim slows to 1× WITHOUT pausing; lastSelectedTimeScale is preserved', async () => {
const { inst } = await setupLanding('slow-last.db', 1000);
vi.setSystemTime(T0 + 2_000); // one full clamped tick at scale 1000 → clock T0+2e6 ≥ arrival (T0)
const sim = await tickOnce(inst);
expect(sim.paused).toBe(true); // persisted on the sim_state row…
expect((await inst.db.select().from(schema.simState))[0].paused).toBe(true); // …and visible to the next snapshot
expect(sim.timeScale).toBe(1000); // speed preserved — no auto-slow anymore (ISS-041)
expect(sim.paused).toBe(false); // no pause anymore — ISS-057 replaces it with a slow-down…
expect((await inst.db.select().from(schema.simState))[0].paused).toBe(false); // …persisted on the row…
expect(sim.timeScale).toBe(1); // …slowed to the floor…
expect(sim.lastSelectedTimeScale).toBe(1000); // …with the last manual speed kept for resume
expect((await inst.db.select().from(schema.planes))[0].status).toBe('available'); // landing itself unchanged
});
it('a landing while another plane is still flying → no pause', async () => {
const { inst, from, type } = await setupLanding('pause-other.db', 1000);
it('a landing while another plane is still flying → slows to 1× (any plane on ground)', async () => {
const { inst, from, type } = await setupLanding('slow-other.db', 1000);
// A second plane on the same route departing at T0 — arrives far after this tick's clock.
await inst.db.insert(schema.planes).values({
id: 'p-b', model: 'atr72', status: 'flying', fuel: type.maxFuel, maxFuel: type.maxFuel,
@@ -346,26 +348,26 @@ describe('pause on landing when the last plane lands (ISS-041)', () => {
vi.setSystemTime(T0 + 2_000);
const sim = await tickOnce(inst);
expect(sim.paused).toBe(false); // p-b still in flight — no pause (nothing would auto-unpause it)
expect(sim.timeScale).toBe(1000);
expect(sim.paused).toBe(false);
expect(sim.timeScale).toBe(1); // the just-landed starter is on ground → slow (literal issue reading)
const byId = new Map((await inst.db.select().from(schema.planes)).map((p) => [p.id, p.status]));
expect(byId.get('plane-starter')).toBe('available'); // the starter did land…
expect(byId.get('p-b')).toBe('flying'); // …but the fleet is not idle
expect(byId.get('p-b')).toBe('flying'); // …but the fleet is not idle — still slowed
});
it('last plane lands at scale 1 → pauses anyway, scale untouched', async () => {
const { inst } = await setupLanding('pause-one.db', 1);
it('already at 1× → stays 1 (no-op slow)', async () => {
const { inst } = await setupLanding('slow-one.db', 1);
vi.setSystemTime(T0 + 2_000); // one tick at scale 1 advances 2e3 game ms — past the T0 arrival
const sim = await tickOnce(inst);
expect(sim.paused).toBe(true);
expect(sim.paused).toBe(false);
expect(sim.timeScale).toBe(1);
expect((await inst.db.select().from(schema.planes))[0].status).toBe('available');
});
it('a chained plane does NOT trigger the last-plane pause (ISS-043)', async () => {
const { inst } = await setupLanding('pause-chain.db', 1000); // full tank, arrival exactly T0
it('a chained plane does NOT slow the sim (nothing is grounded)', async () => {
const { inst } = await setupLanding('slow-chain.db', 1000); // full tank, arrival exactly T0
// A queued next hop with plenty of fuel → chains on landing instead of grounding.
await inst.db.update(schema.planes)
.set({ queue: JSON.stringify(['EDDV']) })
@@ -374,23 +376,25 @@ describe('pause on landing when the last plane lands (ISS-041)', () => {
vi.setSystemTime(T0 + 2_000); // one full clamped tick at scale 1000 → clock T0+2e6 ≥ arrival (T0)
const sim = await tickOnce(inst);
expect(sim.paused).toBe(false); // still flying after the chain — no premature pause…
expect(sim.paused).toBe(false);
expect(sim.timeScale).toBe(1000); // still flying after the chain, nothing grounded — no slow…
const plane = (await inst.db.select().from(schema.planes))[0];
expect(plane.status).toBe('flying'); // …the fresh select in tickOnce counts it as airborne
expect(plane.destIcao).toBe('EDDV');
});
it('paused sim with all planes landed → no change', async () => {
const inst = await freshDb('pause-paused.db');
it('manually-paused sim is untouched by ticks (no landing processed while paused)', async () => {
const inst = await freshDb('slow-paused.db');
// The fleet is fully grounded (the seeded starter) — a paused tick must not touch the
// controls even though "no plane in flight" holds: the trigger requires a landing this tick.
await inst.db.update(schema.simState).set({ timeScale: 1000, paused: true });
// controls even though "any plane on ground" holds: the trigger requires a landing this tick.
await inst.db.update(schema.simState).set({ timeScale: 1000, lastSelectedTimeScale: 1000, paused: true });
vi.setSystemTime(T0 + 5_000);
const sim = await tickOnce(inst);
expect(sim.paused).toBe(true); // no landing processed while paused → trigger never fires
expect(sim.timeScale).toBe(1000);
expect(sim.lastSelectedTimeScale).toBe(1000);
expect((await inst.db.select().from(schema.planes))[0].status).toBe('available');
});
});