Files
Flight-Ops/e2e/state.spec.ts
T

83 lines
3.5 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 { execSync } from 'node:child_process';
import path from 'node:path';
import { test, expect } from '@playwright/test';
// Repo root — docker-compose.yml lives here; `docker compose restart` runs on
// this host (the test runner), browsers stay remote.
const repoRoot = path.join(__dirname, '..');
interface StateBody {
account: { cash: number; reputation: number };
simState: { clockMs: number; timeScale: number; paused: boolean };
planes: Array<Record<string, unknown>>;
packages: Array<Record<string, unknown>>;
}
test('GET /api/state returns a seeded snapshot', async ({ page }) => {
const res = await page.request.get('/api/state');
expect(res.status()).toBe(200);
expect(res.headers()['content-type']).toContain('application/json');
const body = (await res.json()) as StateBody;
expect(typeof body.account.cash).toBe('number');
expect(typeof body.account.reputation).toBe('number');
expect(typeof body.simState.clockMs).toBe('number');
expect(typeof body.simState.timeScale).toBe('number');
expect(body.simState.paused).toBe(false);
expect(Array.isArray(body.planes)).toBe(true);
expect(body.planes.length).toBeGreaterThanOrEqual(1);
for (const key of ['id', 'model', 'status', 'fuel', 'maxFuel', 'locationIcao', 'locationName', 'lat', 'lon']) {
expect(body.planes[0], `plane.${key}`).toHaveProperty(key);
}
expect(Array.isArray(body.packages)).toBe(true);
expect(body.packages.length).toBeGreaterThan(0);
for (const key of ['id', 'originIcao', 'destIcao', 'reward', 'cargoType', 'deadlineSimTime', 'status']) {
expect(body.packages[0], `package.${key}`).toHaveProperty(key);
}
});
test('state survives a container restart (sim clock + account persisted)', async ({ page }) => {
test.setTimeout(180_000);
const resBefore = await page.request.get('/api/state');
expect(resBefore.status()).toBe(200);
const before = (await resBefore.json()) as StateBody;
execSync('docker compose restart flight', { cwd: repoRoot, stdio: 'pipe' });
// Wait for the app to come back, capturing the post-restart snapshot.
let after: StateBody | undefined;
await expect
.poll(async () => {
try {
const res = await page.request.get('/api/state');
if (res.status() === 200) after = (await res.json()) as StateBody;
return !!after;
} catch {
return false; // connection refused / 502 while the container starts
}
}, { timeout: 120_000, message: 'waiting for /api/state after docker compose restart' })
.toBe(true);
expect(after).toBeDefined();
const a = after!;
// The DB file on the volume must be loaded, not re-seeded (PLAN.md: "sim clock
// persisted across sessions"): account and entity ids are byte-identical.
// v0.7.0 tick-on-read caveat: every GET advances the clock by at most one
// clamped tick (MAX_TICK_ELAPSED_MS × timeScale, see lib/sim/engine.ts), so the
// post-restart clock may be ahead — but never by more than that bound.
expect(a.simState.clockMs).toBeGreaterThanOrEqual(before.simState.clockMs);
const MAX_TICK_ELAPSED_MS = 2_000; // must match lib/sim/engine.ts
expect(a.simState.clockMs - before.simState.clockMs).toBeLessThanOrEqual(
MAX_TICK_ELAPSED_MS * a.simState.timeScale,
);
expect(a.account.cash).toBe(before.account.cash);
expect(a.account.reputation).toBe(before.account.reputation);
expect(a.planes.map((p) => p.id).sort()).toEqual(before.planes.map((p) => p.id).sort());
expect(a.packages.map((p) => p.id).sort()).toEqual(before.packages.map((p) => p.id).sort());
});