v1.7.0: multiplayer test coverage — player-scoped fixtures + N-player clock verification (ISS-116, ISS-113)
CI / build + unit + e2e (pull_request) Successful in 13m33s
CI / build + unit + e2e (pull_request) Successful in 13m33s
This commit is contained in:
@@ -742,6 +742,32 @@ describe('grounded fleet freezes the world clock (#135)', () => {
|
||||
expect(sim.clockMs).toBeGreaterThan(STARTING_CLOCK_MS); // the repair gate is what keeps time moving
|
||||
});
|
||||
|
||||
it('N-player fleet: one owner airborne keeps the clock running; all owners grounded freezes it (ISS-113)', async () => {
|
||||
const inst = await freshDb('nplayer-clock.db');
|
||||
// A second player with their own GROUNDED plane — the world now has two owners.
|
||||
await inst.db.insert(schema.players).values({ id: 'player-b', provider: 'test', username: 'b', cash: 1_000, reputation: STARTING_REPUTATION });
|
||||
await inst.db.insert(schema.planes).values({
|
||||
id: 'plane-b', ownerId: 'player-b', model: 'atr72', status: 'available', fuel: 100, maxFuel: PLANE_TYPES.atr72.maxFuel,
|
||||
locationIcao: 'EDDF', destIcao: null, lat: 50.03, lon: 8.57, departureSimTime: null,
|
||||
});
|
||||
|
||||
// A (local) takes off while B stays grounded → the world advances — one owner flying is enough…
|
||||
await makeWorldActive(inst);
|
||||
vi.setSystemTime(T0 + 1_000);
|
||||
const sim = await tickOnce(inst);
|
||||
expect(sim.clockMs).toBe(STARTING_CLOCK_MS + 1_000 * WORLD_TIME_SCALE); // …at the fixed world rate
|
||||
|
||||
// …and grounding A's plane (B was already grounded) freezes the whole world.
|
||||
await inst.db.update(schema.planes)
|
||||
.set({ status: 'available', destIcao: null, departureSimTime: null })
|
||||
.where(eq(schema.planes.id, 'plane-starter'));
|
||||
vi.setSystemTime(T0 + 2_000); // a full second of real idleness with every owner grounded
|
||||
const frozen = await tickOnce(inst);
|
||||
|
||||
expect(frozen.clockMs).toBe(STARTING_CLOCK_MS + 1_000 * WORLD_TIME_SCALE); // frozen — no advance…
|
||||
expect(frozen.lastRealTickMs).toBe(T0 + 2_000); // …but the real-time anchor keeps tracking
|
||||
});
|
||||
|
||||
it('a frozen tick still refills the pool so a grounded fleet never starves (#135 follow-up)', async () => {
|
||||
const inst = await freshDb('frozen-topup.db'); // seeded starter is on the ground → frozen
|
||||
// Drain the open market the way wall-clock expiry would (removal paths run while frozen).
|
||||
@@ -944,6 +970,24 @@ describe('spawnPoolPackages (v0.65.0 per-airport pools)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('MAX picks the local row when local leads — symmetric to the player-b case (v1.5.0)', async () => {
|
||||
const inst = await freshDb('pool-shared-rep-local.db');
|
||||
// Local is far ahead of b's starting rep — a "prefer non-local" implementation would pick b and fail here.
|
||||
await updateLocalPlayerEconomy(inst, { reputation: 60 });
|
||||
await inst.db.insert(schema.players).values({ id: 'player-b', provider: 'test', username: 'b', cash: 1_000, reputation: STARTING_REPUTATION });
|
||||
for (const p of await inst.db.select().from(schema.packages)) {
|
||||
await inst.db.delete(schema.packages).where(eq(schema.packages.id, p.id));
|
||||
}
|
||||
|
||||
expect(await spawnPoolPackages(inst, T0 + 5_000, mulberry32(7), T0)).toBe(POOL_TARGET_PER_AIRPORT);
|
||||
|
||||
const openRows = (await inst.db.select().from(schema.packages)).filter((p) => p.status === 'open');
|
||||
for (const p of openRows) {
|
||||
expect(p.reward).toBe(await expectedRewardAt(p, 60)); // every job scaled to the MAX…
|
||||
expect(p.reward).not.toBe(await expectedRewardAt(p, STARTING_REPUTATION)); // …not to b's starting rep
|
||||
}
|
||||
});
|
||||
|
||||
it("single player: spawn quality is that player's own reputation (v1.5.0 regression guard)", async () => {
|
||||
const inst = await freshDb('pool-single-rep.db');
|
||||
// Raise the only player's rep so the bonus is non-trivial, then respawn from zero with a fixed RNG —
|
||||
|
||||
Reference in New Issue
Block a user