fix(useGameState): re-arm SSE silence check on every message — a stream that goes silent after the first event must fall back to polling again (run-178 root cause); unit: pin NZAA→NFTF full-pipeline marker lons on stored dataset coords
CI / build + unit + e2e (pull_request) Failing after 19m22s
CI / build + unit + e2e (pull_request) Failing after 19m22s
This commit is contained in:
@@ -2,6 +2,9 @@ import { describe, expect, it } from 'vitest';
|
||||
import type { StateSnapshot } from './game/state';
|
||||
import { planesGeoJson, queuedRoutesGeoJson, routesGeoJson, hoverRouteGeoJson, hoverTraceGeoJson, greatCircleArc, ROUTE_ARC_SEGMENTS, type AirportCoord } from './mapData';
|
||||
import { haversineKm } from './geo';
|
||||
import { loadAirportsGeoJson } from './airports';
|
||||
import { positionAt } from './sim/movement';
|
||||
import { PLANE_TYPES } from './game/catalog';
|
||||
|
||||
const AIRPORTS = new Map<string, AirportCoord>([
|
||||
['EDDF', { lat: 50.03, lon: 8.56 }],
|
||||
@@ -503,3 +506,39 @@ describe('greatCircleArc — antimeridian crossing (ISS-120 v0.53.0)', () => {
|
||||
expect(Math.abs(last[1] - nzch.lat)).toBeLessThan(1e-9);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NZAA→NFTF crossing leg — full pipeline on stored dataset coords (ISS-124)', () => {
|
||||
// The exact production pipeline for one flying frame: the server snapshot holds positionAt's WRAPPED
|
||||
// lat/lon (atan2 → [−180, 180]); planesGeoJson re-expresses lon against the departure airport
|
||||
// (locationIcao is always the leg origin while flying — lib/game/state.ts). This pins what lons the
|
||||
// e2e marker sampling can EVER see for this dataset's stored coords.
|
||||
it('marker lons cross above +180 eastward, continuously', async () => {
|
||||
const fc = await loadAirportsGeoJson();
|
||||
const nzaa = fc.features.find((f) => f.properties.icao === 'NZAA')!.properties;
|
||||
const nftf = fc.features.find((f) => f.properties.icao === 'NFTF')!.properties;
|
||||
expect(nzaa.lon).toBeGreaterThan(170); // stored east-positive (Auckland +174.8, not −175.x)
|
||||
expect(nftf.lon).toBeLessThan(-170);
|
||||
|
||||
const km = haversineKm(nzaa.lat, nzaa.lon, nftf.lat, nftf.lon);
|
||||
const index = new Map<string, AirportCoord>([
|
||||
['NZAA', { lat: nzaa.lat, lon: nzaa.lon }],
|
||||
['NFTF', { lat: nftf.lat, lon: nftf.lon }],
|
||||
]);
|
||||
const lons: number[] = [];
|
||||
for (let i = 0; i <= 50; i++) {
|
||||
const pos = positionAt(nzaa.lat, nzaa.lon, nftf.lat, nftf.lon, i / 50); // wrapped — exactly what the snapshot holds
|
||||
const snap = snapshot({
|
||||
planes: [
|
||||
{ id: 'plane-starter', model: 'atr72', status: 'flying', fuel: 1000, maxFuel: PLANE_TYPES.atr72.maxFuel, locationIcao: 'NZAA', locationName: 'Auckland International Airport', destIcao: 'NFTF', destName: "Fua'amotu International Airport", departureSimTime: 0, lat: pos.lat, lon: pos.lon, health: 100, repairUntilSimTime: null, queue: [] },
|
||||
],
|
||||
});
|
||||
lons.push(planesGeoJson(snap, index).features[0]!.geometry.coordinates[0]!);
|
||||
}
|
||||
console.log(`NZAA→NFTF (${km.toFixed(1)} km): sampled marker lons ${JSON.stringify(lons.map((x) => Number(x.toFixed(2))))}`);
|
||||
for (let i = 1; i < lons.length; i++) {
|
||||
expect(Math.abs(lons[i]! - lons[i - 1]!), `jump between samples ${i - 1} and ${i}`).toBeLessThan(180);
|
||||
}
|
||||
expect(Math.max(...lons)).toBeGreaterThan(180); // eastward crossing → onto the + world copy
|
||||
expect(Math.min(...lons)).toBeGreaterThanOrEqual(nzaa.lon - 1e-9); // never wraps to the west copy
|
||||
});
|
||||
});
|
||||
|
||||
+2
-1
@@ -106,7 +106,8 @@ export function useGameState(intervalMs = 1000): GameStateResult {
|
||||
// malformed event — the next one overwrites it anyway
|
||||
}
|
||||
stopPolling(); // fresh push flow → polling no longer needed
|
||||
if (silenceTimer !== null) clearTimeout(silenceTimer); // connect check served its purpose
|
||||
if (silenceTimer !== null) clearTimeout(silenceTimer);
|
||||
silenceTimer = setTimeout(startPolling, SSE_SILENCE_MS); // re-arm on EVERY message: a stream that goes silent after the first event (half-open connection — no onerror fires) must fall back to polling again, or state freezes until reload
|
||||
};
|
||||
source.onerror = () => startPolling(); // stream trouble → poll until pushes resume
|
||||
silenceTimer = setTimeout(startPolling, SSE_SILENCE_MS); // connected but silent for 5 s → poll
|
||||
|
||||
Reference in New Issue
Block a user