CI / build + unit + e2e (pull_request) Failing after 6m37s
- planes.queue TEXT NOT NULL DEFAULT '[]' — ordered JSON array of not-yet-started
leg ICACs; active leg stays destIcao + departureSimTime. DDL + guarded ALTER
migration for existing DBs; seed sets queue: [] explicitly.
- deliverPackages() auto-chains on arrival: with a non-empty queue and fuel
covering the first queued leg (same requiredFuelFor rule as /fly), the plane
immediately starts that leg from the arrival airport (stays 'flying',
departureSimTime = arrival instant, queue shortened). Short tank → grounds
safely with the queue kept intact; empty queue → unchanged grounding. Cargo
delivery for the reached airport runs first and is unaffected. Ordering kept:
delivery still runs before tickOnce's pause check, so a chained plane counts
as still flying — no premature last-plane pause (ISS-041 preserved).
- New endpoints via performAction(): POST /api/planes/{planeId}/queue {destIcao}
(append; works grounded or flying; 400 bad body/unknown ICAO, 404 unknown
plane) and POST /api/planes/{planeId}/queue/cancel {index} (remove queued leg
by index; active in-flight leg never affected; 400 non-integer/out-of-range,
404 unknown plane). Both return the full state snapshot.
- PlaneState.queue: string[] in /api/state (null/malformed-safe parse helper
shared with the engine); OpenAPI updated (PlaneState.queue, QueueLegRequest,
CancelQueuedLegRequest, both paths) and openapi.test.ts expectations extended.
- Tests: auto-chain block in engine.test.ts (chain / short-fuel ground / empty
queue) + chained-plane-no-pause regression guard; queueLeg/cancelQueuedLeg
blocks in actions.test.ts; type-compat queue: [] fixtures in deriveEvents,
right-pane, mapData and seed tests.
v0.25.0 — part 1 (backend) of ISS-043; UI/map rendering lands in a follow-up.
164 lines
6.0 KiB
TypeScript
164 lines
6.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import type { PackageState, PlaneState, StateSnapshot } from './game/state';
|
||
import { diffSnapshots } from './deriveEvents';
|
||
|
||
// ISS-008 (v0.17.0): snapshot-diffing event derivation — pure core of the toast/notification
|
||
// feature, unit-tested in node like advanceSimClock/fetchState.
|
||
|
||
const CATALOG = [
|
||
{ model: 'atr72', label: 'ATR 72-600', price: 200_000, maxFuel: 6300, burnPerKm: 1.75, emptyWeightKg: 9300, speedKmh: 510, capacity: 6 },
|
||
];
|
||
|
||
const basePlane = (id: string): PlaneState => ({
|
||
id,
|
||
model: 'atr72',
|
||
status: 'available',
|
||
fuel: 6300,
|
||
maxFuel: 6300,
|
||
locationIcao: 'EDDF',
|
||
locationName: 'Frankfurt',
|
||
destIcao: null,
|
||
destName: null,
|
||
departureSimTime: null,
|
||
lat: 50.03,
|
||
lon: 8.56,
|
||
queue: [],
|
||
});
|
||
|
||
const basePackage = (id: string): PackageState => ({
|
||
id,
|
||
originIcao: 'EDDF',
|
||
originName: 'Frankfurt',
|
||
destIcao: 'LEMD',
|
||
destName: 'Madrid–Barajas',
|
||
reward: 1234.5,
|
||
cargoType: 'mail',
|
||
cargoCount: 2,
|
||
deadlineSimTime: 0,
|
||
status: 'open',
|
||
planeId: null,
|
||
createdSimTime: 0,
|
||
});
|
||
|
||
const snapshot = (over: Partial<StateSnapshot> = {}): StateSnapshot => ({
|
||
account: { cash: 20_000, reputation: 10 },
|
||
simState: { clockMs: 0, timeScale: 1, paused: false },
|
||
planes: [basePlane('plane-starter')],
|
||
packages: [],
|
||
catalog: CATALOG,
|
||
fuelPricePerL: 0.8,
|
||
sellFraction: 0.5,
|
||
...over,
|
||
});
|
||
|
||
describe('diffSnapshots', () => {
|
||
it('returns [] for identical snapshots (only clock/cash drift)', () => {
|
||
const prev = snapshot();
|
||
const next = snapshot({ account: { cash: 21_000, reputation: 11 }, simState: { clockMs: 60_000, timeScale: 1, paused: false } });
|
||
expect(diffSnapshots(prev, next)).toEqual([]);
|
||
});
|
||
|
||
it('returns [] for the first snapshot (no baseline — no "new package" spam on page load)', () => {
|
||
const next = snapshot({ packages: [basePackage('pkg-1'), basePackage('pkg-2')] });
|
||
expect(diffSnapshots(null, next)).toEqual([]);
|
||
});
|
||
|
||
it('emits a landing event when a plane goes flying → available (new locationName is the arrival)', () => {
|
||
const prev = snapshot({
|
||
planes: [
|
||
{
|
||
...basePlane('plane-starter'),
|
||
status: 'flying',
|
||
destIcao: 'LEMD',
|
||
destName: 'Madrid–Barajas',
|
||
departureSimTime: 0,
|
||
},
|
||
],
|
||
});
|
||
const next = snapshot({
|
||
planes: [
|
||
{ ...basePlane('plane-starter'), locationIcao: 'LEMD', locationName: 'Madrid–Barajas' },
|
||
],
|
||
});
|
||
expect(diffSnapshots(prev, next)).toEqual([
|
||
{ kind: 'info', message: 'ATR 72-600 landed at Madrid–Barajas' },
|
||
]);
|
||
});
|
||
|
||
it('emits a delivery event with the reward when a loaded package is delivered on arrival', () => {
|
||
const prev = snapshot({ packages: [{ ...basePackage('pkg-1'), status: 'loaded', planeId: 'plane-starter' }] });
|
||
const next = snapshot({ packages: [{ ...basePackage('pkg-1'), status: 'delivered', planeId: 'plane-starter' }] });
|
||
expect(diffSnapshots(prev, next)).toEqual([
|
||
{ kind: 'info', message: 'Package delivered: +€1,234.50' },
|
||
]);
|
||
});
|
||
|
||
it('emits an expiry event for open → expired and loaded → expired', () => {
|
||
const fromOpen = diffSnapshots(
|
||
snapshot({ packages: [basePackage('pkg-1')] }),
|
||
snapshot({ packages: [{ ...basePackage('pkg-1'), status: 'expired' }] }),
|
||
);
|
||
expect(fromOpen).toEqual([{ kind: 'info', message: 'Package expired: Frankfurt → Madrid–Barajas' }]);
|
||
|
||
const fromLoaded = diffSnapshots(
|
||
snapshot({ packages: [{ ...basePackage('pkg-2'), status: 'loaded', planeId: 'plane-starter' }] }),
|
||
snapshot({ packages: [{ ...basePackage('pkg-2'), status: 'expired', planeId: 'plane-starter' }] }),
|
||
);
|
||
expect(fromLoaded).toEqual([{ kind: 'info', message: 'Package expired: Frankfurt → Madrid–Barajas' }]);
|
||
});
|
||
|
||
it('emits a new-package event for ids only in the next snapshot', () => {
|
||
const prev = snapshot({ packages: [basePackage('pkg-1')] });
|
||
const next = snapshot({ packages: [basePackage('pkg-1'), basePackage('pkg-2')] });
|
||
expect(diffSnapshots(prev, next)).toEqual([
|
||
{ kind: 'info', message: 'New package available: Frankfurt → Madrid–Barajas' },
|
||
]);
|
||
});
|
||
|
||
it('emits bought/sold events when the plane id set changes (with the model name)', () => {
|
||
const bought = diffSnapshots(
|
||
snapshot(),
|
||
snapshot({ planes: [basePlane('plane-starter'), basePlane('plane-2')] }),
|
||
);
|
||
expect(bought).toEqual([{ kind: 'info', message: 'Bought ATR 72-600' }]);
|
||
|
||
const sold = diffSnapshots(
|
||
snapshot({ planes: [basePlane('plane-starter'), basePlane('plane-2')] }),
|
||
snapshot(),
|
||
);
|
||
expect(sold).toEqual([{ kind: 'info', message: 'Sold ATR 72-600' }]);
|
||
});
|
||
|
||
it('reports every transition in one diff (planes first, then packages)', () => {
|
||
const prev = snapshot({
|
||
planes: [
|
||
{ ...basePlane('plane-starter'), status: 'flying', destIcao: 'LEMD', destName: 'Madrid–Barajas' },
|
||
basePlane('plane-2'),
|
||
],
|
||
packages: [{ ...basePackage('pkg-1'), status: 'loaded', planeId: 'plane-starter' }],
|
||
});
|
||
const next = snapshot({
|
||
planes: [
|
||
{ ...basePlane('plane-starter'), locationIcao: 'LEMD', locationName: 'Madrid–Barajas' },
|
||
basePlane('plane-3'),
|
||
],
|
||
packages: [{ ...basePackage('pkg-1'), status: 'delivered', planeId: 'plane-starter' }],
|
||
});
|
||
expect(diffSnapshots(prev, next)).toEqual([
|
||
{ kind: 'info', message: 'ATR 72-600 landed at Madrid–Barajas' },
|
||
{ kind: 'info', message: 'Bought ATR 72-600' },
|
||
{ kind: 'info', message: 'Sold ATR 72-600' },
|
||
{ kind: 'info', message: 'Package delivered: +€1,234.50' },
|
||
]);
|
||
});
|
||
|
||
it('ignores transitions that are not events (open → loaded, available → flying)', () => {
|
||
const prev = snapshot({ packages: [basePackage('pkg-1')] });
|
||
const next = snapshot({
|
||
planes: [{ ...basePlane('plane-starter'), status: 'flying', destIcao: 'LEMD', destName: 'Madrid–Barajas' }],
|
||
packages: [{ ...basePackage('pkg-1'), status: 'loaded', planeId: 'plane-starter' }],
|
||
});
|
||
expect(diffSnapshots(prev, next)).toEqual([]);
|
||
});
|
||
});
|