#!/usr/bin/env node /** * Rendered proof for backdrop-selective callout plates at GROUND LEVEL. * * The owner's scenario: a cockpit sitting at JFK, where the geoid runs ~34 m * under the ellipsoid so the HUD honestly reads a NEGATIVE altitude. Every * label against the open sky came back with a full dark plate, because a * sub-ellipsoid camera fell into `skyBackdropFactor`'s degenerate guard. * * WHAT THIS ASSERTS, AND WHY IT IS NOT A UNIT TEST IN DISGUISE. An earlier * version of this harness called `skyBackdropFactor` directly and would have * passed with the painter stubbed out — the number was right and nothing was * drawn. Every assertion here now comes from the RENDERED path instead: * * ids/counts — `styleManager.getDetectionDiagnostics()` (src/ui.js), the * overlay's own published `visibleCount` / `labeledKeys`, so a contact only * counts once the sweep, the arbiter and the placement solve have all run. * plate scale — tapped off `CanvasRenderingContext2D` while the frame paints. * `paintDetectionCallout` fills the plate at `alpha * plateScale` and the * accent bar at `alpha`, in that order, then draws the callsign, so the * ratio of the two fills IS the plate scale and the callsign attributes it * to a named contact. Nothing is recomputed; this reads what was painted. * pixels — the plate rect comes from the same tap (`roundRect` immediately * precedes its `fill`), and the harness reads back mean alpha inside that * rect on `#world-overlay-canvas`, the real compositing surface. * * `--teeth` proves those assertions have teeth: it stubs the canvas paint calls * the callout painter uses to no-ops, so nothing reaches the framebuffer, and * the plate-scale and pixel checks must go RED. Run it after any change here. * * Three poses over one synthetic contact field: * level — cockpit on the ramp, nose on the horizon. This is the frame * the owner shot: approach traffic against open sky. * tilted-down — same cockpit, pitched down over the apron. Asserts that NO * ground contact is labelled, which is not this fix's doing: * from a camera under the ellipsoid Cesium's * `EllipsoidalOccluder` culls every point below the local * horizontal before detection ever sees it. * lifted — the same view a moment after rotate, at +40 m of ellipsoid * height. The apron contacts are back, and they keep their * FULL plates: the proof that sky selectivity did not quietly * become "no plates anywhere". * * Run (dev server already up): * node scripts/qa-cockpit-plates.mjs --url http://localhost:4268 * node scripts/qa-cockpit-plates.mjs --url http://localhost:4268 --tag before * node scripts/qa-cockpit-plates.mjs --url http://localhost:4268 --teeth * * Defaults to the real GPU (ANGLE/Metal). `--swiftshader` selects software GL, * which is deterministic but is NOT real-GPU evidence; the banner says which * backend actually ran and the run records it alongside the results. */ import fs from 'node:fs'; import path from 'node:path'; import puppeteer from 'puppeteer'; const argv = process.argv.slice(2); const getOpt = (name, fallback) => { const index = argv.indexOf(name); return index >= 0 && argv[index + 1] ? argv[index + 1] : fallback; }; const APP_URL = getOpt('--url', 'http://localhost:4173'); const APP_ORIGIN = new URL(APP_URL).origin; const TAG = getOpt('--tag', 'after'); const HEADFUL = argv.includes('--headful'); const TEETH = argv.includes('--teeth'); const SWIFTSHADER = argv.includes('--swiftshader'); const SHOT_DIR = path.resolve('qa-shots/cockpitplates'); /** Mirrors `SKY_PLATE_SCALE` in src/overlays/worldOverlayTokens.js. */ const SKY_PLATE_SCALE = 0.22; /** Painted alpha is a ratio of two floats; this is generous slack on it. */ const PLATE_SCALE_TOLERANCE = 0.02; // JFK. Ramp ~4 m MSL with a geoid height of about -34 m, so ellipsoid height // on the deck is ~-30 m and a widebody cockpit reads ALT -18 m: the owner's // exact HUD number, and a camera genuinely INSIDE the ellipsoid. const JFK_LON = -73.7781; const JFK_LAT = 40.6413; const COCKPIT_H = -18; const RAMP_H = -30; const M_PER_DEG_LAT = 110_574; /** * Contacts are placed due north of the cockpit, which faces north, so the sky * set and the ground set share one frame and one screenshot can show both. */ const CONTACTS = [ // --- against open sky: above the cockpit's eye level --- { id: 'AAL1042', metric: '168KT', range: 6_000, height: 620, want: 'sky' }, { id: 'DAL883', metric: '154KT', range: 9_000, height: 940, want: 'sky' }, { id: 'JBU221', metric: '178KT', range: 12_000, height: 1_500, want: 'sky' }, { id: 'UAL508', metric: '145KT', range: 4_200, height: 380, want: 'sky' }, { id: 'FDX1190', metric: '190KT', range: 16_000, height: 2_400, want: 'sky' }, // --- against the apron: below the cockpit floor --- { id: 'N6172G', metric: '12KT', range: 150, height: RAMP_H, want: 'ground' }, { id: 'SWA2255', metric: '8KT', range: 260, height: RAMP_H, want: 'ground' }, { id: 'DAL2190', metric: '15KT', range: 400, height: RAMP_H - 4, want: 'ground' }, { id: 'AAL77', metric: '6KT', range: 620, height: RAMP_H - 6, want: 'ground' }, ]; const SKY_IDS = CONTACTS.filter((contact) => contact.want === 'sky').map((contact) => contact.id); const GROUND_IDS = CONTACTS.filter((contact) => contact.want === 'ground').map((contact) => contact.id); const POSES = [ { name: 'level', height: COCKPIT_H, pitchDeg: -1.5, // Approach traffic is above eye level, so it survives the occluder and must // paint at the feathered scale. expectPlated: SKY_IDS, expectScale: SKY_PLATE_SCALE, expectAbsent: GROUND_IDS, note: 'sub-ellipsoid cockpit, nose on the horizon', }, { name: 'tilted-down', height: COCKPIT_H, pitchDeg: -14, expectPlated: [], expectScale: null, expectAbsent: GROUND_IDS, note: 'same cockpit pitched down; the occluder owns this emptiness', }, { name: 'lifted', height: 40, pitchDeg: -14, expectPlated: GROUND_IDS, expectScale: 1, expectAbsent: [], note: 'above the ellipsoid after rotate; apron plates at full strength', }, ]; const CHROME_CANDIDATES = [ process.env.PUPPETEER_EXECUTABLE_PATH, // Version-pinned Chrome-for-Testing over the auto-updating system Chrome: // its software-GL behavior shifts across majors and has produced false // negatives in this repo's harnesses before. (() => { try { return puppeteer.executablePath(); } catch { return null; } })(), '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', ].filter(Boolean); const results = []; function record(name, ok, detail) { results.push({ name, ok, detail }); console.log(` [${ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m'}] ${name}${detail ? ` — ${detail}` : ''}`); } const stubJson = (body) => ({ status: 200, contentType: 'application/json', body: JSON.stringify(body), }); /** * Installed before any app code runs. Records the callout painter's own canvas * calls; under `--teeth` it replaces them with no-ops instead, so the painter * stops reaching the framebuffer and every rendered assertion should fail. */ function installPaintTap(teeth) { const proto = CanvasRenderingContext2D.prototype; const origRoundRect = proto.roundRect; const origFill = proto.fill; const origFillText = proto.fillText; window.__PLATE_TAP = { on: false, events: [], lastRect: null, teeth }; if (teeth) { // Stub the painter: no record, no pixels. This is the negative control. proto.roundRect = function () {}; proto.fill = function () {}; proto.fillText = function () {}; return; } proto.roundRect = function (x, y, w, h, r) { if (window.__PLATE_TAP.on) window.__PLATE_TAP.lastRect = { x, y, w, h }; return origRoundRect.call(this, x, y, w, h, r); }; proto.fill = function (...args) { if (window.__PLATE_TAP.on) { window.__PLATE_TAP.events.push({ kind: 'fill', alpha: this.globalAlpha, rect: window.__PLATE_TAP.lastRect || null, }); } return origFill.apply(this, args); }; proto.fillText = function (text, x, y, ...rest) { if (window.__PLATE_TAP.on) { window.__PLATE_TAP.events.push({ kind: 'text', alpha: this.globalAlpha, text: String(text) }); } return origFillText.call(this, text, x, y, ...rest); }; } async function main() { const backend = SWIFTSHADER ? 'software GL (SwiftShader)' : 'real GPU (ANGLE/Metal)'; console.log('\nCockpit Plate Backdrop QA — rendered path'); console.log(` App URL : ${APP_URL}`); console.log(` Tag : ${TAG}${TEETH ? ' (TEETH: painter stubbed, expect RED)' : ''}`); console.log(` Backend : ${backend}\n`); try { const response = await fetch(APP_URL); if (!response.ok) throw new Error(`HTTP ${response.status}`); } catch (error) { console.error(`Dev server not reachable at ${APP_URL}: ${error.message}`); process.exit(2); } fs.mkdirSync(SHOT_DIR, { recursive: true }); const executablePath = CHROME_CANDIDATES.find((candidate) => { try { return fs.existsSync(candidate); } catch { return false; } }); const browser = await puppeteer.launch({ headless: HEADFUL ? false : 'new', ...(executablePath ? { executablePath } : {}), args: [ '--no-sandbox', '--disable-setuid-sandbox', ...(SWIFTSHADER ? ['--use-gl=angle', '--use-angle=swiftshader'] : ['--use-angle=metal', '--enable-gpu', '--ignore-gpu-blocklist']), '--disable-dev-shm-usage', '--disable-background-timer-throttling', '--disable-renderer-backgrounding', '--window-size=1280,800', ], }); const consoleErrors = []; const perPose = []; let renderer = 'unknown'; try { const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 }); page.on('console', (message) => { if (message.type() !== 'error') return; const text = message.text(); if (!/Failed to load resource.*404/i.test(text)) consoleErrors.push(text); }); page.on('pageerror', (error) => consoleErrors.push(`pageerror: ${error.message}`)); await page.evaluateOnNewDocument(installPaintTap, TEETH); await page.setRequestInterception(true); page.on('request', (request) => { const url = new URL(request.url()); if (url.origin !== APP_ORIGIN) return void request.continue(); // Silence the live feeds: the injected field is the only population. if (url.pathname === '/api/ais-live') { return void request.respond(stubJson({ rows: [], source: 'QA', status: 'open', error: null, refreshing: false, newestPositionAt: null, lastMessageAt: null, })); } if (url.pathname === '/api/adsblol/mil') { return void request.respond(stubJson({ msg: 'No error', now: Date.now(), ac: [] })); } if (url.pathname === '/api/opensky') { return void request.respond(stubJson({ time: Math.floor(Date.now() / 1000), states: [] })); } if (url.pathname === '/api/opensky-track') { return void request.respond(stubJson({ path: [] })); } if (url.pathname === '/api/openai/hud-summary') { return void request.respond(stubJson({ summary: 'Cockpit plate QA' })); } return void request.continue(); }); await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 90_000 }); await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 90_000 }); await page.waitForFunction( () => document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 90_000 }, ); renderer = await page.evaluate(() => { const gl = document.createElement('canvas').getContext('webgl2') || document.createElement('canvas').getContext('webgl'); if (!gl) return 'no webgl'; const ext = gl.getExtension('WEBGL_debug_renderer_info'); return ext ? String(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)) : String(gl.getParameter(gl.RENDERER)); }); const softwareRenderer = /swiftshader|llvmpipe|software/i.test(renderer); console.log(` GL : ${renderer}\n`); record( SWIFTSHADER ? 'running on software GL as requested (structural checks, not GPU evidence)' : 'running on the real GPU, so the screenshots are real-GPU evidence', SWIFTSHADER ? softwareRenderer : !softwareRenderer, renderer, ); // Inject the field. The layers keep their real pipelines; only the source // of observations is replaced, exactly as scripts/qa-labels.mjs does. await page.evaluate((payload) => { const { viewer, dataManager, styleManager } = window.__godsEyeView; viewer.camera.cancelFlight(); const Cartesian3 = viewer.camera.position.constructor; window.__PLATE_QA_FIELD = payload.contacts.map((contact) => ({ sourceId: contact.id.toLowerCase(), id: contact.id, metric: contact.metric, position: Cartesian3.fromDegrees( payload.lon, payload.lat + contact.range / payload.mPerDegLat, contact.height, ), type: 'AIR', tier: 'civil', })); for (const layerId of ['flights', 'military', 'satellites']) { const entry = dataManager.layers.get(layerId); if (!entry?.module) continue; entry.module.getDetectableObjects = () => ( layerId === 'flights' ? window.__PLATE_QA_FIELD : [] ); } styleManager.setDetection({ enabled: true, densityPct: 100, allocationStrategy: 'elastic' }); }, { lon: JFK_LON, lat: JFK_LAT, mPerDegLat: M_PER_DEG_LAT, contacts: CONTACTS }); for (const pose of POSES) { console.log(`\n — pose "${pose.name}" (${pose.note})`); await page.evaluate((payload) => { const { viewer } = window.__godsEyeView; const Cartesian3 = viewer.camera.position.constructor; viewer.camera.setView({ destination: Cartesian3.fromDegrees(payload.lon, payload.lat, payload.height), orientation: { heading: 0, pitch: payload.pitchDeg * Math.PI / 180, roll: 0 }, }); }, { lon: JFK_LON, lat: JFK_LAT, height: pose.height, pitchDeg: pose.pitchDeg }); // Let the tiles settle and the label solve run before the tap window. await new Promise((resolve) => setTimeout(resolve, 6_000)); await page.evaluate(() => { window.__PLATE_TAP.events = []; window.__PLATE_TAP.on = true; // Drive the frames this measurement needs, instead of assuming something // else is holding the render loop open. // // This harness enables no data layer and never enters real Cockpit (which // takes its own hold), so nothing in the scene animates. It used to get // its frames for free from detection's continuous-render hold — and that // hold was itself the bug: it pinned every idle tab at 60 fps, which is // why it was removed (see src/data/detectionRenderDemand.js). The scene is // now correctly idle during the tap window, so a measurement of PAINT // OUTPUT has to ask for its own renders. Production is unaffected: real // Cockpit holds, and a parked map keeps showing the last correct frame // precisely because nothing changed. const scene = window.__godsEyeView.viewer.scene; const pump = () => { if (!window.__PLATE_TAP.on) return; scene.requestRender(); requestAnimationFrame(pump); }; pump(); }); await new Promise((resolve) => setTimeout(resolve, 1_200)); const observed = await page.evaluate((wantedIds) => { window.__PLATE_TAP.on = false; const events = window.__PLATE_TAP.events; const wanted = new Set(wantedIds); // paintDetectionCallout paints, in order: the plate (alpha*plateScale), // the accent bar (alpha), the leader, then the callsign. So the two // fills before a callsign are that callout's plate and accent, and // their ratio is the plate scale that was actually painted. const byId = new Map(); for (let i = 0; i < events.length; i++) { const event = events[i]; if (event.kind !== 'text') continue; const id = event.text.trim(); if (!wanted.has(id)) continue; const fills = []; for (let j = i - 1; j >= 0 && fills.length < 2; j--) { if (events[j].kind === 'fill') fills.unshift(events[j]); } if (fills.length < 2) continue; const [plate, accent] = fills; if (!(accent.alpha > 0)) continue; byId.set(id, { id, plateAlpha: plate.alpha, accentAlpha: accent.alpha, plateScale: plate.alpha / accent.alpha, rect: plate.rect, }); } // Pixel readback from the real compositing surface, over the plate rect // the painter itself just used. const canvas = document.getElementById('world-overlay-canvas'); const ctx = canvas?.getContext('2d', { willReadFrequently: true }); const dpr = canvas && canvas.clientWidth ? canvas.width / canvas.clientWidth : 1; for (const entry of byId.values()) { entry.meanPixelAlpha = null; if (!ctx || !entry.rect || !(entry.rect.w > 0) || !(entry.rect.h > 0)) continue; const x = Math.max(0, Math.round(entry.rect.x * dpr)); const y = Math.max(0, Math.round(entry.rect.y * dpr)); const w = Math.min(canvas.width - x, Math.round(entry.rect.w * dpr)); const h = Math.min(canvas.height - y, Math.round(entry.rect.h * dpr)); if (!(w > 0) || !(h > 0)) continue; const data = ctx.getImageData(x, y, w, h).data; let total = 0; for (let p = 3; p < data.length; p += 4) total += data[p]; entry.meanPixelAlpha = total / (data.length / 4) / 255; } const diagnostics = window.__godsEyeView.styleManager.getDetectionDiagnostics(); return { plated: [...byId.values()], visibleCount: diagnostics?.visibleCount ?? null, labeledKeys: diagnostics?.labeledKeys ?? null, profile: diagnostics?.profile ?? null, fillEvents: events.filter((event) => event.kind === 'fill').length, }; }, [...pose.expectPlated, ...pose.expectAbsent]); const labelledIds = (observed.labeledKeys || []) .filter((key) => key.startsWith('flights:')) .map((key) => key.slice('flights:'.length).toUpperCase()); console.log( ` diagnostics: profile=${observed.profile} visibleCount=${observed.visibleCount}` + ` labelled=[${labelledIds.join(' ')}] fills=${observed.fillEvents}`, ); for (const entry of observed.plated) { console.log( ` ${entry.id.padEnd(8)} plateScale=${entry.plateScale.toFixed(4)}` + ` (plate ${entry.plateAlpha.toFixed(4)} / accent ${entry.accentAlpha.toFixed(4)})` + ` meanPixelAlpha=${entry.meanPixelAlpha === null ? 'n/a' : entry.meanPixelAlpha.toFixed(4)}`, ); } // 1. The overlay actually rendered something this pose. record( `${pose.name}: the overlay reports live rendered contacts`, Number.isFinite(observed.visibleCount) && observed.visibleCount > 0, `visibleCount=${observed.visibleCount}`, ); // 2. The contacts we expect to be labelled are labelled, by id. const missingLabels = pose.expectPlated.filter((id) => !labelledIds.includes(id)); record( `${pose.name}: every expected contact reaches the label solve`, missingLabels.length === 0, missingLabels.length ? `missing ${missingLabels.join(',')}` : `${pose.expectPlated.length} labelled`, ); // 3. The contacts that must NOT be labelled are absent. const unexpected = pose.expectAbsent.filter((id) => labelledIds.includes(id)); record( `${pose.name}: below-eye-level contacts stay out of the solve`, unexpected.length === 0, unexpected.length ? `unexpectedly labelled ${unexpected.join(',')}` : 'none labelled, as expected', ); // 4/5/6. Painted plate scale and pixels, per named contact. if (pose.expectPlated.length > 0) { const platedIds = observed.plated.map((entry) => entry.id); const missingPaint = pose.expectPlated.filter((id) => !platedIds.includes(id)); record( `${pose.name}: the callout painter ran for every expected contact`, missingPaint.length === 0, missingPaint.length ? `no paint recorded for ${missingPaint.join(',')}` : `${platedIds.length} painted`, ); const offScale = observed.plated.filter( (entry) => Math.abs(entry.plateScale - pose.expectScale) > PLATE_SCALE_TOLERANCE, ); record( `${pose.name}: painted plate scale is ${pose.expectScale}`, observed.plated.length > 0 && offScale.length === 0, offScale.length ? offScale.map((entry) => `${entry.id}=${entry.plateScale.toFixed(3)}`).join(' ') : observed.plated.map((entry) => entry.plateScale.toFixed(3)).join(' '), ); const inked = observed.plated.filter((entry) => (entry.meanPixelAlpha ?? 0) > 0.01); record( `${pose.name}: the plate rect carries real pixels on the overlay canvas`, observed.plated.length > 0 && inked.length === observed.plated.length, observed.plated .map((entry) => `${entry.id}=${entry.meanPixelAlpha === null ? 'n/a' : entry.meanPixelAlpha.toFixed(3)}`) .join(' '), ); } perPose.push({ pose: pose.name, ...observed, labelledIds }); const shot = path.join(SHOT_DIR, `${TAG}-${pose.name}.png`); await page.screenshot({ path: shot }); console.log(` shot: ${shot}`); } // Cross-pose pixel comparison: the same theme, the same canvas, the same // painter — only the backdrop differs, so sky plates must be lighter. const skyInk = perPose.find((entry) => entry.pose === 'level')?.plated || []; const groundInk = perPose.find((entry) => entry.pose === 'lifted')?.plated || []; const meanOf = (rows) => (rows.length ? rows.reduce((sum, entry) => sum + (entry.meanPixelAlpha ?? 0), 0) / rows.length : null); const skyMean = meanOf(skyInk); const groundMean = meanOf(groundInk); record( 'sky-backed plates are measurably lighter on the canvas than ground-backed ones', Number.isFinite(skyMean) && Number.isFinite(groundMean) && skyMean < groundMean, `sky mean alpha ${skyMean === null ? 'n/a' : skyMean.toFixed(4)}` + ` vs ground ${groundMean === null ? 'n/a' : groundMean.toFixed(4)}`, ); record( 'no page errors while the overlay ran at ground level', consoleErrors.length === 0, consoleErrors.slice(0, 3).join(' | ') || 'clean', ); } finally { await browser.close(); } fs.writeFileSync( path.join(SHOT_DIR, `${TAG}-rendered.json`), `${JSON.stringify({ tag: TAG, teeth: TEETH, backend, renderer, cockpitHeight: COCKPIT_H, poses: perPose, }, null, 2)}\n`, ); const failed = results.filter((entry) => !entry.ok); console.log(`\n ${results.length - failed.length}/${results.length} checks passed\n`); if (TEETH) { // Under --teeth the run is EXPECTED to fail; a green run would mean the // assertions do not depend on the painter and the harness is decorative. const ok = failed.length > 0; console.log(ok ? ` \x1b[32mTEETH OK\x1b[0m — ${failed.length} check(s) went red with the painter stubbed:\n ${failed.map((entry) => entry.name).join('\n ')}\n` : ' \x1b[31mTEETH FAILED\x1b[0m — every check passed with the painter stubbed; the harness proves nothing.\n'); process.exit(ok ? 0 : 1); } process.exit(failed.length === 0 ? 0 : 1); } main().catch((error) => { console.error(error); process.exit(1); });