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.
122 lines
5.0 KiB
TypeScript
122 lines
5.0 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-load-api-'));
|
|
process.env.FLIGHT_DB_PATH = path.join(dir, 'load.db');
|
|
});
|
|
|
|
afterAll(async () => {
|
|
delete process.env.FLIGHT_DB_PATH;
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
const post = (id: string, body: unknown) =>
|
|
POST(new Request(`http://localhost/api/packages/${id}/load`, { method: 'POST', body: JSON.stringify(body) }), {
|
|
params: Promise.resolve({ id }),
|
|
});
|
|
|
|
describe('POST /api/packages/:id/load', () => {
|
|
it('rejects a missing/non-string planeId and unknown fields with 400', async () => {
|
|
for (const body of [{}, { planeId: 42 }, { bogus: 1 }]) {
|
|
const res = await post('p-x', body);
|
|
expect(res.status).toBe(400);
|
|
expect(((await res.json()) as { error: string }).error).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
it('rejects an unknown package id with 404', async () => {
|
|
const res = await post('nope', { planeId: 'plane-starter' });
|
|
expect(res.status).toBe(404);
|
|
expect(((await res.json()) as { error: string }).error).toMatch(/not found/);
|
|
});
|
|
|
|
it('rejects an unknown plane id with 404', async () => {
|
|
const { db } = await getGameDb();
|
|
await db.insert(schema.packages).values({
|
|
id: 'p-ghost-plane', originIcao: 'EDDF', destIcao: 'LEMD', reward: 1, cargoType: 'Mail', cargoCount: 1,
|
|
deadlineSimTime: Date.now() + 3_600_000, status: 'open', planeId: null, createdSimTime: Date.now(),
|
|
});
|
|
|
|
const res = await post('p-ghost-plane', { planeId: 'ghost' });
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it('rejects loading a package whose origin the plane is not at with 409', async () => {
|
|
const { db } = await getGameDb();
|
|
// The seeded starter sits at EDDF — a package from LEMD cannot be loaded there.
|
|
await db.insert(schema.packages).values({
|
|
id: 'p-elsewhere', originIcao: 'LEMD', destIcao: 'EDDF', reward: 1, cargoType: 'Mail', cargoCount: 1,
|
|
deadlineSimTime: Date.now() + 3_600_000, status: 'open', planeId: null, createdSimTime: Date.now(),
|
|
});
|
|
|
|
const res = await post('p-elsewhere', { planeId: 'plane-starter' });
|
|
expect(res.status).toBe(409);
|
|
expect(((await res.json()) as { error: string }).error).toMatch(/at EDDF/);
|
|
});
|
|
|
|
it('rejects loading a non-open package with 409', async () => {
|
|
const { db } = await getGameDb();
|
|
await db.insert(schema.packages).values({
|
|
id: 'p-busy', originIcao: 'EDDF', destIcao: 'LEMD', reward: 1, cargoType: 'Mail', cargoCount: 1,
|
|
deadlineSimTime: Date.now() + 3_600_000, status: 'loaded', planeId: 'plane-starter', createdSimTime: Date.now(),
|
|
});
|
|
|
|
const res = await post('p-busy', { planeId: 'plane-starter' });
|
|
expect(res.status).toBe(409);
|
|
expect(((await res.json()) as { error: string }).error).toMatch(/loaded/);
|
|
});
|
|
|
|
it('loads an open package at the plane\'s airport and returns the full snapshot', async () => {
|
|
const { db } = await getGameDb();
|
|
// A known open package originating where the starter actually sits (EDDF).
|
|
await db.insert(schema.packages).values({
|
|
id: 'p-home', originIcao: 'EDDF', destIcao: 'LEMD', reward: 1_000, cargoType: 'Mail', cargoCount: 4,
|
|
deadlineSimTime: Date.now() + 24 * 3_600_000, status: 'open', planeId: null, createdSimTime: Date.now(),
|
|
});
|
|
|
|
const res = await post('p-home', { planeId: 'plane-starter' });
|
|
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<Record<string, unknown>>;
|
|
packages: Array<{ id: string; status: string; planeId: string | null }>;
|
|
};
|
|
expect(Object.keys(body).sort()).toEqual([
|
|
'account',
|
|
'catalog',
|
|
'fuelPricePerL',
|
|
'packages',
|
|
'planes',
|
|
'sellFraction',
|
|
'simState',
|
|
]);
|
|
const loaded = body.packages.find((p) => p.id === 'p-home')!;
|
|
expect(loaded.status).toBe('loaded');
|
|
expect(loaded.planeId).toBe('plane-starter');
|
|
|
|
// Persisted, not just in the response.
|
|
const row = (await db.select().from(schema.packages).where(eq(schema.packages.id, 'p-home')))[0];
|
|
expect(row.status).toBe('loaded');
|
|
expect(row.planeId).toBe('plane-starter');
|
|
|
|
// A second load of the same package is rejected — it is no longer open.
|
|
const again = await post('p-home', { planeId: 'plane-starter' });
|
|
expect(again.status).toBe(409);
|
|
});
|
|
});
|