Files
Flight-Ops/lib/game/state.test.ts
T
2026-09-26 19:38:25 +00:00

116 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import * as schema from '@/lib/db/schema';
import { getGameDb } from '@/lib/db/client';
import { PLANE_TYPES } from './catalog';
import { STARTING_CASH, STARTING_REPUTATION } from './seed';
import { getStateSnapshot, SSE_PACKAGE_STATUSES, sseFrame, type PackageState, type StateSnapshot } from './state';
// ISS-133 (v0.59.0) — the SSE projection keeps only the package statuses any component renders.
const pkg = (status: PackageState['status'], id: string): PackageState => ({
id,
originIcao: 'LEMD',
originName: 'Madrid–Barajas',
destIcao: 'EHAM',
destName: 'Amsterdam Schiphol',
reward: 1000,
cargoType: 'mail',
cargoCount: 3,
weightKg: 900,
special: null,
expiresRealMs: status === 'open' ? 2_000_000 : null, // v0.65.0 — open jobs carry their wall-clock expiry
distanceKm: 2000,
deadlineSimTime: 1_000_000,
status,
planeId: null,
createdSimTime: 0,
carrierName: null, // v1.4.0 (ISS-112) — unassigned by default in fixtures
});
const snapshot = (packages: PackageState[]): StateSnapshot => ({
account: { cash: 20_000, reputation: 1 },
simState: { clockMs: 42 },
catalog: [],
fuelPricePerL: 0.8,
sellFraction: 0.5,
planes: [],
players: [], // v1.6.0 (ISS-115) — leaderboard rows; empty in these sseFrame fixtures
packages,
});
describe('sseFrame', () => {
it('keeps exactly the open/loaded/expired packages and drops terminal history', () => {
const all = [
pkg('open', 'p-open'),
pkg('delivered', 'p-delivered'),
pkg('loaded', 'p-loaded'),
pkg('abandoned', 'p-abandoned'),
pkg('expired', 'p-expired'),
];
const frame = sseFrame(snapshot(all));
expect(frame.packages.map((p) => p.id)).toEqual(['p-open', 'p-loaded', 'p-expired']);
});
it('passes the rest of the snapshot through untouched and does not mutate its input', () => {
const all = [pkg('open', 'a'), pkg('delivered', 'b')];
const snap = snapshot(all);
const frame = sseFrame(snap);
expect(frame.account).toEqual({ cash: 20_000, reputation: 1 });
expect(frame.simState).toEqual({ clockMs: 42 });
expect(frame.planes).toEqual([]);
expect(all.map((p) => p.status)).toContain('delivered'); // input intact
});
it('SSE_PACKAGE_STATUSES matches what the client renders (open market, on-board cargo)', () => {
expect([...SSE_PACKAGE_STATUSES].sort()).toEqual(['expired', 'loaded', 'open']);
});
});
// v1.6.0 (ISS-115) — the snapshot's players array: one row per player with cash/reputation and
// how many planes they own. Runs against a throwaway seeded DB via the app singleton, exactly
// like the /api/state route tests (fresh module registry per file ⇒ fresh singleton).
describe('getStateSnapshot players', () => {
let dir: string;
beforeAll(async () => {
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'flight-snapshot-players-'));
process.env.FLIGHT_DB_PATH = path.join(dir, 'players.db');
await getGameDb(); // seeds the local player + starter plane
});
afterAll(async () => {
delete process.env.FLIGHT_DB_PATH;
await fs.rm(dir, { recursive: true, force: true });
});
it('single-player regression guard — one row for the seeded local account', async () => {
const snap = await getStateSnapshot('local');
expect(snap.players).toEqual([
{ id: 'local', username: 'kamilkrzywda', cash: STARTING_CASH, reputation: STARTING_REPUTATION, planeCount: 1 },
]);
});
it('two players — cash/reputation/planeCount per owner (v1.6.0, ISS-115)', async () => {
const inst = await getGameDb();
// A second account with a distinct economy and two planes of its own — rows minted like /api/register does.
await inst.db.insert(schema.players).values({ id: 'b', provider: 'local', username: 'player-b', cash: 5_000, reputation: 3 });
const type = PLANE_TYPES.atr72;
for (const id of ['plane-b-1', 'plane-b-2']) {
await inst.db.insert(schema.planes).values({
id, ownerId: 'b', model: 'atr72', status: 'available',
fuel: type.maxFuel, maxFuel: type.maxFuel, locationIcao: 'EDDF', lat: 50.03, lon: 8.56,
});
}
const snap = await getStateSnapshot('b'); // any session sees the whole world's players
expect(snap.players).toHaveLength(2);
const local = snap.players.find((p) => p.id === 'local')!;
expect(local).toEqual({ id: 'local', username: 'kamilkrzywda', cash: STARTING_CASH, reputation: STARTING_REPUTATION, planeCount: 1 }); // starter still counted once…
const b = snap.players.find((p) => p.id === 'b')!;
expect(b).toEqual({ id: 'b', username: 'player-b', cash: 5_000, reputation: 3, planeCount: 2 }); // …and both of B's planes count for B.
});
});