116 lines
4.8 KiB
TypeScript
116 lines
4.8 KiB
TypeScript
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.
|
||
});
|
||
});
|