mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge remote-tracking branch 'upstream/main' into fix/houston-cctv-pack
# Conflicts: # docs/CURRENT-STATE.md
This commit is contained in:
178 files changed
+20385
-1524
No files matched your search
@@ -57,6 +57,9 @@
|
||||
"src/app/mapLifecycle.test.mjs",
|
||||
"src/app/startupCamera.test.mjs",
|
||||
"src/app/viewer.js",
|
||||
"src/app/zoomOutGuard.test.mjs",
|
||||
"src/app/controls.test.mjs",
|
||||
"scripts/qa-zoom-out-guard.mjs",
|
||||
"src/app/atmosphereCompat.test.mjs",
|
||||
"src/standalone/application.js",
|
||||
"src/standalone/controls.js",
|
||||
@@ -1103,6 +1106,7 @@
|
||||
"scripts/qa-anime-sky.mjs",
|
||||
"scripts/qa-sky-model-lighting.mjs",
|
||||
"server/providers/overpass/requestAdmission.test.mjs",
|
||||
"src/tooling/trackArrivalObservation.mjs",
|
||||
"src/tooling/trackArrivalObservation.test.mjs",
|
||||
"src/ui/styles/status-badge.css",
|
||||
"src/ui/cctvBadges.test.mjs",
|
||||
@@ -1112,6 +1116,9 @@
|
||||
"src/voice/globeResetConfirmation.test.mjs",
|
||||
"src/hudDms.test.mjs",
|
||||
"src/hudSummaryGate.test.mjs",
|
||||
"src/hudSummaryLine.test.mjs",
|
||||
"src/testSupport/stubMgrs.mjs",
|
||||
"src/tools/queries/hud-caption-feed-state.test.mjs",
|
||||
"src/voice/hudPlaceLabels.test.mjs",
|
||||
"src/voice/panel/preferences.test.mjs",
|
||||
"src/voice/panel/preferencesView.test.mjs",
|
||||
@@ -1204,5 +1211,18 @@
|
||||
"src/standalone/layerSources.test.mjs",
|
||||
"src/tooling/nominatimGuardLifecycle.test.mjs",
|
||||
"src/tooling/nominatimLockOwnership.test.mjs",
|
||||
"src/tooling/nominatimOutlineRoute.test.mjs"
|
||||
"src/tooling/nominatimOutlineRoute.test.mjs",
|
||||
"server/providers/common/rateLimitKeys.test.mjs",
|
||||
"src/voice/areaCandidateContract.js",
|
||||
"src/voice/layerActionOwnership.js",
|
||||
"src/data/areaGeometry.test.mjs",
|
||||
"src/data/areaResolver.test.mjs",
|
||||
"src/sources/osmFeatures.test.mjs",
|
||||
"src/layers/osmPlaces/index.test.mjs",
|
||||
"src/tooling/boundedCache.test.mjs",
|
||||
"src/tooling/osmFeaturesRoute.test.mjs",
|
||||
"src/voice/areaActions.test.mjs",
|
||||
"src/voice/imageryActions.test.mjs",
|
||||
"src/voice/osmActions.test.mjs",
|
||||
"src/voice/realtimeProtocol.test.mjs"
|
||||
]
|
||||
@@ -416,7 +416,11 @@
|
||||
"src/sources/httpBody.js",
|
||||
"src/i18n/marker.js",
|
||||
"src/sources/overpass.js",
|
||||
"src/sources/rateLimit.js"
|
||||
"src/sources/rateLimit.js",
|
||||
"server/providers/common/boundedCache.js",
|
||||
"src/data/osmPresets.js",
|
||||
"server/providers/overpass/osmFeatures.js",
|
||||
"server/providers/overpass/osmFeaturesRoute.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -485,6 +489,7 @@
|
||||
"exports": ["./server/providers/openai"],
|
||||
"modules": [
|
||||
"src/i18n/marker.js",
|
||||
"src/i18n/locale.js",
|
||||
"src/hudLayoutPolicy.js",
|
||||
"server/providers/common/rate-limit.js",
|
||||
"server/providers/common/request.js",
|
||||
@@ -507,9 +512,11 @@
|
||||
"server/providers/common/same-site.js",
|
||||
"src/localRequestGate.mjs",
|
||||
"src/voice/actionSchemas.js",
|
||||
"src/voice/areaCandidateContract.js",
|
||||
"src/voice/layerManifest.js",
|
||||
"server/providers/openai/toolDescriptions.js",
|
||||
"src/sources/rateLimit.js"
|
||||
"src/sources/rateLimit.js",
|
||||
"server/providers/overpass/constants.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -2273,6 +2280,7 @@
|
||||
"src/app/layers/traffic.js",
|
||||
"src/app/operations.js",
|
||||
"src/app/scene.js",
|
||||
"src/app/zoomOutGuard.js",
|
||||
"src/sources/sourceSlot.js",
|
||||
"src/app/sources.js",
|
||||
"src/app/startupChrome.js",
|
||||
@@ -2352,6 +2360,7 @@
|
||||
"src/data/groundSnap.js",
|
||||
"src/data/iconOrientation.js",
|
||||
"src/data/infrastructure.js",
|
||||
"src/data/osmOverlays.js",
|
||||
"src/data/installationFeedback.js",
|
||||
"src/data/labelArbiter.js",
|
||||
"src/data/idSwitches.js",
|
||||
@@ -3182,7 +3191,20 @@
|
||||
"src/data/placeBoundaries.js",
|
||||
"src/data/placeNames.js",
|
||||
"src/data/local_data/us_census_places/files.js",
|
||||
"src/data/local_data/wof_neighborhoods/files.js"
|
||||
"src/data/local_data/wof_neighborhoods/files.js",
|
||||
"src/sources/osmFeatures.js",
|
||||
"src/voice/imageryActions.js",
|
||||
"src/voice/layerActionOwnership.js",
|
||||
"src/data/areaGeometry.js",
|
||||
"src/layers/osmPlaces/model.js",
|
||||
"src/data/areaStore.js",
|
||||
"src/data/areaResolver.js",
|
||||
"src/voice/areaCandidateContract.js",
|
||||
"src/data/osmPresets.js",
|
||||
"src/voice/osmActions.js",
|
||||
"src/layers/osmPlaces/index.js",
|
||||
"src/app/layers/osmPlaces.js",
|
||||
"src/voice/areaActions.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -3282,6 +3304,7 @@
|
||||
"src/data/geoid.js",
|
||||
"src/data/iconOrientation.js",
|
||||
"src/data/infrastructure.js",
|
||||
"src/data/osmOverlays.js",
|
||||
"src/data/installationFeedback.js",
|
||||
"src/data/satellitePass.js",
|
||||
"src/data/labelArbiter.js",
|
||||
@@ -3789,7 +3812,10 @@
|
||||
"src/data/placeBoundaries.js",
|
||||
"src/data/placeNames.js",
|
||||
"src/data/local_data/us_census_places/files.js",
|
||||
"src/data/local_data/wof_neighborhoods/files.js"
|
||||
"src/data/local_data/wof_neighborhoods/files.js",
|
||||
"src/layers/osmPlaces/model.js",
|
||||
"src/layers/osmPlaces/index.js",
|
||||
"src/app/layers/osmPlaces.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
@@ -4154,6 +4180,7 @@
|
||||
"exports": ["./voice/action-schemas"],
|
||||
"modules": [
|
||||
"src/voice/actionSchemas.js",
|
||||
"src/voice/areaCandidateContract.js",
|
||||
"src/voice/layerManifest.js",
|
||||
"src/hudLayoutPolicy.js"
|
||||
],
|
||||
|
||||
+20
-3
@@ -3073,20 +3073,27 @@ async function main() {
|
||||
expectedSlot: targetIndex,
|
||||
previewPlayDelta: previewPlays - playsBefore,
|
||||
commitPlayDelta: window.__qaRadioPlayCalls.length - playsBefore,
|
||||
audioState: state.audioState,
|
||||
tuningStatic: state.tuningStatic,
|
||||
samples,
|
||||
spread: Math.max(...samples) - Math.min(...samples),
|
||||
};
|
||||
});
|
||||
check(
|
||||
'pointer preview never plays and release commits the nearest station once without a needle sweep',
|
||||
'from a stopped Radio, pointer release only selects the nearest station, silently and without a needle sweep',
|
||||
tunerDirectRelease.targetId
|
||||
&& tunerDirectRelease.selectedId === tunerDirectRelease.targetId
|
||||
&& tunerDirectRelease.sliderSlot === tunerDirectRelease.expectedSlot
|
||||
&& tunerDirectRelease.previewPlayDelta === 0
|
||||
&& tunerDirectRelease.commitPlayDelta === 1
|
||||
&& tunerDirectRelease.commitPlayDelta === 0
|
||||
&& tunerDirectRelease.audioState === 'stopped'
|
||||
&& !tunerDirectRelease.tuningStatic
|
||||
&& tunerDirectRelease.spread < 0.5,
|
||||
JSON.stringify(tunerDirectRelease),
|
||||
);
|
||||
// The release plays only if a stream was active when the drag began.
|
||||
await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('radio').module.togglePlayback({ origin: 'user' }));
|
||||
await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing');
|
||||
// Earlier cases scroll and resize the directory. Bring the physical drag
|
||||
// target back into its scroller before deriving viewport mouse coordinates.
|
||||
await page.$eval('#radio-tuner-slider', (slider) => slider.scrollIntoView({ block: 'center', inline: 'nearest' }));
|
||||
@@ -3494,6 +3501,9 @@ async function main() {
|
||||
});
|
||||
});
|
||||
await sleep(500);
|
||||
// Next left the Radio stopped; a release plays only from an active stream.
|
||||
await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('radio').module.togglePlayback({ origin: 'user' }));
|
||||
await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing');
|
||||
const failedTuner = await page.evaluate(async () => {
|
||||
const gev = window.__godsEyeView;
|
||||
const slider = document.getElementById('radio-tuner-slider');
|
||||
@@ -3692,7 +3702,14 @@ async function main() {
|
||||
radio.stopPlayback();
|
||||
Array.from({ length: gev.viewer.dataSources.length }, (_, index) => gev.viewer.dataSources.get(index))
|
||||
.find((item) => item.name === 'Radio stations').clustering.enabled = true;
|
||||
radio.selectStation('00000000-0000-4000-8000-000000000001', { autoplay: false, focus: false });
|
||||
// Next plays only while a stream is active, so start from a playing station.
|
||||
radio.selectStation('00000000-0000-4000-8000-000000000001', { autoplay: true, focus: false });
|
||||
});
|
||||
await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing');
|
||||
await page.evaluate(() => {
|
||||
const gev = window.__godsEyeView;
|
||||
const camera = gev.viewer.camera;
|
||||
const radio = gev.dataManager.layers.get('radio').module;
|
||||
const pool = gev.styleManager._radioControls._radioTunerPool;
|
||||
const selectedIndex = pool.findIndex((station) => station.id === radio.getUIState().selected?.id);
|
||||
window.__qaRadioExpectedPrimary = pool[(selectedIndex + 1) % pool.length];
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
* node scripts/qa-voice-routing.mjs --layer behavior --deixis-only --headful --shots qa-shots/point-and-ask
|
||||
* node scripts/qa-voice-routing.mjs --layer deixis --budget 12 # pointer_context routing (model turns)
|
||||
* node scripts/qa-voice-routing.mjs --layer deixis-live --url ... # in-app text turns (model turns)
|
||||
* node scripts/qa-voice-routing.mjs --layer behavior --behavior geometry # areas, drawn counts, imagery
|
||||
*
|
||||
* House gotchas honored: camera.cancelFlight() before every teleport; puppeteer
|
||||
* suites must run sequentially with other harnesses (SwiftShader saturation);
|
||||
@@ -75,7 +76,7 @@ const LAYER = getOpt('--layer', 'all'); // routing | behavior | all | deixis | d
|
||||
const TURN_BUDGET = Number(getOpt('--budget', '120'));
|
||||
const PHRASES_PER_SESSION = Number(getOpt('--batch', '6'));
|
||||
const ONLY = getOpt('--only', null); // substring filter on phrase text
|
||||
const BEHAVIOR = getOpt('--behavior', 'all'); // all | outlines | acceptance
|
||||
const BEHAVIOR = getOpt('--behavior', 'all'); // all | outlines | acceptance | geometry
|
||||
const DEIXIS_ONLY = process.argv.includes('--deixis-only'); // behavior: point-and-ask block only
|
||||
const HEADFUL = process.argv.includes('--headful'); // real GPU, visible window
|
||||
const SHOT_DIR = getOpt('--shots', null); // point-and-ask evidence screenshots
|
||||
@@ -164,7 +165,11 @@ async function mintSession() {
|
||||
throw new Error(
|
||||
`token mint returned no client secret: ${JSON.stringify(body).slice(0, 160)}`,
|
||||
);
|
||||
return { value, model };
|
||||
// The tools this server offered (osm_query only with an operator Overpass).
|
||||
const tools = new Set(
|
||||
(body?.session?.tools || []).map((tool) => tool?.name).filter(Boolean),
|
||||
);
|
||||
return { value, model, tools };
|
||||
}
|
||||
|
||||
/** One Realtime WS session that can run several text turns sequentially. */
|
||||
@@ -371,8 +376,10 @@ async function runRoutingLayer() {
|
||||
}
|
||||
const batch = list.slice(i, i + PHRASES_PER_SESSION);
|
||||
let session;
|
||||
let offered = new Set();
|
||||
try {
|
||||
const { value, model } = await mintSession();
|
||||
const { value, model, tools } = await mintSession();
|
||||
offered = tools;
|
||||
session = new RoutingSession(value, model, evidence);
|
||||
await session.connect();
|
||||
} catch (e) {
|
||||
@@ -390,6 +397,14 @@ async function runRoutingLayer() {
|
||||
skipped(`route: "${p.phrase}"`, 'turn budget exhausted');
|
||||
continue;
|
||||
}
|
||||
const missing = (p.requires || []).filter((name) => !offered.has(name));
|
||||
if (missing.length) {
|
||||
skipped(
|
||||
`route: "${p.phrase}"`,
|
||||
`${missing.join(', ')} not offered (no configured Overpass)`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
turnsUsed += 1;
|
||||
let calls = [];
|
||||
try {
|
||||
@@ -1361,7 +1376,11 @@ async function runBehaviorLayer() {
|
||||
// The point-and-ask block skips the first-run launcher so its evidence
|
||||
// screenshots show the map; the full run keeps the historical URL.
|
||||
const behaviorUrl = new URL(APP_URL);
|
||||
if (DEIXIS_ONLY || BEHAVIOR === 'acceptance')
|
||||
if (
|
||||
DEIXIS_ONLY ||
|
||||
BEHAVIOR === 'acceptance' ||
|
||||
BEHAVIOR === 'geometry'
|
||||
)
|
||||
behaviorUrl.searchParams.set('welcome', '0');
|
||||
await page.goto(behaviorUrl.href, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
@@ -1420,6 +1439,10 @@ async function runBehaviorLayer() {
|
||||
await runOutlineAcceptance({ page, run, settle, overpassRequests });
|
||||
return;
|
||||
}
|
||||
if (BEHAVIOR === 'geometry') {
|
||||
await runGeometryScenarios({ page, run, settle, overpassRequests });
|
||||
return;
|
||||
}
|
||||
|
||||
// (1) fly_to_location lands near the target. Annotations follow IMMEDIATELY
|
||||
// while the camera is local — the resolver's proximity gate rejects far
|
||||
@@ -2340,6 +2363,8 @@ async function runBehaviorLayer() {
|
||||
resolverLog,
|
||||
packTimes,
|
||||
});
|
||||
// Voice geometry last: it moves the camera and turns layers off.
|
||||
await runGeometryScenarios({ page, run, settle, overpassRequests });
|
||||
|
||||
const shotDir = path.join(ROOT, 'qa-shots');
|
||||
fs.mkdirSync(shotDir, { recursive: true });
|
||||
@@ -2483,6 +2508,193 @@ const OUTLINE_ASKS = [
|
||||
{ entityKind: 'building', latitude: 30.27472, longitude: -97.74035 }, 'building'],
|
||||
].map(([group, target, lat, lon, height, extra = {}, shot = null]) => ({ group, target, lat, lon, height, extra, shot }));
|
||||
|
||||
/**
|
||||
* Voice geometry, runner-driven: reusable areas, drawn-area counts, a
|
||||
* landmark area and recent imagery, all without a default Overpass request.
|
||||
* Public Nominatim use is read from the server's daily count.
|
||||
*/
|
||||
async function runGeometryScenarios({ page, run, settle, overpassRequests }) {
|
||||
const shotDir = path.join(ROOT, 'qa-shots', 'pr-geometry');
|
||||
fs.mkdirSync(shotDir, { recursive: true });
|
||||
const shot = (name) => page.screenshot({ path: path.join(shotDir, `${name}.png`) }).catch(() => {});
|
||||
const nominatimStart = nominatimCount();
|
||||
const overpassStart = overpassRequests.length;
|
||||
const teleport = (lat, lon, height, pitch = -90) => page.evaluate(({ lat, lon, height, pitch }) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
longitude: (lon * Math.PI) / 180, latitude: (lat * Math.PI) / 180, height,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (pitch * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
}, { lat, lon, height, pitch });
|
||||
const timings = {};
|
||||
|
||||
// (G1) "Outline Bagmati, then count fires inside it".
|
||||
await teleport(27.7, 85.4, 400_000);
|
||||
await settle(1500);
|
||||
let t0 = Date.now();
|
||||
const bagmati = await run('resolve_area', { query: 'Bagmati province', draw: true, flyTo: false });
|
||||
timings.bagmatiMs = Date.now() - t0;
|
||||
const bagmatiRefused = [
|
||||
'AREA_IDENTITY_MISMATCH',
|
||||
'AREA_IDENTITY_UNVERIFIED',
|
||||
].includes(bagmati?.code);
|
||||
const bagmatiVerified =
|
||||
bagmati?.ok === true &&
|
||||
bagmati?.drawn === true &&
|
||||
bagmati?.level === 'admin1' &&
|
||||
bagmati?.name === 'Bagmati Province' &&
|
||||
bagmati?.sourceIdentity === 'Bagmati Province';
|
||||
report(
|
||||
bagmatiVerified || bagmatiRefused,
|
||||
'geometry: "outline Bagmati province" uses verified current identity or refuses honestly',
|
||||
`areaId=${bagmati?.areaId} name=${bagmati?.name} sourceIdentity=${bagmati?.sourceIdentity} source=${bagmati?.source} km2=${bagmati?.areaKm2} ms=${timings.bagmatiMs} code=${bagmati?.code || ''} ${bagmati?.error || ''}`,
|
||||
);
|
||||
await settle(3000);
|
||||
await shot('bagmati-outline');
|
||||
t0 = Date.now();
|
||||
let r = await run('resolve_area', { query: 'Bagmati province' });
|
||||
report(
|
||||
bagmatiVerified
|
||||
? r?.ok === true &&
|
||||
r?.cached === true &&
|
||||
r?.ms < 300 &&
|
||||
r?.sourceIdentity === 'Bagmati Province'
|
||||
: bagmatiRefused &&
|
||||
r?.ok === false &&
|
||||
r?.code === bagmati?.code &&
|
||||
!r?.areaId,
|
||||
'geometry: a repeated Bagmati request reuses only verified identity, never refused geometry',
|
||||
`ok=${r?.ok} code=${r?.code || ''} areaId=${r?.areaId || ''} sourceIdentity=${r?.sourceIdentity || ''} cached=${r?.cached} ms=${r?.ms} roundTrip=${Date.now() - t0}`,
|
||||
);
|
||||
// A city no bundled pack holds: the guarded Nominatim outline route.
|
||||
t0 = Date.now();
|
||||
const kathmandu = await run('resolve_area', { query: 'Kathmandu', draw: true, flyTo: false });
|
||||
timings.kathmanduMs = Date.now() - t0;
|
||||
report(kathmandu?.ok === true && kathmandu?.drawn === true && kathmandu?.source === 'osm' && kathmandu?.areaKm2 > 10 && kathmandu?.areaKm2 < 2000,
|
||||
'geometry: "outline Kathmandu" comes from the guarded Nominatim outline',
|
||||
`areaId=${kathmandu?.areaId} source=${kathmandu?.source} km2=${kathmandu?.areaKm2} parts=${kathmandu?.parts} ms=${timings.kathmanduMs} err=${kathmandu?.code || ''} ${kathmandu?.error || ''}`);
|
||||
await settle(2500);
|
||||
await shot('kathmandu-outline');
|
||||
r = await run('resolve_area', { query: 'Georgia' });
|
||||
report(r?.ok === false && r?.needsClarification === true && (r?.candidates || []).length >= 2,
|
||||
'geometry: an ambiguous area asks with candidates instead of guessing',
|
||||
`candidates=${(r?.candidates || []).map((c) => `${c.name}/${c.level}/${c.country || ''}`).join(', ')}`,
|
||||
);
|
||||
if (bagmatiVerified) {
|
||||
await run('set_layer_visibility', {
|
||||
layerId: 'local-firms',
|
||||
enabled: true,
|
||||
});
|
||||
let fires = null;
|
||||
for (let attempt = 0; attempt < 10; attempt += 1) {
|
||||
fires = await run('analyst_query', {
|
||||
layers: ['local-firms'],
|
||||
scope: { kind: 'area', areaId: bagmati.areaId },
|
||||
});
|
||||
if (fires?.ok || !['NOT_READY', 'LAYER_OFF'].includes(fires?.code)) break;
|
||||
await settle(4000);
|
||||
}
|
||||
if (fires?.code === 'FEED_UNAVAILABLE') {
|
||||
skipped(
|
||||
'geometry: "count fires inside it" counts inside the verified Bagmati area',
|
||||
`fires feed unavailable: ${fires?.error}`,
|
||||
);
|
||||
} else {
|
||||
report(
|
||||
fires?.ok === true &&
|
||||
Number.isFinite(fires?.count) &&
|
||||
/Bagmati/i.test(fires?.scopeLabel || '') &&
|
||||
fires?.coverage?.scope === `area:${bagmati.areaId}`,
|
||||
'geometry: "count fires inside it" counts inside the verified Bagmati area',
|
||||
`count=${fires?.count} scopeLabel="${fires?.scopeLabel}" scope=${fires?.coverage?.scope} err=${fires?.error || ''}`,
|
||||
);
|
||||
}
|
||||
await run('set_layer_visibility', {
|
||||
layerId: 'local-firms',
|
||||
enabled: false,
|
||||
});
|
||||
} else {
|
||||
skipped(
|
||||
'geometry: fire count waits for a verified Bagmati area',
|
||||
`boundary refused with ${bagmati?.code || 'unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
// (G2) "Count flights inside the area I drew": the draw tool's own spec.
|
||||
await teleport(30.27, -97.74, 600_000);
|
||||
await run('set_layer_visibility', { layerId: 'flights', enabled: true });
|
||||
for (let attempt = 0; attempt < 12; attempt += 1) {
|
||||
r = await run('analyst_query', { layers: ['flights'], scope: { kind: 'anywhere' } });
|
||||
if (r?.ok && r.count > 0) break;
|
||||
await settle(5000);
|
||||
}
|
||||
const drawnBox = [[-99, 29], [-96.5, 29], [-96.5, 31.5], [-99, 31.5], [-99, 29]];
|
||||
await page.evaluate((ring) => window.__gevAnnotations.annotate([{ type: 'area', manual: true, ring, label: 'QA drawn box' }]), drawnBox);
|
||||
r = await run('analyst_query', { layers: ['flights'], scope: { kind: 'drawn' } });
|
||||
const loaded = await run('analyst_query', { layers: ['flights'], scope: { kind: 'anywhere' } });
|
||||
report(r?.ok === true && r?.scopeLabel === 'inside QA drawn box' && Number.isFinite(r?.count) && r.count <= (loaded?.count ?? Infinity),
|
||||
'geometry: "count flights inside the area I drew" counts inside the drawn outline',
|
||||
`count=${r?.count} of ${loaded?.count} loaded scopeLabel="${r?.scopeLabel}" err=${r?.error || ''}`);
|
||||
await settle(2500);
|
||||
await shot('drawn-area-count');
|
||||
// Thousands of aircraft starve a SwiftShader page; the landmark lookup
|
||||
// below has a 10 s budget.
|
||||
await run('set_layer_visibility', { layerId: 'flights', enabled: false });
|
||||
await run('clear_annotations', {});
|
||||
|
||||
// (G3) "Draw an area around the Ferry Building": the enclosing mapped area
|
||||
// (Nominatim grounds, then the OpenFreeMap area holding it), else an
|
||||
// approximate buffer — either way a drawn, reusable area.
|
||||
await teleport(37.7955, -122.3937, 2500, -60);
|
||||
await settle(1500);
|
||||
t0 = Date.now();
|
||||
const ferry = await run('resolve_area', { query: 'Ferry Building, San Francisco', around: true, flyTo: false });
|
||||
timings.ferryMs = Date.now() - t0;
|
||||
const ferryNear = Array.isArray(ferry?.bbox) && Math.abs(ferry.bbox[1] - 37.795) < 0.05 && Math.abs(ferry.bbox[0] + 122.394) < 0.05;
|
||||
report(ferry?.ok === true && ferry?.drawn === true && ferryNear && (ferry?.approximate === true || ['osm', 'openfreemap'].includes(ferry?.source)),
|
||||
'geometry: "draw an area around the Ferry Building" draws a reusable landmark area',
|
||||
`areaId=${ferry?.areaId} source=${ferry?.source} approximate=${ferry?.approximate} basis=${ferry?.display?.basis} km2=${ferry?.areaKm2} ms=${timings.ferryMs} err=${ferry?.error || ''}`);
|
||||
await settle(3000);
|
||||
await shot('ferry-building-area');
|
||||
|
||||
// (G4) "Find recent imagery of this area": the Recent Imagery catalog for
|
||||
// that area, the best acquisition pinned through the layer itself.
|
||||
t0 = Date.now();
|
||||
r = await run('find_imagery', { areaId: ferry?.areaId || 'missing' });
|
||||
timings.imageryMs = Date.now() - t0;
|
||||
const pinned = await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('recent-imagery')?.module?.getSnapshot?.().pins?.a?.key || null);
|
||||
if (r?.code === 'CATALOG_UNAVAILABLE' || r?.code === 'SEARCH_TIMEOUT') {
|
||||
skipped('geometry: "find recent imagery of this area" pins the best acquisition', `catalog: ${r?.error}`);
|
||||
} else {
|
||||
report(r?.ok === true && (r?.count === 0 ? r?.shown === null : Boolean(r?.shown?.date) && String(pinned).endsWith(`:${r.shown.date}`)),
|
||||
'geometry: "find recent imagery of this area" pins the best acquisition',
|
||||
`count=${r?.count} shown=${JSON.stringify(r?.shown)} pinned=${pinned} ms=${timings.imageryMs} err=${r?.error || ''}`);
|
||||
}
|
||||
await settle(4000);
|
||||
await shot('recent-imagery-area');
|
||||
await run('set_layer_visibility', { layerId: 'recent-imagery', enabled: false });
|
||||
|
||||
// (G5) Bulk place search without an operator Overpass: a plain refusal.
|
||||
const configured = await page.evaluate(() => fetch('/api/overpass/status').then((res) => res.json()).catch(() => null));
|
||||
r = await run('osm_query', { what: 'hospitals' });
|
||||
if (configured?.configured) {
|
||||
skipped('geometry: place search without an operator Overpass is refused', 'this server has OVERPASS_UPSTREAMS');
|
||||
} else {
|
||||
report(r?.ok === false && r?.code === 'OVERPASS_NOT_CONFIGURED',
|
||||
'geometry: place search without an operator Overpass is refused', `code=${r?.code} error=${r?.error}`);
|
||||
}
|
||||
|
||||
const nominatimUsed = nominatimCount() - nominatimStart;
|
||||
const overpass = overpassRequests.slice(overpassStart);
|
||||
console.log(` geometry timings: ${JSON.stringify(timings)}; public Nominatim requests: ${nominatimUsed}`);
|
||||
report(overpass.length === 0, 'geometry: no Overpass request', overpass.slice(0, 3).join(' | ') || 'none');
|
||||
report(nominatimUsed <= 15, 'geometry: public Nominatim requests within budget (15)', `used=${nominatimUsed}`);
|
||||
await run('clear_annotations', {});
|
||||
}
|
||||
|
||||
const NOMINATIM_STATE = path.join(ROOT, '.gev-cache', 'nominatim', 'state.json');
|
||||
function nominatimCount() {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env node
|
||||
/** Real-app zoom and tilt regression. Start a dev server, then run this file. */
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const origin = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: [
|
||||
'--use-gl=angle',
|
||||
'--use-angle=swiftshader',
|
||||
'--enable-unsafe-swiftshader',
|
||||
]),
|
||||
],
|
||||
protocolTimeout: 120_000,
|
||||
});
|
||||
const errors = [];
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
await page.setViewport({ width: 1200, height: 800 });
|
||||
await page.goto(`${origin}/?welcome=0`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__godsEyeView?.viewer &&
|
||||
window.__godsEyeView.viewer.scene.screenSpaceCameraController
|
||||
.enableInputs &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
const renderer = await page.evaluate(async () => {
|
||||
const C = await import('/node_modules/cesium/Build/Cesium/index.js');
|
||||
const { maxPitchForHeight } = await import('/src/app/zoomOutGuard.js');
|
||||
const { readCameraTargetFrame } =
|
||||
await import('/src/ui/cameraOrientationControls.js');
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.trackedEntity = undefined;
|
||||
// Explicit real renders make each gesture independent of host frame rate.
|
||||
viewer.useDefaultRenderLoop = false;
|
||||
const controller = viewer.scene.screenSpaceCameraController;
|
||||
controller.inertiaZoom = 0;
|
||||
controller.inertiaSpin = 0;
|
||||
const original = viewer.camera.setView.bind(viewer.camera);
|
||||
const qa = {
|
||||
viewer,
|
||||
guardWrites: 0,
|
||||
targetPitch: () => readCameraTargetFrame(viewer)?.pitch,
|
||||
history: [],
|
||||
frame() {
|
||||
viewer.scene.requestRender();
|
||||
viewer.render();
|
||||
qa.history.push({
|
||||
height: viewer.camera._positionCartographic.height,
|
||||
guardWrites: qa.guardWrites,
|
||||
position: { ...viewer.camera.position },
|
||||
});
|
||||
if (qa.history.length > 80) qa.history.shift();
|
||||
},
|
||||
pose() {
|
||||
return {
|
||||
height: viewer.camera.positionCartographic.height,
|
||||
pitch: viewer.camera.pitch,
|
||||
maxPitch: maxPitchForHeight(
|
||||
viewer.camera.positionCartographic.height,
|
||||
),
|
||||
guardWrites: qa.guardWrites,
|
||||
};
|
||||
},
|
||||
reset(height = 1_000_000, pitch = -35) {
|
||||
viewer.camera.cancelFlight();
|
||||
viewer.camera.lookAtTransform(C.Matrix4.IDENTITY);
|
||||
original({
|
||||
destination: C.Cartesian3.fromDegrees(0, 0, height),
|
||||
orientation: { heading: 0, pitch: C.Math.toRadians(pitch), roll: 0 },
|
||||
});
|
||||
qa.frame();
|
||||
qa.guardWrites = 0;
|
||||
},
|
||||
};
|
||||
viewer.camera.setView = (options) => {
|
||||
// Cesium itself also calls setView while processing touch movement.
|
||||
if (new Error().stack.includes('/src/app/zoomOutGuard.js'))
|
||||
qa.guardWrites++;
|
||||
return original(options);
|
||||
};
|
||||
window.__zoomGuardQa = qa;
|
||||
const gl = viewer.scene.context._gl;
|
||||
const debug = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
return gl.getParameter(debug ? debug.UNMASKED_RENDERER_WEBGL : gl.RENDERER);
|
||||
});
|
||||
console.log(`[INFO] WebGL renderer: ${renderer}`);
|
||||
const frame = (count = 1) =>
|
||||
page.evaluate(async (n) => {
|
||||
for (let i = 0; i < n; i++) {
|
||||
window.__zoomGuardQa.frame();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
return window.__zoomGuardQa.pose();
|
||||
}, count);
|
||||
const reset = (height, pitch) =>
|
||||
page.evaluate((h, p) => window.__zoomGuardQa.reset(h, p), height, pitch);
|
||||
const session = await page.createCDPSession();
|
||||
for (const viewport of [
|
||||
{ width: 1200, height: 800 },
|
||||
{ width: 390, height: 844 },
|
||||
]) {
|
||||
await page.setViewport(viewport);
|
||||
await page.evaluate(() => window.__zoomGuardQa.viewer.resize());
|
||||
const x = Math.round(viewport.width / 2);
|
||||
const y = Math.round(viewport.height / 2);
|
||||
await reset(1_000_000, -25);
|
||||
await page.mouse.move(x, y);
|
||||
await page.evaluate(
|
||||
({ x, y }) => {
|
||||
window.__zoomGuardQa.viewer.canvas.dispatchEvent(
|
||||
new WheelEvent('wheel', {
|
||||
deltaY: 2400,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ x, y },
|
||||
);
|
||||
const first = await frame();
|
||||
console.log(`[INFO] ${viewport.width}px wheel delivered`);
|
||||
const settled = await frame(40);
|
||||
assert.ok(first.height > 1_000_000, 'wheel must move the real camera');
|
||||
assert.ok(settled.guardWrites > 0, 'zoom guard must run');
|
||||
assert.ok(
|
||||
settled.pitch <= settled.maxPitch + 1e-6,
|
||||
`stationary pitch must finish easing: ${JSON.stringify(settled)}`,
|
||||
);
|
||||
for (let i = 0; i < 12; i++) {
|
||||
await page.evaluate(
|
||||
({ x, y }) => {
|
||||
window.__zoomGuardQa.viewer.canvas.dispatchEvent(
|
||||
new WheelEvent('wheel', {
|
||||
deltaY: 2400,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ x, y },
|
||||
);
|
||||
await frame();
|
||||
}
|
||||
const ceiling = await frame(40);
|
||||
assert.ok(ceiling.height <= 40_000_001, 'user zoom respects the ceiling');
|
||||
assert.ok(
|
||||
ceiling.height > 39_000_000,
|
||||
'exercise the ceiling, not a low orbit',
|
||||
);
|
||||
assert.ok(
|
||||
ceiling.pitch <= ceiling.maxPitch + 1e-6,
|
||||
`ceiling must settle: ${JSON.stringify(await page.evaluate(() => window.__zoomGuardQa.history))}`,
|
||||
);
|
||||
console.log(
|
||||
`[PASS] ${viewport.width}px wheel, stationary easing and ceiling`,
|
||||
);
|
||||
if (process.env.QA_SCREENSHOT_DIR) {
|
||||
fs.mkdirSync(process.env.QA_SCREENSHOT_DIR, { recursive: true });
|
||||
await page.screenshot({
|
||||
path: path.join(
|
||||
process.env.QA_SCREENSHOT_DIR,
|
||||
`zoom-ceiling-${viewport.width}.png`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
await reset();
|
||||
await page.mouse.move(x, y);
|
||||
await page.keyboard.down('Control');
|
||||
await page.mouse.down({ button: 'right' });
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
await page.mouse.move(x, y + i * 3);
|
||||
await frame();
|
||||
}
|
||||
await page.mouse.up({ button: 'right' });
|
||||
await page.keyboard.up('Control');
|
||||
const tilted = await frame(3);
|
||||
assert.equal(
|
||||
tilted.guardWrites,
|
||||
0,
|
||||
'Ctrl+right tilt must not trigger guard',
|
||||
);
|
||||
assert.ok(Math.abs(tilted.height - 1_000_000) > 1, 'tilt must move camera');
|
||||
|
||||
await reset();
|
||||
const touches = (dy) => [
|
||||
{ x: x - 60, y: y + dy, id: 1 },
|
||||
{ x: x + 60, y: y + dy, id: 2 },
|
||||
];
|
||||
await session.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchStart',
|
||||
touchPoints: touches(0),
|
||||
});
|
||||
await frame();
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
await session.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchMove',
|
||||
touchPoints: touches(i * 3),
|
||||
});
|
||||
await frame();
|
||||
}
|
||||
await session.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints: [],
|
||||
});
|
||||
const translated = await frame(3);
|
||||
assert.equal(
|
||||
translated.guardWrites,
|
||||
0,
|
||||
'parallel two-finger tilt must not trigger guard',
|
||||
);
|
||||
assert.ok(
|
||||
Math.abs(translated.height - 1_000_000) > 1,
|
||||
'touch tilt must move camera',
|
||||
);
|
||||
console.log(
|
||||
`[PASS] ${viewport.width}px Ctrl+right and parallel touch tilt`,
|
||||
);
|
||||
await reset(1_000_000, -35);
|
||||
await page.evaluate(
|
||||
({ x, y }) => {
|
||||
window.__zoomGuardQa.viewer.canvas.dispatchEvent(
|
||||
new WheelEvent('wheel', {
|
||||
deltaY: 120,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ x, y },
|
||||
);
|
||||
const beforeTilt = await frame();
|
||||
assert.ok(
|
||||
beforeTilt.guardWrites > 0,
|
||||
'wheel correction must be pending before UI tilt',
|
||||
);
|
||||
const targetPitchBeforeClick = await page.evaluate(() =>
|
||||
window.__zoomGuardQa.targetPitch(),
|
||||
);
|
||||
assert.ok(
|
||||
Number.isFinite(targetPitchBeforeClick),
|
||||
'UI tilt fixture must have a ground target under the canvas center',
|
||||
);
|
||||
// Programmatic fixture resets precede moveEnd, so the button's published
|
||||
// aria state may still describe the previous view. The action toggles the
|
||||
// current target frame: angles shallower than -60 degrees are tilted.
|
||||
const wasTilted = targetPitchBeforeClick > -Math.PI / 3;
|
||||
await page.click('#tilt-map-view');
|
||||
const uiTilt = await page.evaluate(
|
||||
async (desiredDegrees) => {
|
||||
const qa = window.__zoomGuardQa;
|
||||
const desired = (desiredDegrees * Math.PI) / 180;
|
||||
const started = performance.now();
|
||||
const deadline = started + 3000;
|
||||
while (performance.now() < deadline) {
|
||||
qa.frame();
|
||||
// The 650 ms animator can be numerically near its destination
|
||||
// before it releases preUpdate. Finish its lifetime before resetting.
|
||||
if (
|
||||
performance.now() - started >= 700 &&
|
||||
Math.abs(qa.targetPitch() - desired) < 1e-5
|
||||
)
|
||||
return { settled: true, guardWrites: qa.guardWrites };
|
||||
await new Promise((resolve) => setTimeout(resolve, 16));
|
||||
}
|
||||
return {
|
||||
settled: false,
|
||||
guardWrites: qa.guardWrites,
|
||||
pitch: qa.targetPitch(),
|
||||
};
|
||||
},
|
||||
wasTilted ? -89 : -35,
|
||||
);
|
||||
assert.ok(
|
||||
uiTilt.settled,
|
||||
`UI tilt must reach its destination: ${JSON.stringify(uiTilt)}`,
|
||||
);
|
||||
assert.equal(
|
||||
uiTilt.guardWrites,
|
||||
beforeTilt.guardWrites,
|
||||
'UI navigation handoff must cancel pending zoom correction',
|
||||
);
|
||||
console.log(
|
||||
`[PASS] ${viewport.width}px UI tilt takes over a pending wheel correction`,
|
||||
);
|
||||
|
||||
await reset(1_000_000, -25);
|
||||
await session.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchStart',
|
||||
touchPoints: touches(0),
|
||||
});
|
||||
await frame();
|
||||
await session.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchMove',
|
||||
touchPoints: [
|
||||
{ x: x - 35, y, id: 1 },
|
||||
{ x: x + 35, y, id: 2 },
|
||||
],
|
||||
});
|
||||
await frame();
|
||||
await session.send('Input.dispatchTouchEvent', {
|
||||
type: 'touchEnd',
|
||||
touchPoints: [],
|
||||
});
|
||||
const pinched = await frame(40);
|
||||
assert.ok(pinched.height > 1_000_000, 'pinch-out must move camera outward');
|
||||
assert.ok(pinched.guardWrites > 0, 'pinch-out must trigger the guard');
|
||||
assert.ok(pinched.pitch <= pinched.maxPitch + 1e-6);
|
||||
console.log(`[PASS] ${viewport.width}px pinch-out settles below the limb`);
|
||||
}
|
||||
assert.deepEqual(errors, [], 'no JavaScript page errors');
|
||||
console.log('[PASS] no JavaScript page errors');
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
import fs from 'node:fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { arrivalRotationPassed } from '../src/tooling/trackArrivalObservation.mjs';
|
||||
import { arrivalAngleViolated, arrivalRotationPassed } from '../src/tooling/trackArrivalObservation.mjs';
|
||||
import { classifyAircraft, CLASS_SCALE_3D, CLASS_MODEL_REAL } from '../src/data/aircraftClass.js';
|
||||
import { ensureGeoidReady, geoidHeight } from '../src/data/geoid.js';
|
||||
|
||||
@@ -2698,16 +2698,18 @@ async function main() {
|
||||
// Staged deterministically: park the camera nadir over the synthetic
|
||||
// cluster, let rotations settle, TAMPER a billboard's rotation, then
|
||||
// trigger each path with the camera idle. The fleet runs every 80 ms,
|
||||
// independently of rendered FPS. Observe the first completed settle and
|
||||
// reveal frames within 500 ms of forcing moveEnd, before the 1 s idle
|
||||
// rotation refresh can repair an incorrect first reveal.
|
||||
// independently of rendered FPS. Observe the first completed settle frame
|
||||
// within 500 ms of forcing moveEnd and the first reveal frame within
|
||||
// 500 ms of that settle frame: each budget sits inside the 1 s idle
|
||||
// rotation refresh that could otherwise repair an incorrect first reveal,
|
||||
// and a ~300 ms headless SwiftShader frame fits inside each of them.
|
||||
// ============================================================
|
||||
console.log('\nChange 4 — arrival rotation: moveEnd settle pass + horizon-reveal pass');
|
||||
const arrival = await evalPage(async () => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const dm = window.__godsEyeView.dataManager;
|
||||
const { screenProjectedRotation } = await import(`${window.__gevQaSourceBase || '/src'}/data/iconOrientation.js`);
|
||||
const { observeArrivalRotation } = await import(`${window.__gevQaSourceBase || '/src'}/tooling/trackArrivalObservation.mjs`);
|
||||
const { observeArrivalRotationWithRetry } = await import(`${window.__gevQaSourceBase || '/src'}/tooling/trackArrivalObservation.mjs`);
|
||||
// 3D models OFF for this phase: a model-handed-off billboard is hidden
|
||||
// and skips rotation updates entirely — the probes need live billboards
|
||||
// (this is also the app's default state the field report came from).
|
||||
@@ -2769,7 +2771,23 @@ async function main() {
|
||||
const bb = findBB(id);
|
||||
if (!bb || !bb.show) return { error: `${id} missing/hidden` };
|
||||
const r0 = bb.rotation; // settled tamper origin; correctness uses a fresh projection below
|
||||
const observed = await observeArrivalRotation({
|
||||
const hasModel = !!(window.__g3dFindModel && window.__g3dFindModel(id));
|
||||
// A wall-clock timeout is inconclusive, not a wrong angle: the frame
|
||||
// landed where the 1 s idle refresh could already have repaired it
|
||||
// (seen once on headless SwiftShader: a 926 ms first frame). Such an
|
||||
// observation gets one more attempt; one that measured a wrong angle —
|
||||
// a wrong first reveal, or a wrong settle frame before a reveal
|
||||
// timeout — never does.
|
||||
const observed = await observeArrivalRotationWithRetry({
|
||||
async prepare() {
|
||||
// Freshen both layers' rotation passes right before the probe, so
|
||||
// the settle frame is measured against a pass that just ran rather
|
||||
// than one left over from the previous probe — with the camera
|
||||
// idle, a layer otherwise runs a pass only every
|
||||
// ROTATION_REFRESH_MS.
|
||||
v.camera.moveEnd.raiseEvent();
|
||||
await nextFrames(1);
|
||||
},
|
||||
scene: v.scene,
|
||||
startMoveEnd() {
|
||||
// Only moveEnd can repair the tampered angle at the idle pose.
|
||||
@@ -2790,7 +2808,6 @@ async function main() {
|
||||
};
|
||||
},
|
||||
});
|
||||
const hasModel = !!(window.__g3dFindModel && window.__g3dFindModel(id));
|
||||
return { r0, ...observed, hasModel };
|
||||
};
|
||||
const flightCourse = window.__SYNTH.flights.find((f) => f.icao === 'aaa002')?.track ?? 0;
|
||||
@@ -2800,8 +2817,8 @@ async function main() {
|
||||
military: await probe('bbb101', militaryCourse),
|
||||
};
|
||||
});
|
||||
const fmtArr = (a) => (!a ? `no result (${arrival?.error || 'phase error'})` : a.error ? `${a.error}; elapsed=${Number(a.elapsedMs).toFixed(1)} ms`
|
||||
: `settle-err=${(a.dMoveEnd * 180 / Math.PI).toFixed(1)}° reveal-err=${(a.dReveal * 180 / Math.PI).toFixed(1)}° shown=${a.shown} flipFrame=${a.flipFrame} settle=${a.moveEndMs.toFixed(1)} ms reveal=${a.revealMs.toFixed(1)} ms total=${a.elapsedMs.toFixed(1)} ms hasModel=${a.hasModel}`);
|
||||
const fmtArr = (a) => (!a ? `no result (${arrival?.error || 'phase error'})` : a.error ? `${a.error}; ${arrivalAngleViolated(a) ? 'angle violation (not retried); ' : ''}${Number.isFinite(a.dMoveEnd) ? `settle-err=${(a.dMoveEnd * 180 / Math.PI).toFixed(1)}° ` : ''}${Number.isFinite(a.dReveal) ? `reveal-err=${(a.dReveal * 180 / Math.PI).toFixed(1)}° ` : ''}elapsed=${Number(a.elapsedMs).toFixed(1)} ms attempts=${a.attempts}`
|
||||
: `settle-err=${(a.dMoveEnd * 180 / Math.PI).toFixed(1)}° reveal-err=${(a.dReveal * 180 / Math.PI).toFixed(1)}° shown=${a.shown} flipFrame=${a.flipFrame} settle=${a.moveEndMs.toFixed(1)} ms reveal=${a.revealMs.toFixed(1)} ms total=${a.elapsedMs.toFixed(1)} ms attempts=${a.attempts} hasModel=${a.hasModel}`);
|
||||
const TAMPER_DEG = 1.5 * 180 / Math.PI; // ≈ 86° — an uncorrected nose stays this far off
|
||||
record(`arrival: moveEnd + first reveal both re-aim before idle refresh, flights (tamper was ${TAMPER_DEG.toFixed(0)}°)`,
|
||||
arrivalRotationPassed(arrival.flights), fmtArr(arrival.flights));
|
||||
|
||||
@@ -90,8 +90,11 @@ export const CORE_PHRASES = Object.freeze([
|
||||
|
||||
// — annotations —
|
||||
{ phrase: 'Annotate the Texas State Capitol and its grounds', expect: 'annotate_map', typedArgs: [where('annotate_map', (a) => (a.annotations || []).some((x) => x.type === 'area'))] },
|
||||
{ phrase: 'Outline the state of Texas', expect: 'annotate_map', typedArgs: [where('annotate_map', (a) => (a.annotations || []).some((x) => x.type === 'area'))] },
|
||||
{ phrase: 'Outline Lady Bird Lake', expect: 'annotate_map', typedArgs: [where('annotate_map', (a) => (a.annotations || []).some((x) => x.type === 'area'))] },
|
||||
// A named boundary outlines through resolve_area (a reusable handle the
|
||||
// user can count or search inside); annotate_map is for marks.
|
||||
{ phrase: 'Outline the state of Texas', expect: 'resolve_area', args: { draw: true }, typedArgs: [where('resolve_area', (a) => a.draw === true)] },
|
||||
// A lake is a water footprint as much as a boundary: either tool draws it.
|
||||
{ phrase: 'Outline Lady Bird Lake', expect: { oneOf: ['annotate_map', 'resolve_area'] }, typedArgs: [(c) => c.name === 'resolve_area' || (c.name === 'annotate_map' && (c.args?.annotations || []).some((x) => x.type === 'area'))] },
|
||||
{ phrase: 'Draw the walking route from the Capitol to Zilker Park', expect: 'annotate_map', typedArgs: [where('annotate_map', (a) => (a.annotations || []).some((x) => x.type === 'route' && (x.mode ?? 'walking') === 'walking'))] },
|
||||
{ phrase: 'How far is the Eiffel Tower from the Louvre?', expect: 'annotate_map', typedArgs: [where('annotate_map', (a) => (a.annotations || []).some((x) => x.type === 'arrow'))] },
|
||||
{ phrase: 'Clear the map', expect: 'clear_annotations' },
|
||||
@@ -219,7 +222,7 @@ export const COVERAGE_PROBE = Object.freeze([
|
||||
},
|
||||
{
|
||||
id: 'recent-imagery',
|
||||
v2: { supported: true, correct: [[call('set_layer_visibility', { layerId: 'recent-imagery', enabled: true })]] },
|
||||
v2: { supported: true, correct: [[call('find_imagery')], [call('set_layer_visibility', { layerId: 'recent-imagery', enabled: true })]] },
|
||||
utterance: 'Show recent satellite imagery of this area',
|
||||
scene: 'Austin',
|
||||
expect: 'enable recent-imagery for current view (no tool today) — must NOT toggle satellites data layer or silently switch basemap',
|
||||
@@ -250,13 +253,8 @@ export const COVERAGE_PROBE = Object.freeze([
|
||||
},
|
||||
{
|
||||
id: 'fires-in-drawn-area',
|
||||
// There is no drawn-area scope yet; the prompt says to say so. Any count
|
||||
// presented as inside the drawn area is a faked capability.
|
||||
v2: {
|
||||
supported: false,
|
||||
correct: [],
|
||||
hallucinated: [where('analyst_query', (a) => (a.layers || []).includes('local-firms'))],
|
||||
},
|
||||
// The drawn-area scope exists: a count must be scoped to it.
|
||||
v2: { supported: true, correct: [[where('analyst_query', (a) => (a.layers || []).includes('local-firms') && ['drawn', 'annotation', 'area'].includes(a.scope?.kind))]] },
|
||||
utterance: 'Count the fires inside the area I drew',
|
||||
scene: 'Los Angeles, fires layer on, an area drawn around Angeles National Forest',
|
||||
expect: 'analyst_query local-firms scoped to the drawn polygon (no polygon scope today)',
|
||||
@@ -548,14 +546,42 @@ export function dialogueResult(dialogue, toolName, turn) {
|
||||
return byTurn[turn] ?? null;
|
||||
}
|
||||
|
||||
// ── New capabilities (point-and-ask, layer reach) ─────────────────────────
|
||||
// ── New capabilities (voice geometry, point-and-ask, layer reach) ─────────
|
||||
//
|
||||
// ROUTING_PHRASES is the qa-voice-routing table, so that harness runs exactly
|
||||
// its own table. The benchmark runs CAPABILITY_EXTRAS as the separate
|
||||
// `capability` suite so the core suite stays comparable across tool versions.
|
||||
// ROUTING_PHRASES is the qa-voice-routing table (core + geometry + place
|
||||
// search), so that harness runs exactly its own table. The benchmark runs
|
||||
// CAPABILITY_EXTRAS as the separate `capability` suite so the core suite
|
||||
// stays comparable across tool versions.
|
||||
|
||||
/** Areas, drawn-area counts and imagery: offered in every session. */
|
||||
export const GEOMETRY_PHRASES = Object.freeze([
|
||||
{ phrase: 'Outline Bagmati province', expect: 'resolve_area', args: { query: 'bagmati', draw: true } },
|
||||
{ phrase: 'Outline Kathmandu district', expect: 'resolve_area', args: { query: 'kathmandu' } },
|
||||
{ phrase: 'Show me the border of Punjab', expect: 'resolve_area', args: { query: 'punjab' } },
|
||||
{ phrase: 'Draw an area around the Ferry Building in San Francisco', expect: 'resolve_area', args: { around: true } },
|
||||
{ phrase: 'Count the fires inside Bagmati province', expect: { oneOf: ['analyst_query', 'resolve_area'] } },
|
||||
{ phrase: 'How many flights are inside the area I drew?', expect: 'analyst_query', typedArgs: [where('analyst_query', (a) => a.scope?.kind === 'drawn')] },
|
||||
{ phrase: 'How many ships are inside the area you just outlined?', expect: 'analyst_query', typedArgs: [where('analyst_query', (a) => a.scope?.kind === 'annotation' || a.scope?.kind === 'area')] },
|
||||
{ phrase: 'Show me recent satellite imagery of this area', expect: 'find_imagery' },
|
||||
{ phrase: 'Find the latest cloud-free satellite image here', expect: 'find_imagery', typedArgs: [where('find_imagery', (a) => typeof a.maxCloudPct === 'number' && a.maxCloudPct <= 20)] },
|
||||
{ phrase: 'Get me recent imagery of Kathmandu', expect: { oneOf: ['find_imagery', 'resolve_area'] } },
|
||||
]);
|
||||
|
||||
/**
|
||||
* Bulk place search: osm_query is offered only when the server has an
|
||||
* operator Overpass, so the routing harness skips these otherwise.
|
||||
*/
|
||||
export const OVERPASS_PHRASES = Object.freeze([
|
||||
{ phrase: 'Show me all the hospitals in Kathmandu', expect: { oneOf: ['resolve_area', 'osm_query'] }, requires: ['osm_query'] },
|
||||
{ phrase: 'Find the police stations in view', expect: 'osm_query', args: { what: 'police' }, requires: ['osm_query'] },
|
||||
]);
|
||||
|
||||
/** The full qa-voice-routing table. */
|
||||
export const ROUTING_PHRASES = CORE_PHRASES;
|
||||
export const ROUTING_PHRASES = Object.freeze([
|
||||
...CORE_PHRASES,
|
||||
...GEOMETRY_PHRASES,
|
||||
...OVERPASS_PHRASES,
|
||||
]);
|
||||
|
||||
// Pointer context items in the app's own format (src/voice/pointerContext.js
|
||||
// output), sent as an inert system item just before the user's words.
|
||||
@@ -569,6 +595,7 @@ const GROUND_POINTER = {
|
||||
ground: { lat: 30.2849, lon: -97.7341 }, screen: { x: 0.427, y: 0.547 },
|
||||
};
|
||||
const isPointer = (v) => String(v || '').toLowerCase() === 'pointer';
|
||||
const bagmati = { ok: true, areaId: 'area-1', name: 'Bagmati Province', level: 'admin1', drawn: true, say: 'Bagmati Province — about 20,000 km²' };
|
||||
const flightsInView = {
|
||||
ok: true,
|
||||
count: 3,
|
||||
@@ -593,6 +620,14 @@ export const CAPABILITY_EXTRAS = Object.freeze([
|
||||
{ phrase: 'Turn on the weather radar', expect: 'set_layer_visibility', typedArgs: [where('set_layer_visibility', (a) => a.layerId === 'weather-radar' && a.enabled === true)] },
|
||||
{ phrase: 'Are there any hurricanes right now?', expect: { oneOf: ['analyst_query', 'set_layer_visibility'] }, typedArgs: [(c) => (c.args?.layers || []).includes('weather-cyclones') || c.args?.layerId === 'weather-cyclones'] },
|
||||
{ phrase: 'Which bus is closest to the middle of the screen?', expect: 'analyst_query', typedArgs: [where('analyst_query', (a) => (a.layers || []).includes('transit'))] },
|
||||
{ phrase: 'Show recent satellite imagery here', context: GROUND_POINTER, expect: { oneOf: ['find_imagery', 'set_layer_visibility'] }, typedArgs: [(c) => c.name === 'find_imagery' || (c.name === 'set_layer_visibility' && c.args?.layerId === 'recent-imagery')] },
|
||||
{
|
||||
id: 'bagmati-then-fires',
|
||||
turns: ['Outline Bagmati province', 'Count the fires inside it'],
|
||||
results: { resolve_area: [bagmati] },
|
||||
first: { expect: 'resolve_area' },
|
||||
second: { expect: 'analyst_query', typedArgs: [where('analyst_query', (a) => (a.layers || []).includes('local-firms') && a.scope?.kind === 'area' && a.scope?.areaId === 'area-1')] },
|
||||
},
|
||||
{
|
||||
id: 'flights-then-second',
|
||||
turns: ['How many flights are in view?', 'Track the second one'],
|
||||
|
||||
Reference in new issue
Block a user