mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
A request with any point the proxy cannot answer gets 502, and the page used to put every point of that chunk on its geoid, including the ones the proxy had real (cached or stale) heights for. During an upstream 429 pause that drops cached ground under every chunk that also holds a new point. The 502 body now carries `partial`: the request's real heights in order, null for points without one, left out when there are none. The terrain request service attaches it to the error it throws, and the page caches those heights and uses its geoid only for the rest. `get_terrain_height` still reports the failure as before. Co-Authored-By: Claude Opus 5.5 <[email protected]>
410 lines
15 KiB
JavaScript
410 lines
15 KiB
JavaScript
// src/data/terrainHeights.test.mjs — batched, cached client terrain-height
|
|
// resolver (docs/plans/2026-07-05-entity-height-datum-fix.md Task 3).
|
|
//
|
|
// Locks the module's public interface (Task 5/6 call it verbatim):
|
|
// resolveEllipsoidalGround(coords: [{lat, lon, sourceOrthometricM?}])
|
|
// : Promise<Array<{ellipsoid:number, source:'reearth'|'geoid-fallback'}>>
|
|
// cachedEllipsoidalGround(lat, lon): number|null
|
|
//
|
|
// `fetch` is injected via `globalThis.fetch` (no real network) so these tests
|
|
// run offline like every other suite in this repo. Each test installs its own
|
|
// fake and restores the original in a `finally` so injection never leaks
|
|
// across test files (node:test runs this file's tests sequentially, but the
|
|
// module-level in-memory cache in terrainHeights.js DOES persist across
|
|
// tests within this file — tests below use distinct coordinates per case to
|
|
// avoid cross-test cache contamination, except the dedicated cache tests
|
|
// which rely on that persistence deliberately).
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
resolveEllipsoidalGround,
|
|
cachedEllipsoidalGround,
|
|
cachedRealEllipsoidalGround,
|
|
} from './terrainHeights.js';
|
|
|
|
const AUSTIN = { lat: 30.2672, lon: -97.7431 };
|
|
const AUSTIN_GEOID_N = -26.9; // docs/plans/2026-07-05-entity-height-datum-fix.md verified facts
|
|
|
|
/** Installs a fake fetch for the duration of `fn`, restoring the original after. */
|
|
async function withFakeFetch(fakeFetch, fn) {
|
|
const original = globalThis.fetch;
|
|
globalThis.fetch = fakeFetch;
|
|
try {
|
|
return await fn();
|
|
} finally {
|
|
globalThis.fetch = original;
|
|
}
|
|
}
|
|
|
|
/** Builds a fake proxy response matching the real `/api/terrain/heights` shape. */
|
|
function proxyResponse(points) {
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({
|
|
results: points.map(([lon, lat]) => ({
|
|
lon,
|
|
lat,
|
|
elevation: 0,
|
|
geoid: 0,
|
|
ellipsoid: lon + lat, // arbitrary deterministic value keyed by input, for order-mapping assertions
|
|
})),
|
|
}),
|
|
};
|
|
}
|
|
|
|
test('resolveEllipsoidalGround: happy path maps results IN ORDER', async () => {
|
|
const coords = [
|
|
{ lat: 1, lon: 10 },
|
|
{ lat: 2, lon: 20 },
|
|
{ lat: 3, lon: 30 },
|
|
];
|
|
let capturedUrl = null;
|
|
await withFakeFetch(
|
|
async (url) => {
|
|
capturedUrl = String(url);
|
|
// Order in the URL must match the input coord order (lon,lat pairs).
|
|
return proxyResponse([[10, 1], [20, 2], [30, 3]]);
|
|
},
|
|
async () => {
|
|
const out = await resolveEllipsoidalGround(coords);
|
|
assert.equal(out.length, 3);
|
|
assert.deepEqual(
|
|
out.map((r) => r.ellipsoid),
|
|
[11, 22, 33] // lon+lat for each, in input order
|
|
);
|
|
for (const r of out) assert.equal(r.source, 'reearth');
|
|
}
|
|
);
|
|
assert.ok(capturedUrl.includes('/api/terrain/heights'));
|
|
assert.ok(capturedUrl.includes('points='));
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: a 300-point input issues exactly 5 fetches (chunked at <=64/request) AND preserves order across the chunk boundary', async () => {
|
|
// Each coord carries a DISTINGUISHABLE lat/lon derived from its index so
|
|
// the oracle below can be an independent function of the point's INPUT
|
|
// identity — not an echo of whatever the request happened to send. This is
|
|
// what makes the positional assertions able to catch an index-shift or a
|
|
// within-chunk reversal in fetchChunk's zip (chunk[i].key vs
|
|
// body.results[i]); the brief's guard is "point[250] must map to
|
|
// result[250]".
|
|
// lat(i) = i * 0.001, lon(i) = -i * 0.001 → unique per i, both 5dp-clean
|
|
const oracle = (lat, lon) => lat * 1_000_000 + lon; // deterministic, encodes the index (via lat) unambiguously
|
|
const coords = Array.from({ length: 300 }, (_, i) => ({
|
|
lat: i * 0.001,
|
|
lon: -i * 0.001,
|
|
}));
|
|
let fetchCount = 0;
|
|
const chunkSizes = [];
|
|
await withFakeFetch(
|
|
async (url) => {
|
|
fetchCount += 1;
|
|
const u = new URL(String(url), 'http://internal');
|
|
const pts = u.searchParams.get('points').split(';');
|
|
chunkSizes.push(pts.length);
|
|
// Independent oracle: compute ellipsoid = oracle(lat, lon) per point
|
|
// FROM THE PARSED lon,lat the module actually sent, in the order it
|
|
// sent them. The module (fetchChunk) maps our results back positionally
|
|
// to its chunk items; if it mis-zips, the positional asserts below fail.
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({
|
|
results: pts.map((p) => {
|
|
const [lon, lat] = p.split(',').map(Number);
|
|
return { lon, lat, elevation: 0, geoid: 0, ellipsoid: oracle(lat, lon) };
|
|
}),
|
|
}),
|
|
};
|
|
},
|
|
async () => {
|
|
const out = await resolveEllipsoidalGround(coords);
|
|
assert.equal(out.length, 300);
|
|
// Positional correctness across every chunk boundary: outputs equal the oracle of
|
|
// its OWN input coord. The module rounds to 5dp before sending, so
|
|
// compare against the oracle of the rounded coord to stay exact.
|
|
for (let k = 0; k < coords.length; k += 1) {
|
|
const lat = Number(coords[k].lat.toFixed(5));
|
|
const lon = Number(coords[k].lon.toFixed(5));
|
|
assert.equal(
|
|
out[k].ellipsoid,
|
|
oracle(lat, lon),
|
|
`out[${k}] must map to the oracle of coords[${k}] (order held within+across chunks)`
|
|
);
|
|
assert.equal(out[k].source, 'reearth');
|
|
}
|
|
}
|
|
);
|
|
assert.equal(fetchCount, 5);
|
|
assert.deepEqual(chunkSizes, [64, 64, 64, 64, 44]);
|
|
for (const size of chunkSizes) {
|
|
assert.ok(size <= 64, `chunk size ${size} exceeds the 64-point ceiling`);
|
|
}
|
|
assert.equal(
|
|
chunkSizes.reduce((a, b) => a + b, 0),
|
|
300,
|
|
'chunk sizes must sum to the total input length'
|
|
);
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: proxy failure falls back to sourceOrthometricM + geoidHeight (Austin)', async () => {
|
|
await withFakeFetch(
|
|
async () => {
|
|
throw new Error('simulated network failure');
|
|
},
|
|
async () => {
|
|
const out = await resolveEllipsoidalGround([
|
|
{ lat: AUSTIN.lat, lon: AUSTIN.lon, sourceOrthometricM: 150 },
|
|
]);
|
|
assert.equal(out.length, 1);
|
|
const expected = 150 + AUSTIN_GEOID_N; // ≈ 123.1
|
|
assert.ok(
|
|
Math.abs(out[0].ellipsoid - expected) <= 2.5,
|
|
`expected ≈${expected} (±2.5), got ${out[0].ellipsoid}`
|
|
);
|
|
assert.ok(
|
|
Math.abs(out[0].ellipsoid - 123) <= 2.5,
|
|
`expected ≈123 (±2.5) per the brief's worked example, got ${out[0].ellipsoid}`
|
|
);
|
|
assert.equal(out[0].source, 'geoid-fallback');
|
|
}
|
|
);
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: proxy failure + no sourceOrthometricM falls back to geoidHeight alone (H≈0 coast prior)', async () => {
|
|
const point = { lat: 51.0, lon: -1.0 }; // arbitrary, distinct from other tests' cache keys
|
|
await withFakeFetch(
|
|
async () => {
|
|
throw new Error('simulated network failure');
|
|
},
|
|
async () => {
|
|
const out = await resolveEllipsoidalGround([point]);
|
|
assert.equal(out.length, 1);
|
|
assert.equal(out[0].source, 'geoid-fallback');
|
|
// H treated as 0 -> ellipsoid == geoidHeight(lat,lon) alone, finite and plausible.
|
|
assert.ok(Number.isFinite(out[0].ellipsoid));
|
|
assert.ok(Math.abs(out[0].ellipsoid) < 200, 'geoid undulation should be within the -106..+85 m worldwide range (with margin)');
|
|
}
|
|
);
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: a repeated coord issues NO second fetch (in-memory cache)', async () => {
|
|
const point = { lat: 12.34567, lon: 56.78901 };
|
|
let fetchCount = 0;
|
|
await withFakeFetch(
|
|
async (url) => {
|
|
fetchCount += 1;
|
|
const u = new URL(String(url), 'http://internal');
|
|
const pts = u.searchParams.get('points').split(';').map((p) => p.split(',').map(Number));
|
|
return proxyResponse(pts);
|
|
},
|
|
async () => {
|
|
const first = await resolveEllipsoidalGround([point]);
|
|
const second = await resolveEllipsoidalGround([point]);
|
|
assert.equal(first[0].ellipsoid, second[0].ellipsoid);
|
|
assert.equal(second[0].source, 'reearth');
|
|
}
|
|
);
|
|
assert.equal(fetchCount, 1, `expected exactly 1 fetch across both calls (cache hit on the 2nd), got ${fetchCount}`);
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: repeated coord within the SAME batch also only fetches once', async () => {
|
|
const point = { lat: 22.5001, lon: 33.5001 };
|
|
let fetchCount = 0;
|
|
let lastPointCount = 0;
|
|
await withFakeFetch(
|
|
async (url) => {
|
|
fetchCount += 1;
|
|
const u = new URL(String(url), 'http://internal');
|
|
const pts = u.searchParams.get('points').split(';');
|
|
lastPointCount = pts.length;
|
|
const parsed = pts.map((p) => p.split(',').map(Number));
|
|
return proxyResponse(parsed);
|
|
},
|
|
async () => {
|
|
const out = await resolveEllipsoidalGround([point, point, point]);
|
|
assert.equal(out.length, 3);
|
|
assert.equal(out[0].ellipsoid, out[1].ellipsoid);
|
|
assert.equal(out[1].ellipsoid, out[2].ellipsoid);
|
|
}
|
|
);
|
|
assert.equal(fetchCount, 1);
|
|
assert.equal(lastPointCount, 1, 'the duplicate point should be de-duplicated before hitting the network');
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: rounds coordinates to 5 decimals for the cache key (near-identical points share one fetch)', async () => {
|
|
const a = { lat: 44.111112, lon: 55.222223 };
|
|
const b = { lat: 44.1111124, lon: 55.2222234 }; // rounds to the same 5dp key as `a`
|
|
let fetchCount = 0;
|
|
await withFakeFetch(
|
|
async (url) => {
|
|
fetchCount += 1;
|
|
const u = new URL(String(url), 'http://internal');
|
|
const pts = u.searchParams.get('points').split(';').map((p) => p.split(',').map(Number));
|
|
return proxyResponse(pts);
|
|
},
|
|
async () => {
|
|
await resolveEllipsoidalGround([a]);
|
|
await resolveEllipsoidalGround([b]);
|
|
}
|
|
);
|
|
assert.equal(fetchCount, 1, 'points identical at 5-decimal rounding should share the in-memory cache');
|
|
});
|
|
|
|
test('cachedEllipsoidalGround: returns the prior resolved ellipsoid value synchronously', async () => {
|
|
const point = { lat: 9.87654, lon: 8.76543 };
|
|
assert.equal(cachedEllipsoidalGround(point.lat, point.lon), null, 'cold cache must return null');
|
|
await withFakeFetch(
|
|
async (url) => {
|
|
const u = new URL(String(url), 'http://internal');
|
|
const pts = u.searchParams.get('points').split(';').map((p) => p.split(',').map(Number));
|
|
return proxyResponse(pts);
|
|
},
|
|
async () => {
|
|
const [result] = await resolveEllipsoidalGround([point]);
|
|
const cached = cachedEllipsoidalGround(point.lat, point.lon);
|
|
assert.equal(cached, result.ellipsoid);
|
|
}
|
|
);
|
|
});
|
|
|
|
test('cachedEllipsoidalGround: returns null for a coordinate never resolved', () => {
|
|
assert.equal(cachedEllipsoidalGround(-89.99999, 179.99999), null);
|
|
});
|
|
|
|
test('cachedEllipsoidalGround: also populated by the geoid-fallback path (cache stores fallback results too)', async () => {
|
|
const point = { lat: -33.5, lon: 151.2 }; // distinct coord, unused elsewhere
|
|
await withFakeFetch(
|
|
async () => {
|
|
throw new Error('simulated network failure');
|
|
},
|
|
async () => {
|
|
const [result] = await resolveEllipsoidalGround([point]);
|
|
const cached = cachedEllipsoidalGround(point.lat, point.lon);
|
|
assert.equal(cached, result.ellipsoid);
|
|
}
|
|
);
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: a non-ok HTTP response is treated as failure and falls back', async () => {
|
|
const point = { lat: 61.001, lon: -149.001 }; // distinct coord
|
|
await withFakeFetch(
|
|
async () => ({ ok: false, status: 500, json: async () => ({ error: 'boom' }) }),
|
|
async () => {
|
|
const out = await resolveEllipsoidalGround([
|
|
{ ...point, sourceOrthometricM: 10 },
|
|
]);
|
|
assert.equal(out[0].source, 'geoid-fallback');
|
|
assert.ok(Number.isFinite(out[0].ellipsoid));
|
|
}
|
|
);
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: geoid fallback cools down retries, then self-heals after 60s', async () => {
|
|
const point = { lat: 64.20001, lon: -21.90001, sourceOrthometricM: 25 };
|
|
const originalNow = Date.now;
|
|
let now = 5_000_000;
|
|
let fetchCount = 0;
|
|
Date.now = () => now;
|
|
try {
|
|
await withFakeFetch(
|
|
async (url) => {
|
|
fetchCount += 1;
|
|
if (fetchCount === 1) throw new Error('simulated outage');
|
|
const u = new URL(String(url), 'http://internal');
|
|
const points = u.searchParams.get('points').split(';').map((pair) => pair.split(',').map(Number));
|
|
return proxyResponse(points);
|
|
},
|
|
async () => {
|
|
const first = await resolveEllipsoidalGround([point]);
|
|
assert.equal(first[0].source, 'geoid-fallback');
|
|
|
|
const cooling = await resolveEllipsoidalGround([point]);
|
|
assert.equal(cooling[0].source, 'geoid-fallback');
|
|
assert.equal(fetchCount, 1, 'a warm request during cooldown must not hit the failing proxy');
|
|
|
|
now += 60_001;
|
|
const healed = await resolveEllipsoidalGround([point]);
|
|
assert.equal(fetchCount, 2, 'the point should retry after cooldown');
|
|
assert.equal(healed[0].source, 'reearth');
|
|
|
|
await resolveEllipsoidalGround([point]);
|
|
assert.equal(fetchCount, 2, 'a successful retry replaces fallback and clears retry work');
|
|
}
|
|
);
|
|
} finally {
|
|
Date.now = originalNow;
|
|
}
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: an empty input array resolves to an empty array without fetching', async () => {
|
|
let fetchCount = 0;
|
|
await withFakeFetch(
|
|
async () => {
|
|
fetchCount += 1;
|
|
return proxyResponse([]);
|
|
},
|
|
async () => {
|
|
const out = await resolveEllipsoidalGround([]);
|
|
assert.deepEqual(out, []);
|
|
}
|
|
);
|
|
assert.equal(fetchCount, 0);
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: null and string heights cannot become real zeroes', async () => {
|
|
for (const [index, value] of [null, '', '0'].entries()) {
|
|
await withFakeFetch(
|
|
async () => ({ ok: true, json: async () => ({ results: [{ ellipsoid: value }] }) }),
|
|
async () => {
|
|
const out = await resolveEllipsoidalGround([{ lat: 11 + index, lon: 41 }]);
|
|
assert.equal(out[0].source, 'geoid-fallback');
|
|
}
|
|
);
|
|
}
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: a 502 keeps the real heights it carries and falls back only for the rest', async () => {
|
|
const coords = [
|
|
{ lat: 62.001, lon: -150.001, sourceOrthometricM: 10 },
|
|
{ lat: 62.002, lon: -150.002, sourceOrthometricM: 10 },
|
|
{ lat: 62.003, lon: -150.003, sourceOrthometricM: 10 },
|
|
];
|
|
await withFakeFetch(
|
|
async () => ({
|
|
ok: false,
|
|
status: 502,
|
|
json: async () => ({
|
|
error: 'terrain heights fetch failed and no cache available for every point',
|
|
partial: [{ ellipsoid: 412.5 }, null, { ellipsoid: 'x' }],
|
|
}),
|
|
}),
|
|
async () => {
|
|
const out = await resolveEllipsoidalGround(coords);
|
|
assert.deepEqual(out[0], { ellipsoid: 412.5, source: 'reearth' });
|
|
assert.equal(out[1].source, 'geoid-fallback');
|
|
assert.equal(out[2].source, 'geoid-fallback', 'a non-numeric height is not real');
|
|
}
|
|
);
|
|
assert.equal(cachedRealEllipsoidalGround(62.001, -150.001), 412.5);
|
|
assert.equal(cachedRealEllipsoidalGround(62.002, -150.002), null);
|
|
});
|
|
|
|
test('resolveEllipsoidalGround: a partial list of the wrong length is ignored', async () => {
|
|
const coords = [
|
|
{ lat: 63.001, lon: -151.001 },
|
|
{ lat: 63.002, lon: -151.002 },
|
|
];
|
|
await withFakeFetch(
|
|
async () => ({
|
|
ok: false,
|
|
status: 502,
|
|
json: async () => ({ error: 'x', partial: [{ ellipsoid: 7 }] }),
|
|
}),
|
|
async () => {
|
|
const out = await resolveEllipsoidalGround(coords);
|
|
assert.deepEqual(out.map((r) => r.source), ['geoid-fallback', 'geoid-fallback']);
|
|
}
|
|
);
|
|
});
|