Merge main into pr-722

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Sameh KhamisandClaude Opus 5.5 committed 2026-09-24 22:08:54 -07:00
commit 0d58d6e1c4
21 files changed
+739 -30

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { FEATURE_SOURCE_METHODS } from '../sources/featureSource.js';
import { createAnnotationResolver } from './resolver.js';
function featureSource(overrides = {}) {
const source = {};
for (const method of FEATURE_SOURCE_METHODS)
source[method] = async () => null;
return Object.assign(source, overrides);
}
const texasGeocode = {
async geocode() {
return {
place: {
lat: 31.0,
lng: -99.9,
label: 'Texas, USA',
name: 'Texas',
types: ['administrative_area_level_1', 'political'],
},
};
},
};
test('region ring: Natural Earth names resolve without a geocoder', async () => {
const { resolveRegionRingForQuery } = createAnnotationResolver({
featureSource: featureSource(),
});
for (const name of ['Gulf of Mexico', 'the Alps']) {
const region = await resolveRegionRingForQuery(name);
assert.ok(region?.ring?.length >= 3, `${name} resolves to a ring`);
assert.equal(region.error, undefined);
}
});
test('region ring: a slow admin-boundary lookup returns region-timeout within the budget', async () => {
let adminSignal;
let release;
const { resolveRegionRingForQuery } = createAnnotationResolver({
featureSource: featureSource({
getAdministrativeAreas(_point, { signal }) {
adminSignal = signal;
return new Promise((resolve) => {
release = resolve;
});
},
}),
});
const started = Date.now();
const region = await resolveRegionRingForQuery(
'Texas',
undefined,
texasGeocode,
{ budgetMs: 50 },
);
assert.deepEqual(region, {
name: 'Texas',
ring: null,
error: 'region-timeout',
});
assert.ok(
Date.now() - started < 1000,
'returns at the budget, not the lookup',
);
// The lookup is left running so it can fill the boundary cache.
assert.equal(adminSignal?.aborted ?? false, false);
release(null);
});
test('region ring: an unbounded budget waits for the lookup', async () => {
const { resolveRegionRingForQuery } = createAnnotationResolver({
featureSource: featureSource({
getAdministrativeAreas: () =>
new Promise((resolve) => setTimeout(() => resolve(null), 30)),
}),
});
const region = await resolveRegionRingForQuery(
'Texas',
undefined,
texasGeocode,
{ budgetMs: Infinity },
);
assert.equal(region, null);
});
+29 -1
View File
@@ -25,6 +25,9 @@ import {
import { unavailablePlaceSearch } from '../search/placeSearch.js';
import { isPickedWorldPosition } from '../data/scenePick.js';
/** Ms the analyst region lookup waits on geocode + admin boundary. */
export const REGION_FALLBACK_BUDGET_MS = 3_000;
/** Own annotation lookup caches and ranking of supplied feature candidates. */
export function createAnnotationResolver({
boundarySource,
@@ -1877,14 +1880,22 @@ export function createAnnotationResolver({
* resolve to a region-like boundary — the analyst engine reports that
* honestly rather than silently scoping to nothing.
*
* The geocode + admin-boundary rung is capped at `budgetMs`. Past it the
* call returns `{name, ring: null, error: 'region-timeout'}` so a voice
* answer is not held for tens of seconds; the lookup keeps running and
* fills the boundary cache, so asking again shortly is fast.
*
* @param {string} name e.g. "Texas", "the Alps", "France", "Gulf of Mexico"
* @param {AbortSignal} [signal]
* @returns {Promise<{name:string, ring:Array<[number,number]>}|null>}
* @param {object} [placeSearch]
* @param {{budgetMs?: number}} [options] `Infinity` waits for the lookup.
* @returns {Promise<{name:string, ring:Array<[number,number]>}|{name:string, ring:null, error:'region-timeout'}|null>}
*/
async function resolveRegionRingForQuery(
name,
signal,
placeSearch = unavailablePlaceSearch,
{ budgetMs = REGION_FALLBACK_BUDGET_MS } = {},
) {
signal = lifetime
? signal
@@ -1901,6 +1912,23 @@ export function createAnnotationResolver({
const ring = [...ne.polygons].sort((a, b) => b.length - a.length)[0];
if (ring?.length >= 3) return { name: ne.name, ring };
}
const lookup = resolveAdminRegionRing(q, signal, placeSearch);
if (!Number.isFinite(budgetMs)) return lookup;
let timer;
const timeout = new Promise((resolve) => {
timer = setTimeout(
() => resolve({ name: q, ring: null, error: 'region-timeout' }),
budgetMs,
);
});
try {
return await Promise.race([lookup, timeout]);
} finally {
clearTimeout(timer);
}
}
async function resolveAdminRegionRing(q, signal, placeSearch) {
const geo = await geocodePlace(q, null, signal, placeSearch).catch(
() => null,
);
+189
View File
@@ -0,0 +1,189 @@
/**
* Workaround for Apple's Metal/ANGLE shader translator rejecting Cesium's
* per-vertex model atmosphere.
*
* Cesium's `AtmosphereStageVS` calls a function whose `out` parameters are
* bound DIRECTLY to varyings:
*
* czm_computeGroundAtmosphereScattering(
* v_positionWC, lightDirection,
* v_atmosphereRayleighColor, v_atmosphereMieColor, v_atmosphereOpacity);
*
* ANGLE's Metal backend wraps each `out` argument in an `ANGLE_out(...)`
* helper that takes a `thread T &`. Vertex-output struct members live in
* `__metal_generic`, which cannot bind to `thread`, so the program fails to
* LINK (not compile) and Cesium tears the render loop down:
*
* RuntimeError: Program failed to link. ... MSL compilation error:
* reference to type 'thread float3' could not bind to an lvalue of type
* '__metal_generic float3'
*
* This is the only shader in Cesium that binds varyings to `out` params --
* SkyAtmosphere and the ground-atmosphere fragment path both route through
* locals first, so they are unaffected and stay on.
*
* Cesium attaches that stage in `ModelSceneGraph.configurePipeline` under a
* single condition:
*
* const fogRenderable = frameState.fog.enabled && frameState.fog.renderable;
* if (fogRenderable) modelPipelineStages.push(AtmospherePipelineStage);
*
* so clearing `scene.fog.renderable` keeps the broken stage out of the
* pipeline entirely -- the program is never built, so it cannot fail to link.
*
* We clear `renderable` rather than `enabled` deliberately:
* - `Fog.update` still computes fog density when only `renderable` is off,
* preserving the distance-based screen-space-error scaling that governs
* 3D Tiles refinement. Clearing `enabled` returns early and forfeits it.
* - `visualSettings.js` saves and restores `scene.fog.enabled` around IR
* boost. Parking our fix on `enabled` would let that restore resurrect
* the broken shader mid-session; `renderable` is orthogonal to it.
*
* @module app/atmosphereCompat
*/
/** Minimal vertex shader reproducing the exact out-param-to-varying bind. */
const PROBE_VERTEX_SHADER = `#version 300 es
precision highp float;
in vec3 a_position;
out vec3 v_probe;
void writeOut(out vec3 value) { value = vec3(1.0); }
void main() {
writeOut(v_probe);
gl_Position = vec4(a_position, 1.0);
}`;
const PROBE_FRAGMENT_SHADER = `#version 300 es
precision highp float;
in vec3 v_probe;
out vec4 fragColor;
void main() { fragColor = vec4(v_probe, 1.0); }`;
/**
* True when the user agent is iOS or iPadOS.
*
* iPadOS 13+ defaults to desktop-mode browsing and reports `MacIntel` with no
* iPad token, so it is identified by touch points instead -- real Macs report
* `maxTouchPoints` of 0, including Apple Silicon ones that also claim
* `MacIntel`. Brave/Chrome/Firefox on iOS are all WebKit underneath and share
* the same Metal backend, so matching the platform rather than the browser is
* what we want.
*/
export function isAppleMobilePlatform(navigatorLike = globalThis.navigator) {
if (!navigatorLike) return false;
const ua = String(navigatorLike.userAgent || '');
if (/\b(iPad|iPhone|iPod)\b/.test(ua)) return true;
// Desktop-mode iPadOS presents as a Mac. What separates it from a real Mac
// is touch: Macs report 0 touch points, including Apple Silicon ones that
// also claim `MacIntel`. `navigator.platform` is deprecated and may be
// empty, so the UA's Macintosh token is accepted as an equal signal rather
// than relying on platform alone.
const platform = String(navigatorLike.platform || '');
const macLike =
platform === 'MacIntel' ||
platform === 'MacARM' ||
/\bMacintosh\b/.test(ua);
const touchPoints = Number(navigatorLike.maxTouchPoints) || 0;
return macLike && touchPoints > 1;
}
/**
* Link the probe program and report whether the driver rejects the pattern.
*
* Returns `false` when WebGL2 is unavailable so that a missing context is
* never mistaken for a broken driver -- Cesium has its own handling for that,
* and the caller still falls back to platform detection.
*/
export function probeOutParamVaryingLinkFailure(gl) {
if (!gl) return false;
const vs = gl.createShader(gl.VERTEX_SHADER);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
const program = gl.createProgram();
if (!vs || !fs || !program) return false;
try {
gl.shaderSource(vs, PROBE_VERTEX_SHADER);
gl.compileShader(vs);
gl.shaderSource(fs, PROBE_FRAGMENT_SHADER);
gl.compileShader(fs);
// A compile failure here is not the bug we are probing for: the driver
// rejects this pattern at LINK time, after both stages compile cleanly.
if (
!gl.getShaderParameter(vs, gl.COMPILE_STATUS) ||
!gl.getShaderParameter(fs, gl.COMPILE_STATUS)
)
return false;
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
return !gl.getProgramParameter(program, gl.LINK_STATUS);
} catch {
return false;
} finally {
gl.deleteShader(vs);
gl.deleteShader(fs);
gl.deleteProgram(program);
}
}
/** Build a throwaway WebGL2 context for the probe, or null if unavailable. */
function defaultProbeContext() {
try {
const canvas =
typeof OffscreenCanvas === 'function'
? new OffscreenCanvas(1, 1)
: globalThis.document?.createElement('canvas');
return canvas?.getContext('webgl2') ?? null;
} catch {
return null;
}
}
/**
* Decide whether the per-vertex model atmosphere must be kept out of the
* pipeline on this device.
*
* The probe is authoritative when it fires, so a future driver fix silently
* restores the effect without a code change. Platform detection is the
* backstop for the case where no context can be created -- on Apple mobile a
* link failure is fatal and unrecoverable, so we would rather lose fog than
* gamble the whole render loop on the probe having run.
*/
export function shouldDisableModelAtmosphere({
navigatorLike = globalThis.navigator,
createProbeContext = defaultProbeContext,
} = {}) {
const gl = createProbeContext();
if (gl) {
let linkFails;
try {
linkFails = probeOutParamVaryingLinkFailure(gl);
} finally {
// Browsers cap live WebGL contexts (Safari especially); release the
// throwaway probe context now rather than waiting for GC.
try {
gl.getExtension?.('WEBGL_lose_context')?.loseContext();
} catch {
// A context that cannot be released is left to GC.
}
}
if (linkFails) return true;
// Probe ran and the driver accepted the pattern. Apple mobile is still
// quarantined: the probe program is far simpler than Cesium's, and a
// false negative there costs the entire scene.
return isAppleMobilePlatform(navigatorLike);
}
return isAppleMobilePlatform(navigatorLike);
}
/**
* Keep Cesium's broken model-atmosphere stage out of the pipeline.
*
* Returns true when the workaround was applied. Safe to call on any scene;
* scenes without a fog object are left untouched.
*/
export function applyModelAtmosphereWorkaround(scene, options = {}) {
if (!scene?.fog) return false;
if (!shouldDisableModelAtmosphere(options)) return false;
scene.fog.renderable = false;
return true;
}
+221
View File
@@ -0,0 +1,221 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
isAppleMobilePlatform,
probeOutParamVaryingLinkFailure,
shouldDisableModelAtmosphere,
applyModelAtmosphereWorkaround,
} from './atmosphereCompat.js';
const IPAD_OS12 = {
userAgent:
'Mozilla/5.0 (iPad; CPU OS 12_5 like Mac OS X) AppleWebKit/605.1.15',
platform: 'iPad',
maxTouchPoints: 5,
};
// iPadOS 13+ desktop mode: no iPad token, reports as a Mac.
const IPAD_DESKTOP_MODE = {
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15',
platform: 'MacIntel',
maxTouchPoints: 5,
};
const MAC_DESKTOP = {
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15',
platform: 'MacIntel',
maxTouchPoints: 0,
};
const LINUX_DESKTOP = {
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/141',
platform: 'Linux x86_64',
maxTouchPoints: 0,
};
/** Minimal GL double. `linkOk: false` mimics the Metal translator's refusal. */
function fakeGl({ linkOk = true, compileOk = true, throwOn = null } = {}) {
const deleted = { shaders: 0, programs: 0 };
return {
deleted,
VERTEX_SHADER: 1,
FRAGMENT_SHADER: 2,
COMPILE_STATUS: 3,
LINK_STATUS: 4,
createShader: () => ({}),
createProgram: () => ({}),
shaderSource() {
if (throwOn === 'shaderSource') throw new Error('context lost');
},
compileShader() {},
attachShader() {},
linkProgram() {},
getShaderParameter: () => compileOk,
getProgramParameter: () => linkOk,
deleteShader: () => (deleted.shaders += 1),
deleteProgram: () => (deleted.programs += 1),
};
}
test('identifies iPadOS in both legacy and desktop-mode guises', () => {
assert.equal(isAppleMobilePlatform(IPAD_OS12), true);
assert.equal(isAppleMobilePlatform(IPAD_DESKTOP_MODE), true);
});
test('does not mistake a real Mac or a Linux desktop for Apple mobile', () => {
// Regression guard: the desktop-mode check keys on touch points, and a Mac
// reports the same MacIntel platform string with zero of them.
assert.equal(isAppleMobilePlatform(MAC_DESKTOP), false);
assert.equal(isAppleMobilePlatform(LINUX_DESKTOP), false);
});
test('treats a missing navigator as not-Apple rather than throwing', () => {
assert.equal(isAppleMobilePlatform(undefined), false);
assert.equal(isAppleMobilePlatform(null), false);
});
test('detects desktop-mode iPadOS without navigator.platform', () => {
// `navigator.platform` is deprecated and can be absent or stripped; the UA's
// Macintosh token plus touch points has to carry the detection on its own.
assert.equal(
isAppleMobilePlatform({
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15',
maxTouchPoints: 5,
}),
true,
);
// Same UA, no touch: a real Mac, which must stay untouched.
assert.equal(
isAppleMobilePlatform({
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15',
maxTouchPoints: 0,
}),
false,
);
});
test('probe reports failure only when the program fails to link', () => {
assert.equal(
probeOutParamVaryingLinkFailure(fakeGl({ linkOk: false })),
true,
);
assert.equal(
probeOutParamVaryingLinkFailure(fakeGl({ linkOk: true })),
false,
);
});
test('a compile failure is not reported as the link bug', () => {
// The Metal defect surfaces at link time; both stages compile cleanly.
// Treating a compile error as the bug would disable fog for the wrong reason.
assert.equal(
probeOutParamVaryingLinkFailure(
fakeGl({ compileOk: false, linkOk: false }),
),
false,
);
});
test('probe releases its GL objects on both paths', () => {
const ok = fakeGl({ linkOk: true });
probeOutParamVaryingLinkFailure(ok);
assert.equal(ok.deleted.shaders, 2);
assert.equal(ok.deleted.programs, 1);
const thrown = fakeGl({ throwOn: 'shaderSource' });
probeOutParamVaryingLinkFailure(thrown);
assert.equal(thrown.deleted.shaders, 2, 'shaders freed even when GL throws');
assert.equal(thrown.deleted.programs, 1);
});
test('probe treats a null context as healthy, not broken', () => {
assert.equal(probeOutParamVaryingLinkFailure(null), false);
});
test('a failing probe disables the stage on any platform', () => {
assert.equal(
shouldDisableModelAtmosphere({
navigatorLike: LINUX_DESKTOP,
createProbeContext: () => fakeGl({ linkOk: false }),
}),
true,
);
});
test('a passing probe still quarantines Apple mobile but frees desktops', () => {
const createProbeContext = () => fakeGl({ linkOk: true });
assert.equal(
shouldDisableModelAtmosphere({
navigatorLike: IPAD_DESKTOP_MODE,
createProbeContext,
}),
true,
);
assert.equal(
shouldDisableModelAtmosphere({
navigatorLike: LINUX_DESKTOP,
createProbeContext,
}),
false,
);
});
test('falls back to platform detection when no context can be made', () => {
const createProbeContext = () => null;
assert.equal(
shouldDisableModelAtmosphere({
navigatorLike: IPAD_OS12,
createProbeContext,
}),
true,
);
assert.equal(
shouldDisableModelAtmosphere({
navigatorLike: MAC_DESKTOP,
createProbeContext,
}),
false,
);
});
test('applies to the scene by clearing renderable, never enabled', () => {
// `enabled` must survive: Fog.update short-circuits when it is false, which
// forfeits the density used for 3D Tiles screen-space-error scaling, and
// visualSettings' IR boost saves/restores that same flag.
const scene = { fog: { enabled: true, renderable: true } };
const applied = applyModelAtmosphereWorkaround(scene, {
navigatorLike: IPAD_DESKTOP_MODE,
createProbeContext: () => null,
});
assert.equal(applied, true);
assert.equal(scene.fog.renderable, false);
assert.equal(scene.fog.enabled, true);
});
test('leaves unaffected devices completely untouched', () => {
const scene = { fog: { enabled: true, renderable: true } };
const applied = applyModelAtmosphereWorkaround(scene, {
navigatorLike: LINUX_DESKTOP,
createProbeContext: () => fakeGl({ linkOk: true }),
});
assert.equal(applied, false);
assert.equal(scene.fog.renderable, true);
});
test('tolerates a scene with no fog object', () => {
assert.equal(applyModelAtmosphereWorkaround({}, {}), false);
assert.equal(applyModelAtmosphereWorkaround(null, {}), false);
});
test('releases the throwaway probe context after probing', () => {
let lost = 0;
const gl = fakeGl({ linkOk: true });
gl.getExtension = (name) =>
name === 'WEBGL_lose_context' ? { loseContext: () => (lost += 1) } : null;
shouldDisableModelAtmosphere({
navigatorLike: LINUX_DESKTOP,
createProbeContext: () => gl,
});
assert.equal(lost, 1, 'probe context must not linger until GC');
});
+5
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import { applyModelAtmosphereWorkaround } from './atmosphereCompat.js';
const PINCH_ZOOM_MULTIPLIER = 8;
const MAX_PINCH_PIXEL_DELTA = 120;
@@ -124,6 +125,10 @@ export function createApplicationViewer({ container, creditContainer }) {
});
try {
viewer.targetFrameRate = 60;
// Before any tile builds a draw command: Cesium's per-vertex model
// atmosphere fails to LINK on Apple's Metal backend and kills the
// render loop. See app/atmosphereCompat.js.
applyModelAtmosphereWorkaround(viewer.scene);
viewer.scene.globe.show = false;
viewer.scene.skyAtmosphere.show = true;
viewer.scene.skyAtmosphere.atmosphereLightIntensity = 18;
+1 -1
View File
@@ -22,7 +22,7 @@ test('no browser-built module imports a Node core module', () => {
// import survives the build and turns into a runtime failure the moment the
// guard around it is wrong. src/data/naturalEarthRegions.js and
// src/data/neighborhoodPolygons.js both carried one to read their bundled
// JSON packs under node:test; an import attribute serves both runtimes.
// JSON packs under node:test; src/data/bundledJson.js now serves both runtimes.
const offenders = [];
for (const file of browserModules()) {
const source = readFileSync(file, 'utf8');
+9 -1
View File
@@ -18,7 +18,7 @@
*
* Providers (injected — keeps the engine pure and node-testable):
* getRecords(layerKey) → Array<record> (layer accessor snapshot)
* resolveRegionRing(name) → Promise<{ring, name}|null> (NE pack / admin boundary)
* resolveRegionRing(name) → Promise<{ring, name}|{error:'region-timeout'}|null>
* getViewContext() → {lat, lon, viewRadiusKm, bounds?} (camera-derived)
*
* @module data/analystEngine
@@ -234,6 +234,14 @@ export function createAnalystEngine(providers) {
const scope = spec.scope || { kind: 'view' };
if (scope.kind === 'region' && scope.name) {
const region = await providers.resolveRegionRing(scope.name);
if (region?.error === 'region-timeout') {
return {
ok: false,
code: 'region-timeout',
error: `Looking up the boundary for "${scope.name}" is taking too long — ask again in a moment.`,
coverage: { layersQueried, scope: `region:${scope.name}:timeout` },
};
}
if (!region?.ring) {
return {
ok: false,
+13
View File
@@ -187,6 +187,19 @@ test('analyst: unresolved region is an honest failure, not empty success', async
assert.match(r.error, /Atlantis/);
});
test('analyst: a region lookup timeout is reported as region-timeout', async () => {
const eng = createAnalystEngine({
getRecords: () => FLIGHTS,
resolveRegionRing: async (name) => ({ name, ring: null, error: 'region-timeout' }),
getViewContext: () => ({ lat: 30.27, lon: -97.74, viewRadiusKm: 150 }),
});
const r = await eng.query({ layers: ['flights'], scope: { kind: 'region', name: 'Texas' } });
assert.equal(r.ok, false);
assert.equal(r.code, 'region-timeout');
assert.match(r.error, /Texas/);
assert.equal(r.coverage.scope, 'region:Texas:timeout');
});
test('analyst: route fields queryable from cached enrichment only', async () => {
const r = await makeEngine().query({
layers: ['flights'], scope: { kind: 'anywhere' },
+29
View File
@@ -0,0 +1,29 @@
/**
* @file Load a JSON file shipped beside the source modules.
*
* The browser fetches the file from `url` — a `new URL('./….json',
* import.meta.url)` written in the calling module, which Vite serves as-is in
* dev and emits as an asset in the production build. Under Node (node:test)
* that URL is a `file:` URL, which `fetch` cannot read, so the caller's
* JSON-attributed import runs instead.
*
* The attributed import cannot serve the browser: the Vite dev server answers
* it with a JavaScript module, which the browser rejects for a JSON import.
*
* @module data/bundledJson
*/
/**
* @param {URL} url - Resolved against the calling module's `import.meta.url`.
* @param {() => Promise<{default: any}>} importJson - The caller's literal
* `import('./….json', { with: { type: 'json' } })`, used only under Node.
* @returns {Promise<any>} The parsed JSON.
*/
export async function loadBundledJson(url, importJson) {
if (url.protocol === 'file:') return (await importJson()).default;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status} for ${url.pathname}`);
}
return response.json();
}
+36
View File
@@ -0,0 +1,36 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { loadBundledJson } from './bundledJson.js';
const MARINE = new URL('./local_data/natural_earth/marine.json', import.meta.url);
const importMarine = () =>
import('./local_data/natural_earth/marine.json', { with: { type: 'json' } });
test('bundled JSON: a file URL uses the JSON import under Node', async (t) => {
const fetchMock = t.mock.method(globalThis, 'fetch');
const pack = await loadBundledJson(MARINE, importMarine);
assert.ok(pack.features.some((ft) => ft.name === 'Gulf of Mexico'));
assert.equal(fetchMock.mock.callCount(), 0);
});
test('bundled JSON: a served URL is fetched as plain JSON', async (t) => {
const requests = [];
t.mock.method(globalThis, 'fetch', async (url) => {
requests.push(String(url));
return new Response('{"features":[]}', {
headers: { 'content-type': 'application/json' },
});
});
const url = new URL('http://localhost/src/data/local_data/natural_earth/marine.json');
const importJson = () => assert.fail('the browser path must not import JSON');
assert.deepEqual(await loadBundledJson(url, importJson), { features: [] });
assert.deepEqual(requests, [url.href]);
});
test('bundled JSON: an HTTP error rejects so the retryable loader can retry', async (t) => {
t.mock.method(globalThis, 'fetch', async () => new Response('', { status: 404 }));
await assert.rejects(
loadBundledJson(new URL('http://localhost/missing.json'), importMarine),
/HTTP 404/,
);
});
+22 -19
View File
@@ -11,11 +11,12 @@
*
* PURE data module — no Cesium imports, node-testable. The packs are lazy-
* loaded on first lookup and cached in module scope (bbox/area computed once
* at load). In the browser Vite bundles the JSON via dynamic import; under
* node the same files are read from disk. A failed load is retried on the
* next lookup rather than cached (see `createRetryableLoader`).
* at load) through `loadBundledJson`, which works in the browser and under
* node:test. A failed load is retried on the next lookup rather than cached
* (see `createRetryableLoader`).
*/
import { loadBundledJson } from './bundledJson.js';
import { createRetryableLoader } from './retryableLoad.js';
const EARTH_RADIUS_KM = 6371;
@@ -120,20 +121,22 @@ function suffixVariants(norm) {
/** @type {Array|null} flat entry list for listRegions() */
let _entries = null;
async function loadPackFile(base) {
// Vite bundles these JSON files as modules; the import attribute is what Node
// needs to load the same files under node:test (same pattern as
// neighborhoodPolygons.js). One path, so no node: import reaches the browser.
const mod =
base === 'regions'
? await import('./local_data/natural_earth/regions.json', {
with: { type: 'json' },
})
: await import('./local_data/natural_earth/marine.json', {
with: { type: 'json' },
});
return mod.default || mod;
}
const PACKS = {
regions: {
url: new URL('./local_data/natural_earth/regions.json', import.meta.url),
importJson: () =>
import('./local_data/natural_earth/regions.json', {
with: { type: 'json' },
}),
},
marine: {
url: new URL('./local_data/natural_earth/marine.json', import.meta.url),
importJson: () =>
import('./local_data/natural_earth/marine.json', {
with: { type: 'json' },
}),
},
};
function buildEntries(pack, kind) {
const out = [];
@@ -175,8 +178,8 @@ function buildEntries(pack, kind) {
*/
const loadIndex = createRetryableLoader(async () => {
const [regions, marine] = await Promise.all([
loadPackFile('regions'),
loadPackFile('marine'),
loadBundledJson(PACKS.regions.url, PACKS.regions.importJson),
loadBundledJson(PACKS.marine.url, PACKS.marine.importJson),
]);
_entries = [
...buildEntries(regions, 'natural'),
+7 -5
View File
@@ -10,6 +10,7 @@
* touching this module — see `local_data/neighborhoods/SOURCE.md`.
*/
import { loadBundledJson } from './bundledJson.js';
import { createRetryableLoader } from './retryableLoad.js';
// bbox = [west, south, east, north]; only load a city file when the point falls in its box.
@@ -17,7 +18,11 @@ const CITY_FILES = [
{
id: 'san-francisco',
bbox: [-122.55, 37.7, -122.35, 37.84],
loader: () =>
url: new URL(
'./local_data/neighborhoods/san-francisco.json',
import.meta.url,
),
importJson: () =>
import('./local_data/neighborhoods/san-francisco.json', {
with: { type: 'json' },
}),
@@ -107,10 +112,7 @@ function cityLoader(city) {
let loader = _cityLoaders.get(city.id);
if (!loader) {
loader = createRetryableLoader(async () => {
// One path for both runtimes: Vite bundles the JSON as a module, and the
// import attribute is what Node needs to load the same file under node:test.
const mod = await city.loader();
const fc = mod.default || mod;
const fc = await loadBundledJson(city.url, city.importJson);
return Array.isArray(fc.features) ? fc.features : [];
});
_cityLoaders.set(city.id, loader);
+15
View File
@@ -55,6 +55,21 @@ test('macOS ACL removal failure stops before chmod and fails closed', () => {
assert.deepEqual(fileSystem.calls, [], 'mode bits must not disguise an ACL-removal failure');
});
test('macOS ACL removal spawns Apple /bin/chmod by absolute path, not via PATH', () => {
const spawned = [];
hardenCredentialFile(FILE, {
platform: 'darwin',
fileSystem: fileSystemWithMode(0o100600),
spawn(command, args) {
spawned.push([command, args]);
return { status: 0, signal: null };
},
});
// A coreutils-first PATH (Nix home-manager, Homebrew) resolves bare `chmod`
// to GNU chmod, which has no `-N` and would fail every save.
assert.deepEqual(spawned, [['/bin/chmod', ['-N', FILE]]]);
});
test('POSIX hardening verifies the resulting 0600 mode', () => {
const goodFs = fileSystemWithMode(0o100600);
assert.equal(hardenCredentialFile(FILE, {
+2
View File
@@ -48,6 +48,8 @@ export function createStandaloneApplication({
'Nepal',
signal,
placeSearch,
// The locator draws the border whenever it arrives.
{ budgetMs: Infinity },
),
signal: context.signal,
surface: scene.operations.surface,
+1
View File
@@ -4323,6 +4323,7 @@ async function runAnalystQuery(
return {
ok: false,
action: 'analyst_query',
...(result.code ? { code: result.code } : {}),
error: result.error,
coverage: result.coverage,
};