fix: forFlight refuel leaves an exactly-feasible tank (round up) — e2e 409
CI / build + unit + e2e (pull_request) Failing after 5m26s
CI / build + unit + e2e (pull_request) Failing after 5m26s
This commit is contained in:
@@ -443,6 +443,30 @@ describe('refuelPlane', () => {
|
||||
expect(account.cash).toBeCloseTo(cashBefore - cost, 6);
|
||||
});
|
||||
|
||||
it('forFlight leaves an exactly-feasible tank — flyPlane to the package dest succeeds right after', async () => {
|
||||
const inst = await freshDb('refuel-ff-flyafter.db');
|
||||
await insertOpenPackage(inst, 'p-fa', { destIcao: 'LEMD' }); // EDDF → LEMD
|
||||
await loadPackage(inst, 'p-fa', 'plane-starter');
|
||||
|
||||
const fc = await loadAirportsGeoJson();
|
||||
const from = fc.features.find((f) => f.properties.icao === HUB_ICAO)!.properties;
|
||||
const to = fc.features.find((f) => f.properties.icao === 'LEMD')!.properties;
|
||||
const km = haversineKm(from.lat, from.lon, to.lat, to.lon);
|
||||
const type = PLANE_TYPES.atr72;
|
||||
const required = requiredFuelFor(type.burnPerKm, type.emptyWeightKg, type.maxFuel, km);
|
||||
|
||||
// Empty-ish tank: well below the leg requirement.
|
||||
await inst.db.update(schema.planes).set({ fuel: Math.round(required / 4) }).where(eq(schema.planes.id, 'plane-starter'));
|
||||
|
||||
await refuelPlane(inst, 'plane-starter', { mode: 'forFlight', packageId: 'p-fa' });
|
||||
|
||||
// The leg must now be flyable — no epsilon-short 409 (regression guard for the float-exact target).
|
||||
await flyPlane(inst, 'plane-starter', 'LEMD');
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.status).toBe('flying');
|
||||
expect(plane.destIcao).toBe('LEMD');
|
||||
});
|
||||
|
||||
it('forFlight is a free no-op when the tank already covers the leg', async () => {
|
||||
const inst = await freshDb('refuel-forflight-enough.db'); // seeded starter is full at EDDF
|
||||
await insertOpenPackage(inst, 'p-ff2', { destIcao: 'LEMD' });
|
||||
|
||||
+9
-6
@@ -248,7 +248,7 @@ export async function refuelPlane(inst: GameDb, planeId: string, opts: RefuelOpt
|
||||
}
|
||||
|
||||
const mode = opts.mode ?? 'full';
|
||||
let liters: number; // how much fuel to add
|
||||
let targetFuel: number; // tank level after the refuel
|
||||
if (mode === 'forFlight') {
|
||||
if (!opts.packageId) throw new GameActionError(400, 'packageId is required when mode is "forFlight"');
|
||||
|
||||
@@ -269,11 +269,17 @@ export async function refuelPlane(inst: GameDb, planeId: string, opts: RefuelOpt
|
||||
if (requiredL > plane.maxFuel) {
|
||||
throw new GameActionError(409, `the leg to ${pkg.destIcao} needs ${Math.round(requiredL)} L even starting empty — it does not fit a full ${plane.maxFuel} L tank`);
|
||||
}
|
||||
liters = Math.max(0, requiredL - plane.fuel);
|
||||
// Land EXACTLY on the leg requirement when topping up: flyPlane re-computes this same value and
|
||||
// compares with strict `>`, so fuel := requiredL guarantees the leg is feasible. The alternative
|
||||
// (fuel + liters) can round a hair BELOW R in IEEE754 — f + fl(R − f) is not guaranteed ≥ R — and
|
||||
// would 409 on an otherwise-feasible leg. Already enough → free no-op, tank untouched.
|
||||
targetFuel = Math.max(plane.fuel, requiredL);
|
||||
} else {
|
||||
liters = plane.maxFuel - plane.fuel;
|
||||
// Exactly maxFuel (as before v0.16.0) — clients/tests compare against it with strict equality.
|
||||
targetFuel = plane.maxFuel;
|
||||
}
|
||||
|
||||
const liters = Math.max(0, targetFuel - plane.fuel);
|
||||
const cost = Math.round(liters * FUEL_PRICE_PER_L * 100) / 100;
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
if (!account) throw new Error('account row missing');
|
||||
@@ -281,9 +287,6 @@ export async function refuelPlane(inst: GameDb, planeId: string, opts: RefuelOpt
|
||||
throw new GameActionError(409, `insufficient funds: refueling costs ${cost}, you have ${Math.round(account.cash)}`);
|
||||
}
|
||||
|
||||
// Full mode lands EXACTLY on maxFuel (as before v0.16.0), not fuel + liters — the sum can
|
||||
// drift a ulp for fractional tanks and clients/tests compare against maxFuel exactly.
|
||||
const targetFuel = mode === 'forFlight' ? plane.fuel + liters : plane.maxFuel;
|
||||
await inst.db.update(schema.planes).set({ fuel: targetFuel }).where(eq(schema.planes.id, planeId));
|
||||
if (cost > 0) {
|
||||
await inst.db.update(schema.account).set({ cash: Math.round((account.cash - cost) * 100) / 100 });
|
||||
|
||||
Reference in New Issue
Block a user