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. }); });