gods-eye-view/scripts/qa-floor-verify.mjs

164 lines
8.6 KiB
JavaScript

// scripts/qa-floor-verify.mjs — live floor verification at AUS (round 5).
// Pins the camera at Austin airport, enables flights, waits ~3 polls, then
// measures every nearby contact's render height against the ACTUAL rendered
// mesh (sprite- and model-excluded scene.sampleHeight probes). Caught the mesh-latch
// coarse-LOD poison and the taxiing cold-cell regression on 2026-07-06.
// Run: node scripts/qa-floor-verify.mjs (dev server on :4173, real GPU best)
// with the poison fix + simplified chain live.
import puppeteer from 'puppeteer';
import fs from 'node:fs';
// QA_BASE_URL matches the sibling harnesses (qa-height-datum / qa-cctv-v2) so a
// secondary checkout can verify against its own dev server instead of the default :4173.
const APP_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
// CLI: --lat --lon --floor-min --floor-max (defaults: Austin airport)
const argv = Object.fromEntries(process.argv.slice(2).map((a) => a.split('=')).filter((x) => x.length === 2).map(([k, v]) => [k.replace(/^--/, ''), Number(v)]));
const SITE = {
lat: Number.isFinite(argv.lat) ? argv.lat : 30.197,
lon: Number.isFinite(argv.lon) ? argv.lon : -97.666,
floorMin: Number.isFinite(argv['floor-min']) ? argv['floor-min'] : 100,
floorMax: Number.isFinite(argv['floor-max']) ? argv['floor-max'] : 250,
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system Chrome
// (2026-07-30 lesson, already applied to the other harnesses): /Applications
// auto-updates underneath the harnesses and its software-GL behavior shifts
// across majors.
const chrome = [
process.env.PUPPETEER_EXECUTABLE_PATH,
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
].filter(Boolean).find((p) => fs.existsSync(p));
const browser = await puppeteer.launch({
headless: 'new', executablePath: chrome,
args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist', '--no-sandbox'],
});
const page = await browser.newPage();
await page.setViewport({ width: 1400, height: 900 });
page.on('pageerror', (e) => console.log('[pageerror]', e.message));
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 120000 });
await page.waitForFunction(() => window.__godsEyeView?.dataManager && window.__godsEyeView?.viewer, { timeout: 150000 });
await sleep(2000);
// Let the app's boot fly-to fully settle FIRST (it was overriding an early
// setView, leaving the camera away from Austin — every probe then read
// coarse global tiles at ~25 m and the oracle was garbage).
await sleep(12000);
await page.evaluate((site) => { window.__QA_SITE = site; }, SITE);
const camPin = async () => page.evaluate(() => {
const v = window.__godsEyeView.viewer;
const C = v.camera.positionCartographic.constructor;
v.camera.cancelFlight();
v.camera.setView({ destination: v.scene.globe.ellipsoid.cartographicToCartesian(C.fromDegrees(window.__QA_SITE.lon, window.__QA_SITE.lat, 3000)) });
v.camera.moveEnd.raiseEvent();
const c = v.camera.positionCartographic;
return { lat: +(c.latitude * 180 / Math.PI).toFixed(3), lon: +(c.longitude * 180 / Math.PI).toFixed(3), h: Math.round(c.height) };
});
console.log('camera pinned:', JSON.stringify(await camPin()));
await page.evaluate(async () => { await window.__godsEyeView.dataManager.toggle('flights'); });
console.log('waiting 100s: tiles stream + 3 polls...');
await sleep(100000);
// re-pin + report readiness right before probing
console.log('camera at probe time:', JSON.stringify(await camPin()));
const ready = await page.evaluate(() => {
const v = window.__godsEyeView.viewer;
const prims = v.scene.primitives;
for (let i = 0; i < prims.length; i++) {
const p = prims.get(i);
if (p && p.tilesLoaded !== undefined && p.show) return { tilesLoaded: !!p.tilesLoaded };
}
return { tilesLoaded: null };
});
console.log('tileset ready:', JSON.stringify(ready));
await sleep(8000);
const report = await page.evaluate(() => {
const gev = window.__godsEyeView;
const v = gev.viewer;
const layer = gev.dataManager.layers.get('flights')?.module;
if (!layer) return { err: 'no flights module' };
const ell = v.scene.globe.ellipsoid;
const C = v.camera.positionCartographic.constructor;
const center = ell.cartographicToCartesian(C.fromDegrees(window.__QA_SITE.lon, window.__QA_SITE.lat, 200));
const nearby = layer.getNearby(center, 15000, 60);
// Exclude EVERY billboard from the probes — sprites are pickable, so an
// unexcluded probe can return another aircraft's height as "the mesh".
// Exclude every fleet/tracked 3D Model too: getNearby() also returns contacts
// whose billboard is hidden because a model owns the visual, and those probes
// would otherwise land on aircraft geometry. `activeAnimations` +
// `minimumPixelSize` are Model-only — the 3D-Tiles tileset has neither, and
// excluding IT would leave sampleHeight with no mesh to read at all.
const excludes = [];
const walk = (coll) => {
const n = coll.length;
for (let i = 0; i < n; i++) {
let pr; try { pr = coll.get(i); } catch { continue; }
if (!pr) continue;
if (typeof pr.length === 'number' && typeof pr.get === 'function') { walk(pr); continue; }
if (pr.image !== undefined && pr.alignedAxis !== undefined) { excludes.push(pr); continue; }
if (pr.activeAnimations !== undefined && pr.minimumPixelSize !== undefined) excludes.push(pr);
}
};
walk(v.scene.primitives);
const out = [];
for (const p of nearby) {
const c = ell.cartesianToCartographic(p.position);
const latDeg = c.latitude * 180 / Math.PI, lonDeg = c.longitude * 180 / Math.PI;
let meshH = null;
try {
const h = v.scene.sampleHeight(C.fromDegrees(lonDeg, latDeg), excludes);
if (Number.isFinite(h)) meshH = h;
} catch { /* ignore */ }
out.push({
id: p.id,
renderAltM: +c.height.toFixed(1),
meshM: meshH != null ? +meshH.toFixed(1) : null,
aboveMeshM: meshH != null ? +(c.height - meshH).toFixed(1) : null,
});
}
// Visibility census (round 6): getNearby only returns contacts a sprite or a
// 3D model is actually drawing, so also walk the raw collections — a culled
// "invisible plane" shows up here.
let shown = 0, hidden = 0;
const censusWalk = (coll) => {
const n = coll.length;
for (let i = 0; i < n; i++) {
let pr; try { pr = coll.get(i); } catch { continue; }
if (!pr) continue;
if (typeof pr.length === 'number' && typeof pr.get === 'function') { censusWalk(pr); continue; }
if (pr.image === undefined || pr.alignedAxis === undefined) continue;
const cc = ell.cartesianToCartographic(pr.position);
const la = cc.latitude * 180 / Math.PI, lo = cc.longitude * 180 / Math.PI;
if (Math.abs(la - window.__QA_SITE.lat) > 0.08 || Math.abs(lo - window.__QA_SITE.lon) > 0.10) continue;
if (pr.show) shown++; else hidden++;
}
};
censusWalk(v.scene.primitives);
return { ausContacts: out.length, contacts: out.slice(0, 16), spritesShown: shown, spritesHidden: hidden };
});
console.log(JSON.stringify(report, null, 1));
// verdict — plausible mesh readings only (a wild meshM means the probe ray
// missed the tileset entirely; those rows prove nothing either way)
// Trust only probes that read a plausible AUS-area surface (~100..250 m
// ellipsoidal) — anything else hit a coarse tile or nothing (proves nothing).
const lows = (report.contacts || []).filter((c) =>
c.renderAltM < SITE.floorMax + 450 && c.aboveMeshM != null && c.meshM > SITE.floorMin && c.meshM < SITE.floorMax);
const buried = lows.filter((c) => c.aboveMeshM < -2);
console.log(`low contacts with plausible mesh readings: ${lows.length}; buried (< -2m): ${buried.length}`);
for (const b of buried) {
console.log(` BURIED ${b.id}: render ${b.renderAltM} m vs mesh ${b.meshM} m (${b.aboveMeshM} m)`);
}
const verdict = lows.length === 0 ? 'INCONCLUSIVE' : (buried.length === 0 ? 'PASS' : 'FAIL');
console.log(`VERDICT: ${verdict}`);
await browser.close();
// Exit code (2026-08-19): this harness printed VERDICT: FAIL and still exited 0,
// so a real regression (taxiing contacts rendering under the mesh) stayed
// invisible to every automated caller. Now it reports like its siblings
// (qa-height-datum / qa-cctv-v2): 0 = PASS, 1 = FAIL, 2 = could-not-measure.
// INCONCLUSIVE is deliberately NOT 0 — a run that measured no usable contacts
// proves nothing, and must not read as a pass. The VERDICT line is unchanged,
// so anything scraping stdout (the L9 QA matrix) is unaffected.
process.exit(verdict === 'PASS' ? 0 : (verdict === 'FAIL' ? 1 : 2));