// 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> // 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']); } ); });