CI / build + unit + e2e (pull_request) Failing after 12m10s
The world always runs at WORLD_TIME_SCALE = 24 (lib/worldClock.ts). A server
background tick (instrumentation.ts -> startBackgroundTick, 1 s interval)
advances the sim with no open browser; idle-pause freezes the clock exactly
while no plane is flying or in repair. Fresh worlds start year 2000
(STARTING_CLOCK_MS in seed.ts).
- Removed: POST /api/sim + OpenAPI entry, applyTimeControls/clampTimeScale,
lib/timeScale.ts, LeftPane pause button + scale slider machinery,
ISS-057 slow-on-landing/resume in flyPlane.
- Snapshot simState is now { clockMs } only; client extrapolates at the fixed
rate between polls and freezes while idle (useSimClock).
- New test-only hook POST /api/test/set-timescale behind FLIGHT_TEST_HOOKS=1
(sibling of set-health); all e2e setScale helpers call it, afterEach restores
the world rate. E2E pause tests replaced by idle-pause assertions; sim.spec.ts
activates the world via set-health + /repair for advance/expiry tests.
111 lines
4.5 KiB
TypeScript
111 lines
4.5 KiB
TypeScript
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { eq } from 'drizzle-orm';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import * as schema from '@/lib/db/schema';
|
|
import { getGameDb } from '@/lib/db/client';
|
|
import { POST } from './route';
|
|
|
|
let dir: string;
|
|
|
|
beforeAll(async () => {
|
|
// Same pattern as app/api/state/route.test.ts: point the DB singleton at a
|
|
// throwaway file (fresh module registry per test file → fresh singleton).
|
|
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'flight-fly-api-'));
|
|
process.env.FLIGHT_DB_PATH = path.join(dir, 'fly.db');
|
|
});
|
|
|
|
afterAll(async () => {
|
|
delete process.env.FLIGHT_DB_PATH;
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
const post = (planeId: string, body: unknown) =>
|
|
POST(new Request(`http://localhost/api/planes/${planeId}/fly`, { method: 'POST', body: JSON.stringify(body) }), {
|
|
params: Promise.resolve({ planeId }),
|
|
});
|
|
|
|
describe('POST /api/planes/:planeId/fly', () => {
|
|
it('rejects a missing/non-string destIcao and unknown fields with 400', async () => {
|
|
for (const body of [{}, { destIcao: 42 }, { bogus: 1 }]) {
|
|
const res = await post('plane-starter', body);
|
|
expect(res.status).toBe(400);
|
|
expect(((await res.json()) as { error: string }).error).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
it('rejects an ICAO not in the airport dataset with 400', async () => {
|
|
const res = await post('plane-starter', { destIcao: 'ZZZZ' });
|
|
expect(res.status).toBe(400);
|
|
expect(((await res.json()) as { error: string }).error).toMatch(/unknown airport/);
|
|
});
|
|
|
|
it('rejects an unknown plane id with 404', async () => {
|
|
const res = await post('ghost-plane', { destIcao: 'LEMD' });
|
|
expect(res.status).toBe(404);
|
|
expect(((await res.json()) as { error: string }).error).toMatch(/not found/);
|
|
});
|
|
|
|
it('rejects a flying plane with 409', async () => {
|
|
const { db } = await getGameDb();
|
|
await db.update(schema.planes)
|
|
.set({ status: 'flying', departureSimTime: Date.now(), destIcao: 'LEMD' })
|
|
.where(eq(schema.planes.id, 'plane-starter'));
|
|
|
|
const res = await post('plane-starter', { destIcao: 'EDDM' });
|
|
expect(res.status).toBe(409);
|
|
expect(((await res.json()) as { error: string }).error).toMatch(/flying/);
|
|
|
|
// Ground it again for later tests.
|
|
await db.update(schema.planes)
|
|
.set({ status: 'available', departureSimTime: null, destIcao: null })
|
|
.where(eq(schema.planes.id, 'plane-starter'));
|
|
});
|
|
|
|
it('rejects a leg that does not fit even a full tank with 409 (ISS-082 backstop)', async () => {
|
|
const { db } = await getGameDb();
|
|
// UNNT → LPPD ≈ 7670 km needs ~10k L — beyond the ATR's 6300 L tank, so no auto-refuel can make it flyable.
|
|
await db.update(schema.planes).set({ locationIcao: 'UNNT' }).where(eq(schema.planes.id, 'plane-starter'));
|
|
|
|
const res = await post('plane-starter', { destIcao: 'LPPD' });
|
|
expect(res.status).toBe(409);
|
|
expect(((await res.json()) as { error: string }).error).toMatch(/insufficient fuel/i);
|
|
|
|
// Put the starter back at the hub for later tests.
|
|
await db.update(schema.planes).set({ locationIcao: 'EDDF' }).where(eq(schema.planes.id, 'plane-starter'));
|
|
});
|
|
|
|
it('flies the plane and returns the full snapshot with departure + destination set', async () => {
|
|
const res = await post('plane-starter', { destIcao: 'LEMD' });
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers.get('content-type')).toContain('application/json');
|
|
|
|
// Response convention (v0.8.0): the FULL snapshot, same shape as GET /api/state.
|
|
const body = (await res.json()) as {
|
|
account: { cash: number };
|
|
simState: { clockMs: number }; // ISS-117
|
|
planes: Array<{ id: string; status: string; destIcao: string | null; departureSimTime: number | null; locationIcao: string }>;
|
|
};
|
|
expect(Object.keys(body).sort()).toEqual([
|
|
'account',
|
|
'catalog',
|
|
'fuelPricePerL',
|
|
'packages',
|
|
'planes',
|
|
'sellFraction',
|
|
'simState',
|
|
]);
|
|
|
|
const plane = body.planes.find((p) => p.id === 'plane-starter')!;
|
|
expect(plane.status).toBe('flying');
|
|
expect(plane.destIcao).toBe('LEMD'); // destination set at fly time (v0.12.0)
|
|
expect(typeof plane.departureSimTime).toBe('number'); // departure set at fly time
|
|
expect(plane.locationIcao).toBe('EDDF'); // origin unchanged until arrival
|
|
|
|
// A second flight of the same (now flying) plane is rejected — it is no longer available.
|
|
const again = await post('plane-starter', { destIcao: 'EDDM' });
|
|
expect(again.status).toBe(409);
|
|
});
|
|
});
|