Wind-adjusted fuel burn ±20% + refuel-for-flight buys 150% margin (ISS-070, ISS-071, v0.36.0)
CI / build + unit + e2e (pull_request) Successful in 10m34s
CI / build + unit + e2e (pull_request) Successful in 10m34s
This commit is contained in:
+28
-18
@@ -6,8 +6,9 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { openGameDb, type GameDb } from '@/lib/db/client';
|
||||
import * as schema from '@/lib/db/schema';
|
||||
import { loadAirportsGeoJson } from '@/lib/airports';
|
||||
import { haversineKm } from '@/lib/geo';
|
||||
import { requiredFuelFor } from '@/lib/sim/movement';
|
||||
import { haversineKm, initialBearingDeg } from '@/lib/geo';
|
||||
import { weatherFor } from '@/lib/weather';
|
||||
import { requiredFuelFor, windBurnFactor } from '@/lib/sim/movement';
|
||||
import { FUEL_PRICE_PER_L, PLANE_TYPES, SELL_FRACTION } from './catalog';
|
||||
import { HUB_ICAO } from './seed';
|
||||
import {
|
||||
@@ -591,17 +592,29 @@ describe('refuelPlane', () => {
|
||||
|
||||
// --- forFlight mode (v0.16.0) ---
|
||||
|
||||
it('forFlight adds exactly the liters needed for the package leg and deducts L × price', async () => {
|
||||
const inst = await freshDb('refuel-forflight-ok.db');
|
||||
await insertOpenPackage(inst, 'p-ff', { destIcao: 'LEMD' }); // EDDF → LEMD
|
||||
await loadPackage(inst, 'p-ff', 'plane-starter');
|
||||
|
||||
// Wind-adjusted requirement for the hub → LEMD leg (v0.36.0) — computed with the SAME helpers
|
||||
// refuelPlane uses, so the expectations stay exact without hardcoded literals.
|
||||
async function adjustedRequiredForHubToLemd() {
|
||||
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);
|
||||
const w = weatherFor(HUB_ICAO);
|
||||
return requiredFuelFor(
|
||||
type.burnPerKm * windBurnFactor(w.windKt, w.windDirDeg, initialBearingDeg(from.lat, from.lon, to.lat, to.lon)),
|
||||
type.emptyWeightKg,
|
||||
type.maxFuel,
|
||||
km,
|
||||
);
|
||||
}
|
||||
|
||||
it('forFlight buys 150% of the weather-adjusted leg requirement and deducts L × price', async () => {
|
||||
const inst = await freshDb('refuel-forflight-ok.db');
|
||||
await insertOpenPackage(inst, 'p-ff', { destIcao: 'LEMD' }); // EDDF → LEMD
|
||||
await loadPackage(inst, 'p-ff', 'plane-starter');
|
||||
|
||||
const required = await adjustedRequiredForHubToLemd();
|
||||
|
||||
// Partial tank below the requirement so L > 0.
|
||||
const startFuel = Math.round(required / 2);
|
||||
@@ -610,31 +623,28 @@ describe('refuelPlane', () => {
|
||||
|
||||
const { cost } = await refuelPlane(inst, 'plane-starter', { mode: 'forFlight', packageId: 'p-ff' });
|
||||
|
||||
expect(cost).toBe(Math.round((required - startFuel) * FUEL_PRICE_PER_L * 100) / 100);
|
||||
// ISS-071: target is 150% of the requirement (below tank capacity for this leg).
|
||||
const targetFuel = Math.min(PLANE_TYPES.atr72.maxFuel, Math.max(startFuel, required * 1.5));
|
||||
expect(cost).toBe(Math.round((targetFuel - startFuel) * FUEL_PRICE_PER_L * 100) / 100);
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.fuel).toBeCloseTo(required, 6); // topped up to exactly the leg requirement
|
||||
expect(plane.fuel).toBeCloseTo(targetFuel, 6); // topped up to exactly the 150% target
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
expect(account.cash).toBeCloseTo(cashBefore - cost, 6);
|
||||
});
|
||||
|
||||
it('forFlight leaves an exactly-feasible tank — flyPlane to the package dest succeeds right after', async () => {
|
||||
it('forFlight leaves a tank with a 50% margin — 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);
|
||||
const required = await adjustedRequiredForHubToLemd();
|
||||
|
||||
// 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).
|
||||
// The leg must now be flyable — the 150% margin covers flyPlane's strict `>` check.
|
||||
await flyPlane(inst, 'plane-starter', 'LEMD');
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.status).toBe('flying');
|
||||
|
||||
Reference in New Issue
Block a user