Files
Flight-Ops/components/right-pane.test.ts
T
kamilkrzywda a801311fc5
CI / build + unit + e2e (pull_request) Failing after 5m28s
feat: v0.16.0 — ISS-005 weight-proportional fuel burn + refuel just enough for the loaded package
Part A: r(F) = k(We + rho*F) L/km, k calibrated so a full tank burns exactly
the old constant; closed forms in lib/sim/movement.ts (fuelAtWeighted,
requiredFuelFor, burnRateConstant, FUEL_DENSITY_KG_PER_L). Catalog gains
emptyWeightKg (ATR72 OEW 9300 kg), exposed in the snapshot. All three burn
call sites (engine landing, state live fuel, fly validation) moved to the
weighted model; old constant-rate fuelAt removed.

Part B: POST /api/planes/{planeId}/refuel takes an optional body
{mode:'full'|'forFlight', packageId} — no body keeps today's fill-to-full
behavior exactly (fuel lands bit-exact on maxFuel); forFlight adds just
enough for the loaded package's destination leg (409 not-loaded/expired/
doesn't-fit, 400 bad body). UI: per-cargo-row 'Refuel' button
(refuel-for-{pkg.id}), market button renamed 'Refuel to full'.

OpenAPI: RefuelRequest schema + optional requestBody + 400 on refuel, fly's
409 description updated, CatalogEntry.emptyWeightKg. Tests: new weighted-burn
suite, forFlight action/route cases (incl. ~7670 km UNNT<->LPPD doesn't-fit),
openapi guards; landing-fuel assertions in engine.test.ts + e2e/actions.spec.ts
use the closed form; the e2e 'return leg unfuelable' scenario reworked — a full
tank now covers the EDDF<->LEZL round trip, so the 409 moves to the second
outbound. New e2e: row Refuel deducts exactly L*price / raises fuel by exactly
L via the endpoint's own response snapshot.
2026-09-19 12:24:34 +00:00

121 lines
4.3 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { loadEligiblePlanes, slotsUsedFor } from './RightPane';
import type { PackageState, PlaneState, StateSnapshot } from '@/lib/game/state';
// v0.12.0 — the package-model picker rules: a plane can LOAD a package only while
// it is GROUNDED at the package's origin with a free slot (capacity from the
// snapshot catalog; expired packages still occupy theirs). The Load button acts
// directly on the single eligible plane, or shows a picker when several qualify.
const CATALOG = [
{ model: 'atr72', label: 'ATR 72-600', price: 200_000, maxFuel: 6300, burnPerKm: 1.75, emptyWeightKg: 9_300, speedKmh: 510, capacity: 6 },
];
function plane(over: Partial<PlaneState> & Pick<PlaneState, 'id'>): PlaneState {
return {
model: 'atr72',
status: 'available',
fuel: 100,
maxFuel: 100,
locationIcao: 'EDDF',
locationName: 'Frankfurt Main',
destIcao: null,
destName: null,
departureSimTime: null,
lat: 50,
lon: 8.5,
...over,
};
}
function pkg(over: Partial<PackageState> & Pick<PackageState, 'id'>): PackageState {
return {
originIcao: 'EDDF',
originName: 'Frankfurt Main',
destIcao: 'LEMD',
destName: 'Madrid Barajas',
reward: 1000,
cargoType: 'mail',
cargoCount: 4,
deadlineSimTime: 1_000_000,
status: 'open',
planeId: null,
createdSimTime: 0,
...over,
};
}
function snap(planes: PlaneState[], packages: PackageState[] = []): StateSnapshot {
return {
account: { cash: 0, reputation: 0 },
simState: { clockMs: 0, timeScale: 1, paused: false },
planes,
packages,
catalog: CATALOG,
fuelPricePerL: 0.8,
sellFraction: 0.5,
};
}
describe('slotsUsedFor', () => {
it('counts loaded AND expired packages bound to the plane (both occupy a slot)', () => {
const s = snap(
[plane({ id: 'p1' })],
[
pkg({ id: 'a', status: 'loaded', planeId: 'p1' }),
pkg({ id: 'b', status: 'expired', planeId: 'p1' }),
pkg({ id: 'c', status: 'open', planeId: null }), // not on any plane
pkg({ id: 'd', status: 'delivered', planeId: 'p1' }), // terminal — slot long freed
],
);
expect(slotsUsedFor(s, 'p1')).toBe(2);
});
it('is 0 for a plane with no bound cargo', () => {
expect(slotsUsedFor(snap([plane({ id: 'p1' })]), 'p1')).toBe(0);
});
});
describe('loadEligiblePlanes (Load button eligibility)', () => {
it('includes only grounded planes at the package origin with a free slot', () => {
const s = snap([
plane({ id: 'at-origin' }), // eligible
plane({ id: 'elsewhere', locationIcao: 'LEMD' }), // wrong airport
plane({ id: 'in-flight', status: 'flying', destIcao: 'LEMD', destName: 'Madrid Barajas' }), // flying
]);
expect(loadEligiblePlanes(s, pkg({ id: 'x' })).map((p) => p.id)).toEqual(['at-origin']);
});
it('excludes a plane whose slots are full (capacity from the catalog)', () => {
const packages = Array.from({ length: 6 }, (_, i) =>
pkg({ id: `fill-${i}`, status: 'loaded', planeId: 'full' }),
);
const s = snap([plane({ id: 'full' })], packages);
expect(loadEligiblePlanes(s, pkg({ id: 'x' }))).toEqual([]);
});
it('a full plane with one slot freed by an expired package still counts as full', () => {
const packages = [
...Array.from({ length: 5 }, (_, i) => pkg({ id: `fill-${i}`, status: 'loaded', planeId: 'full' })),
pkg({ id: 'exp', status: 'expired', planeId: 'full' }), // occupies its slot
];
const s = snap([plane({ id: 'full' })], packages);
expect(loadEligiblePlanes(s, pkg({ id: 'x' }))).toEqual([]);
});
it('returns several planes when several qualify (picker case)', () => {
const s = snap([plane({ id: 'a' }), plane({ id: 'b' })]);
expect(loadEligiblePlanes(s, pkg({ id: 'x' })).map((p) => p.id).sort()).toEqual(['a', 'b']);
});
it('returns nothing when no plane is at the origin (Load stays disabled)', () => {
const s = snap([plane({ id: 'far', locationIcao: 'LEMD' })]);
expect(loadEligiblePlanes(s, pkg({ id: 'x' }))).toEqual([]);
});
it('a plane with an unknown model has no capacity → never eligible', () => {
const s = snap([plane({ id: 'mystery', model: 'mystery' })]);
expect(loadEligiblePlanes(s, pkg({ id: 'x' }))).toEqual([]);
});
});