Release God's Eye View as open source

This commit is contained in:
Bilawal Sidhu committed 2026-08-24 17:02:16 -05:00
1 parent a5aaea7e16
commit 880a672b5e
433 files changed
+219893 -23

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env node
/**
* Build src/data/local_data/neighborhoods/san-francisco.json from the official
* public-domain DataSF "Analysis Neighborhoods" dataset.
*
* Source (map view): https://data.sfgov.org/Geographic-Locations-and-Boundaries/Analysis-Neighborhoods-Map/p5b7-5n3h
* Underlying dataset: https://data.sfgov.org/d/j2bu-swwd (columns: the_geom, nhood)
* Fetched from: https://data.sfgov.org/resource/j2bu-swwd.geojson?$limit=100
* License: Open Data Commons Public Domain Dedication and License (PDDL 1.0)
* (dataset metadata `licenseId: "PDDL"` at
* https://data.sfgov.org/api/views/j2bu-swwd.json)
*
* Transform (deterministic):
* 1. `properties.nhood` → `properties.name` (no renames — DataSF names kept verbatim).
* 2. Douglas-Peucker simplification per ring, tolerance 2e-5° (~2 m) — shaves
* hyper-detailed shoreline vertices without visibly moving boundaries.
* 3. Coordinates rounded to 6 decimals (~0.1 m); consecutive duplicates dropped;
* rings re-closed; rings that collapse below 4 points dropped.
* 4. Single-part MultiPolygons collapsed to Polygon.
* 5. Features sorted by name for a stable diff.
*
* Usage:
* node scripts/build-sf-neighborhoods.mjs [raw.geojson]
* With no argument it downloads the live dataset; with an argument it reads the
* given raw GeoJSON file (the exact bytes retrieved on 2026-07-30 in our case).
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const SOURCE_URL = 'https://data.sfgov.org/resource/j2bu-swwd.geojson?$limit=100';
const OUT = path.join(path.dirname(fileURLToPath(import.meta.url)),
'..', 'src', 'data', 'local_data', 'neighborhoods', 'san-francisco.json');
const TOLERANCE = 2e-5; // degrees, ~2 m
const DECIMALS = 6;
/** Perpendicular distance from point p to segment a-b (in degrees, planar — fine at this scale). */
function segDist(p, a, b) {
let [x, y] = p; const [x1, y1] = a; const [x2, y2] = b;
let dx = x2 - x1; let dy = y2 - y1;
if (dx !== 0 || dy !== 0) {
const t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy);
if (t > 1) { x -= x2; y -= y2; return Math.hypot(x, y); }
if (t > 0) { x -= x1 + dx * t; y -= y1 + dy * t; return Math.hypot(x, y); }
}
return Math.hypot(x - x1, y - y1);
}
/** Iterative Douglas-Peucker on an open point list. */
function douglasPeucker(points, tolerance) {
if (points.length <= 2) return points.slice();
const keep = new Uint8Array(points.length);
keep[0] = keep[points.length - 1] = 1;
const stack = [[0, points.length - 1]];
while (stack.length) {
const [first, last] = stack.pop();
let maxDist = 0; let index = -1;
for (let i = first + 1; i < last; i++) {
const d = segDist(points[i], points[first], points[last]);
if (d > maxDist) { maxDist = d; index = i; }
}
if (maxDist > tolerance && index !== -1) {
keep[index] = 1;
stack.push([first, index], [index, last]);
}
}
return points.filter((_, i) => keep[i]);
}
const round = (v) => Number(v.toFixed(DECIMALS));
/** Simplify + round one ring; returns null if it degenerates. */
function processRing(ring) {
// GeoJSON rings are closed (first == last); simplify the open part.
const open = ring.length > 1
&& ring[0][0] === ring[ring.length - 1][0] && ring[0][1] === ring[ring.length - 1][1]
? ring.slice(0, -1) : ring.slice();
const simplified = douglasPeucker(open, TOLERANCE).map(([lon, lat]) => [round(lon), round(lat)]);
const out = [];
for (const pt of simplified) {
const prev = out[out.length - 1];
if (prev && prev[0] === pt[0] && prev[1] === pt[1]) continue;
out.push(pt);
}
if (out.length < 3) return null;
out.push([out[0][0], out[0][1]]); // re-close
return out;
}
async function main() {
let rawText;
if (process.argv[2]) {
rawText = fs.readFileSync(process.argv[2], 'utf8');
console.log(`read ${process.argv[2]}`);
} else {
console.log(`fetching ${SOURCE_URL}`);
const res = await fetch(SOURCE_URL);
if (!res.ok) throw new Error(`HTTP ${res.status} from DataSF`);
rawText = await res.text();
}
const raw = JSON.parse(rawText);
if (raw.type !== 'FeatureCollection' || !Array.isArray(raw.features)) {
throw new Error('unexpected payload: not a FeatureCollection');
}
let inVerts = 0; let outVerts = 0;
const features = [];
for (const f of raw.features) {
const name = f.properties && f.properties.nhood;
if (!name || !f.geometry) throw new Error(`feature missing nhood/geometry: ${JSON.stringify(f.properties)}`);
const polysIn = f.geometry.type === 'MultiPolygon' ? f.geometry.coordinates
: f.geometry.type === 'Polygon' ? [f.geometry.coordinates]
: null;
if (!polysIn) throw new Error(`unexpected geometry type ${f.geometry.type} for ${name}`);
const polys = [];
for (const poly of polysIn) {
const rings = [];
for (const ring of poly) {
inVerts += ring.length;
const r = processRing(ring);
if (r) { rings.push(r); outVerts += r.length; }
}
if (rings.length && poly[0] && rings[0]) polys.push(rings);
}
if (!polys.length) throw new Error(`geometry collapsed for ${name}`);
const geometry = polys.length === 1
? { type: 'Polygon', coordinates: polys[0] }
: { type: 'MultiPolygon', coordinates: polys };
features.push({ type: 'Feature', properties: { name }, geometry });
}
features.sort((a, b) => a.properties.name.localeCompare(b.properties.name));
const out = {
type: 'FeatureCollection',
city: 'San Francisco',
source: 'DataSF "Analysis Neighborhoods" (dataset j2bu-swwd, map view p5b7-5n3h) — PDDL 1.0 (public domain); see SOURCE.md',
features,
};
fs.writeFileSync(OUT, `${JSON.stringify(out)}\n`);
const kb = (fs.statSync(OUT).size / 1024).toFixed(0);
console.log(`wrote ${OUT}: ${features.length} features, ${inVerts} → ${outVerts} vertices, ${kb} KB`);
}
main().catch((err) => { console.error(err); process.exit(1); });
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PORT="${PORT:-4173}"
HOST="${HOST:-0.0.0.0}"
CCTV_SOURCES_FILE="${CCTV_SOURCES_FILE:-config/cctv_sources.austin.json}"
CCTV_PREFER_AUSTIN="${CCTV_PREFER_AUSTIN:-1}"
CCTV_AUSTIN_MAX_SOURCES="${CCTV_AUSTIN_MAX_SOURCES:-36}"
CCTV_MAX_SOURCES="${CCTV_MAX_SOURCES:-48}"
if ! command -v npm >/dev/null 2>&1; then
echo "error: npm not found"
exit 1
fi
GOOGLE_MAPS_API_KEY_ENV="${GOOGLE_MAPS_API_KEY:-}"
GOOGLE_MAPS_API_KEY_KEYCHAIN=""
GOOGLE_MAPS_API_KEY_SOURCE=""
if command -v security >/dev/null 2>&1; then
for acct in "api-key" "default" "key"; do
GOOGLE_MAPS_API_KEY_KEYCHAIN="$(security find-generic-password -s "google-maps-api" -a "${acct}" -w 2>/dev/null || true)"
if [[ -n "${GOOGLE_MAPS_API_KEY_KEYCHAIN}" ]]; then
GOOGLE_MAPS_API_KEY_SOURCE="keychain:${acct}"
break
fi
done
fi
if [[ -n "${GOOGLE_MAPS_API_KEY_KEYCHAIN}" ]]; then
GOOGLE_MAPS_API_KEY="${GOOGLE_MAPS_API_KEY_KEYCHAIN}"
elif [[ -n "${GOOGLE_MAPS_API_KEY_ENV}" ]]; then
GOOGLE_MAPS_API_KEY="${GOOGLE_MAPS_API_KEY_ENV}"
GOOGLE_MAPS_API_KEY_SOURCE="env"
else
GOOGLE_MAPS_API_KEY=""
fi
if [[ -z "${GOOGLE_MAPS_API_KEY}" ]]; then
echo "error: Google Maps API key missing."
echo "set GOOGLE_MAPS_API_KEY in env, or add Keychain item: service=google-maps-api account=api-key"
exit 1
fi
read_keychain_secret() {
local service="$1"
local account="$2"
security find-generic-password -s "$service" -a "$account" -w 2>/dev/null || true
}
load_opensky_oauth_from_file() {
local file_path="$1"
if [[ -z "${file_path}" || ! -f "${file_path}" ]]; then
return
fi
if ! command -v node >/dev/null 2>&1; then
echo "warning: node not found; cannot parse OPENSKY_CREDENTIALS_FILE"
return
fi
local parsed
parsed="$(node -e "const fs=require('fs');const p=process.argv[1];try{const raw=JSON.parse(fs.readFileSync(p,'utf8'));const id=String(raw.clientId??raw.client_id??'').trim();const secret=String(raw.clientSecret??raw.client_secret??'').trim();process.stdout.write(id+'\\t'+secret);}catch{process.exit(1)}" "${file_path}" 2>/dev/null || true)"
if [[ "${parsed}" != *$'\t'* ]]; then
return
fi
local parsed_client_id="${parsed%%$'\t'*}"
local parsed_client_secret="${parsed#*$'\t'}"
if [[ -z "${OPENSKY_CLIENT_ID}" && -n "${parsed_client_id}" ]]; then
OPENSKY_CLIENT_ID="${parsed_client_id}"
fi
if [[ -z "${OPENSKY_CLIENT_SECRET}" && -n "${parsed_client_secret}" ]]; then
OPENSKY_CLIENT_SECRET="${parsed_client_secret}"
fi
}
resolve_opensky_credentials() {
OPENSKY_AUTH_MODE="$(printf '%s' "${OPENSKY_AUTH_MODE:-oauth}" | tr '[:upper:]' '[:lower:]')"
OPENSKY_CREDENTIALS_FILE="${OPENSKY_CREDENTIALS_FILE:-}"
case "${OPENSKY_AUTH_MODE}" in
basic|oauth|auto|anon) ;;
*)
echo "warning: invalid OPENSKY_AUTH_MODE='${OPENSKY_AUTH_MODE}', defaulting to 'oauth'"
OPENSKY_AUTH_MODE="oauth"
;;
esac
OPENSKY_CLIENT_ID="${OPENSKY_CLIENT_ID:-}"
OPENSKY_CLIENT_SECRET="${OPENSKY_CLIENT_SECRET:-}"
OPENSKY_USERNAME="${OPENSKY_USERNAME:-}"
OPENSKY_PASSWORD="${OPENSKY_PASSWORD:-}"
if [[ "${OPENSKY_AUTH_MODE}" == "basic" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_USERNAME}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "username" "user" "login" "email" "default"; do
OPENSKY_USERNAME="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_USERNAME}" ]]; then
break 2
fi
done
done
fi
if [[ "${OPENSKY_AUTH_MODE}" == "basic" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_PASSWORD}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "password" "pass" "token" "secret" "default"; do
OPENSKY_PASSWORD="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_PASSWORD}" ]]; then
break 2
fi
done
done
fi
if [[ "${OPENSKY_AUTH_MODE}" == "oauth" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -n "${OPENSKY_CREDENTIALS_FILE}" ]]; then
load_opensky_oauth_from_file "${OPENSKY_CREDENTIALS_FILE}"
fi
if [[ "${OPENSKY_AUTH_MODE}" == "oauth" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_CLIENT_ID}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "client_id" "client-id" "client" "api-key"; do
OPENSKY_CLIENT_ID="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_CLIENT_ID}" ]]; then
break 2
fi
done
done
fi
if [[ "${OPENSKY_AUTH_MODE}" == "oauth" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_CLIENT_SECRET}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "client_secret" "client-secret" "secret"; do
OPENSKY_CLIENT_SECRET="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_CLIENT_SECRET}" ]]; then
break 2
fi
done
done
fi
case "${OPENSKY_AUTH_MODE}" in
basic)
OPENSKY_CLIENT_ID=""
OPENSKY_CLIENT_SECRET=""
;;
oauth)
OPENSKY_USERNAME=""
OPENSKY_PASSWORD=""
;;
anon)
OPENSKY_CLIENT_ID=""
OPENSKY_CLIENT_SECRET=""
OPENSKY_USERNAME=""
OPENSKY_PASSWORD=""
;;
esac
}
resolve_opensky_credentials
echo "Starting God's Eye View with CCTV source pack..."
echo "URL: http://localhost:${PORT}/"
echo "Google Maps key source: ${GOOGLE_MAPS_API_KEY_SOURCE}"
if [[ -f "${CCTV_SOURCES_FILE}" ]]; then
echo "CCTV_SOURCES_FILE: ${CCTV_SOURCES_FILE}"
else
echo "CCTV_SOURCES_FILE: ${CCTV_SOURCES_FILE} (missing, using Austin open-data feed discovery)"
fi
echo "OpenSky auth mode: ${OPENSKY_AUTH_MODE}"
if [[ -n "${OPENSKY_CREDENTIALS_FILE}" ]]; then
if [[ -f "${OPENSKY_CREDENTIALS_FILE}" ]]; then
echo "OpenSky credentials file: ${OPENSKY_CREDENTIALS_FILE}"
else
echo "OpenSky credentials file: ${OPENSKY_CREDENTIALS_FILE} (missing)"
fi
fi
case "${OPENSKY_AUTH_MODE}" in
basic)
if [[ -n "${OPENSKY_USERNAME}" && -n "${OPENSKY_PASSWORD}" ]]; then
echo "OpenSky basic auth: configured"
else
echo "OpenSky basic auth: missing credentials"
fi
;;
oauth)
if [[ -n "${OPENSKY_CLIENT_ID}" && -n "${OPENSKY_CLIENT_SECRET}" ]]; then
echo "OpenSky OAuth: configured"
else
echo "OpenSky OAuth: missing client credentials"
fi
;;
auto)
if [[ -n "${OPENSKY_CLIENT_ID}" && -n "${OPENSKY_CLIENT_SECRET}" ]]; then
echo "OpenSky auto auth: OAuth configured"
elif [[ -n "${OPENSKY_USERNAME}" && -n "${OPENSKY_PASSWORD}" ]]; then
echo "OpenSky auto auth: basic configured"
else
echo "OpenSky auto auth: no credentials found"
fi
;;
anon)
echo "OpenSky auth: disabled (anonymous mode)"
;;
esac
GOOGLE_MAPS_API_KEY="${GOOGLE_MAPS_API_KEY}" \
CCTV_SOURCES_FILE="${CCTV_SOURCES_FILE}" \
CCTV_PREFER_AUSTIN="${CCTV_PREFER_AUSTIN}" \
CCTV_AUSTIN_MAX_SOURCES="${CCTV_AUSTIN_MAX_SOURCES}" \
CCTV_MAX_SOURCES="${CCTV_MAX_SOURCES}" \
OPENSKY_AUTH_MODE="${OPENSKY_AUTH_MODE}" \
OPENSKY_CREDENTIALS_FILE="${OPENSKY_CREDENTIALS_FILE}" \
OPENSKY_CLIENT_ID="${OPENSKY_CLIENT_ID}" \
OPENSKY_CLIENT_SECRET="${OPENSKY_CLIENT_SECRET}" \
OPENSKY_USERNAME="${OPENSKY_USERNAME}" \
OPENSKY_PASSWORD="${OPENSKY_PASSWORD}" \
npm run dev -- --host "${HOST}" --port "${PORT}" --force
+362
View File
@@ -0,0 +1,362 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PORT="${PORT:-4173}"
# Local-only by default: the dev server brokers configured API keys, so it
# should not be reachable from the network unless explicitly requested.
# Set HOST=0.0.0.0 to opt in to LAN exposure (a warning is printed).
HOST="${HOST:-localhost}"
# CCTV source packs (all keyless): Austin (~815 live upstream), Caltrans
# districts 4,7,11,3 = SF/LA/San Diego/Sacramento (~1,860 live upstream),
# TfL London JamCams (~870 live upstream). Caps keep the densest cores per
# pack; override per-run for lighter/heavier loads. Kill switches:
# CCTV_CALTRANS_DISTRICTS='' and CCTV_TFL_ENABLED=0.
CCTV_AUSTIN_MAX_SOURCES="${CCTV_AUSTIN_MAX_SOURCES:-250}"
# Use `-` not `:-` so an explicit empty string (the documented kill switch)
# is preserved rather than replaced by the default. Still set-u-safe when unset.
CCTV_CALTRANS_DISTRICTS="${CCTV_CALTRANS_DISTRICTS-4,7,11,3}"
CCTV_CALTRANS_MAX_SOURCES="${CCTV_CALTRANS_MAX_SOURCES:-300}"
CCTV_TFL_ENABLED="${CCTV_TFL_ENABLED:-1}"
CCTV_TFL_MAX_SOURCES="${CCTV_TFL_MAX_SOURCES:-250}"
CCTV_MAX_SOURCES="${CCTV_MAX_SOURCES:-900}"
if command -v npm >/dev/null 2>&1; then
DEV_COMMAND=(npm run dev --)
elif command -v pnpm >/dev/null 2>&1; then
DEV_COMMAND=(pnpm run dev)
else
echo "error: neither npm nor pnpm found"
exit 1
fi
read_dotenv_value() {
local variable_name="$1"
if [[ ! -f ".env" ]]; then
return
fi
if ! command -v node >/dev/null 2>&1; then
echo "warning: node not found; cannot parse .env" >&2
return
fi
node scripts/read-dotenv-value.mjs "${variable_name}"
}
# Vite loads .env for browser build-time configuration, but this launcher needs
# the Maps key before Vite starts. Preserve a shell-provided value; otherwise
# read the project-local .env without executing it as shell code.
GOOGLE_MAPS_API_KEY_ENV="${GOOGLE_MAPS_API_KEY:-}"
GOOGLE_MAPS_API_KEY_ENV_SOURCE="env"
if [[ -z "${GOOGLE_MAPS_API_KEY_ENV}" && -f ".env" ]]; then
GOOGLE_MAPS_API_KEY_ENV="$(read_dotenv_value "GOOGLE_MAPS_API_KEY")"
GOOGLE_MAPS_API_KEY_ENV_SOURCE=".env"
fi
GOOGLE_MAPS_API_KEY_KEYCHAIN=""
GOOGLE_MAPS_API_KEY_SOURCE=""
if command -v security >/dev/null 2>&1; then
for acct in "api-key" "default" "key"; do
GOOGLE_MAPS_API_KEY_KEYCHAIN="$(security find-generic-password -s "google-maps-api" -a "${acct}" -w 2>/dev/null || true)"
if [[ -n "${GOOGLE_MAPS_API_KEY_KEYCHAIN}" ]]; then
GOOGLE_MAPS_API_KEY_SOURCE="keychain:${acct}"
break
fi
done
fi
if [[ -n "${GOOGLE_MAPS_API_KEY_KEYCHAIN}" ]]; then
GOOGLE_MAPS_API_KEY="${GOOGLE_MAPS_API_KEY_KEYCHAIN}"
elif [[ -n "${GOOGLE_MAPS_API_KEY_ENV}" ]]; then
GOOGLE_MAPS_API_KEY="${GOOGLE_MAPS_API_KEY_ENV}"
GOOGLE_MAPS_API_KEY_SOURCE="${GOOGLE_MAPS_API_KEY_ENV_SOURCE}"
else
GOOGLE_MAPS_API_KEY=""
fi
if [[ -z "${GOOGLE_MAPS_API_KEY}" ]]; then
echo "error: Google Maps API key missing."
echo "set GOOGLE_MAPS_API_KEY in env, or add Keychain item: service=google-maps-api account=api-key"
exit 1
fi
read_keychain_secret() {
local service="$1"
local account="$2"
security find-generic-password -s "$service" -a "$account" -w 2>/dev/null || true
}
load_opensky_oauth_from_file() {
local file_path="$1"
if [[ -z "${file_path}" || ! -f "${file_path}" ]]; then
return
fi
if ! command -v node >/dev/null 2>&1; then
echo "warning: node not found; cannot parse OPENSKY_CREDENTIALS_FILE"
return
fi
local parsed
parsed="$(node -e "const fs=require('fs');const p=process.argv[1];try{const raw=JSON.parse(fs.readFileSync(p,'utf8'));const id=String(raw.clientId??raw.client_id??'').trim();const secret=String(raw.clientSecret??raw.client_secret??'').trim();process.stdout.write(id+'\\t'+secret);}catch{process.exit(1)}" -- "${file_path}" 2>/dev/null || true)"
if [[ "${parsed}" != *$'\t'* ]]; then
return
fi
local parsed_client_id="${parsed%%$'\t'*}"
local parsed_client_secret="${parsed#*$'\t'}"
if [[ -z "${OPENSKY_CLIENT_ID}" && -n "${parsed_client_id}" ]]; then
OPENSKY_CLIENT_ID="${parsed_client_id}"
fi
if [[ -z "${OPENSKY_CLIENT_SECRET}" && -n "${parsed_client_secret}" ]]; then
OPENSKY_CLIENT_SECRET="${parsed_client_secret}"
fi
}
resolve_opensky_credentials() {
OPENSKY_AUTH_MODE="${OPENSKY_AUTH_MODE:-$(read_dotenv_value "OPENSKY_AUTH_MODE")}"
OPENSKY_AUTH_MODE="$(printf '%s' "${OPENSKY_AUTH_MODE:-oauth}" | tr '[:upper:]' '[:lower:]')"
OPENSKY_CREDENTIALS_FILE="${OPENSKY_CREDENTIALS_FILE:-$(read_dotenv_value "OPENSKY_CREDENTIALS_FILE")}"
case "${OPENSKY_AUTH_MODE}" in
basic|oauth|auto|anon) ;;
*)
echo "warning: invalid OPENSKY_AUTH_MODE='${OPENSKY_AUTH_MODE}', defaulting to 'oauth'"
OPENSKY_AUTH_MODE="oauth"
;;
esac
# Explicit env wins, then .env, then the credentials file / Keychain below.
OPENSKY_CLIENT_ID="${OPENSKY_CLIENT_ID:-$(read_dotenv_value "OPENSKY_CLIENT_ID")}"
OPENSKY_CLIENT_SECRET="${OPENSKY_CLIENT_SECRET:-$(read_dotenv_value "OPENSKY_CLIENT_SECRET")}"
OPENSKY_USERNAME="${OPENSKY_USERNAME:-$(read_dotenv_value "OPENSKY_USERNAME")}"
OPENSKY_PASSWORD="${OPENSKY_PASSWORD:-$(read_dotenv_value "OPENSKY_PASSWORD")}"
if [[ "${OPENSKY_AUTH_MODE}" == "basic" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_USERNAME}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "username" "user" "login" "email" "default"; do
OPENSKY_USERNAME="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_USERNAME}" ]]; then
break 2
fi
done
done
fi
if [[ "${OPENSKY_AUTH_MODE}" == "basic" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_PASSWORD}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "password" "pass" "token" "secret" "default"; do
OPENSKY_PASSWORD="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_PASSWORD}" ]]; then
break 2
fi
done
done
fi
if [[ "${OPENSKY_AUTH_MODE}" == "oauth" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -n "${OPENSKY_CREDENTIALS_FILE}" ]]; then
load_opensky_oauth_from_file "${OPENSKY_CREDENTIALS_FILE}"
fi
if [[ "${OPENSKY_AUTH_MODE}" == "oauth" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_CLIENT_ID}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "client_id" "client-id" "client" "api-key"; do
OPENSKY_CLIENT_ID="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_CLIENT_ID}" ]]; then
break 2
fi
done
done
fi
if [[ "${OPENSKY_AUTH_MODE}" == "oauth" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_CLIENT_SECRET}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "client_secret" "client-secret" "secret"; do
OPENSKY_CLIENT_SECRET="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_CLIENT_SECRET}" ]]; then
break 2
fi
done
done
fi
case "${OPENSKY_AUTH_MODE}" in
basic)
OPENSKY_CLIENT_ID=""
OPENSKY_CLIENT_SECRET=""
;;
oauth)
OPENSKY_USERNAME=""
OPENSKY_PASSWORD=""
;;
anon)
OPENSKY_CLIENT_ID=""
OPENSKY_CLIENT_SECRET=""
OPENSKY_USERNAME=""
OPENSKY_PASSWORD=""
;;
esac
}
resolve_opensky_credentials
# Optional keys: explicit env wins, followed by .env and Keychain fallback.
# Add to Keychain with e.g.:
# security add-generic-password -U -s "openai-api" -a "api-key" -w
OPENAI_API_KEY="${OPENAI_API_KEY:-$(read_dotenv_value "OPENAI_API_KEY")}"
AISSTREAM_API_KEY="${AISSTREAM_API_KEY:-$(read_dotenv_value "AISSTREAM_API_KEY")}"
CESIUM_ION_TOKEN="${CESIUM_ION_TOKEN:-$(read_dotenv_value "CESIUM_ION_TOKEN")}"
LL2_API_TOKEN="${LL2_API_TOKEN:-$(read_dotenv_value "LL2_API_TOKEN")}"
TOMTOM_API_KEY="${TOMTOM_API_KEY:-$(read_dotenv_value "TOMTOM_API_KEY")}"
FIRMS_MAP_KEY="${FIRMS_MAP_KEY:-$(read_dotenv_value "FIRMS_MAP_KEY")}"
OPENAI_API_KEY="${OPENAI_API_KEY:-$(read_keychain_secret "openai-api" "api-key")}"
AISSTREAM_API_KEY="${AISSTREAM_API_KEY:-$(read_keychain_secret "aisstream-api" "api-key")}"
CESIUM_ION_TOKEN="${CESIUM_ION_TOKEN:-$(read_keychain_secret "cesium-ion" "token")}"
TOMTOM_API_KEY="${TOMTOM_API_KEY:-$(read_keychain_secret "tomtom-api" "api-key")}"
FIRMS_MAP_KEY="${FIRMS_MAP_KEY:-$(read_keychain_secret "firms-map" "map-key")}"
if [[ ! -f "src/data/cctv.js" ]]; then
echo "error: expected CCTV layer file missing: src/data/cctv.js"
exit 1
fi
if ! grep -q "dataManager.register(cctvLayer)" src/main.js; then
echo "error: CCTV layer not wired in src/main.js"
exit 1
fi
echo "Stopping all existing God's Eye View dev servers..."
pkill -f "${ROOT_DIR}/node_modules/.bin/vite" >/dev/null 2>&1 || true
pkill -f "${ROOT_DIR}/node_modules/vite/bin/vite.js" >/dev/null 2>&1 || true
# Also clear the requested port in case it is held by a stale wrapper or a
# server started through a different package-manager command.
if command -v lsof >/dev/null 2>&1; then
PIDS="$(lsof -tiTCP:${PORT} -sTCP:LISTEN 2>/dev/null || true)"
if [[ -n "${PIDS}" ]]; then
echo "${PIDS}" | xargs kill -9 >/dev/null 2>&1 || true
fi
fi
echo "Clearing Vite cache..."
rm -rf node_modules/.vite
echo "Starting fresh God's Eye View dev server..."
case "${HOST}" in
localhost|127.0.0.1|::1)
echo "Local-only mode: reachable at http://localhost:${PORT}/ (set HOST=0.0.0.0 for LAN)"
;;
*)
LAN_IP=""
if command -v ipconfig >/dev/null 2>&1; then
# macOS: first active interface wins
for iface in en0 en1; do
LAN_IP="$(ipconfig getifaddr "${iface}" 2>/dev/null || true)"
[[ -n "${LAN_IP}" ]] && break
done
elif command -v hostname >/dev/null 2>&1; then
# Linux: hostname -I lists addresses; take the first
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
fi
echo ""
echo "!! =============================================================="
echo "!! WARNING: HOST=${HOST} — network-exposed mode."
echo "!! This dev server brokers your configured API keys (OpenAI,"
echo "!! OpenSky, AISStream, TomTom, FIRMS, LL2, Google) to ANYONE who can"
echo "!! reach it on the network. Use only on networks you trust."
echo "!! Consider the opt-in per-IP throttles GEV_RATELIMIT_OPENAI_PER_MIN"
echo "!! and GEV_RATELIMIT_GOOGLE_PER_MIN (see .env.example) — and note"
echo "!! they are NOT billing caps; set provider-side budget alerts too."
if [[ -n "${LAN_IP}" ]]; then
echo "!! LAN URL: http://${LAN_IP}:${PORT}/"
else
echo "!! LAN URL: http://<this-machine-ip>:${PORT}/"
fi
echo "!! =============================================================="
echo ""
echo "URL (this machine): http://localhost:${PORT}/"
;;
esac
echo "Google Maps key source: ${GOOGLE_MAPS_API_KEY_SOURCE}"
echo "Tip: after server starts, hard refresh browser (Cmd+Shift+R)."
echo "If panels are still missing, run this once in browser console:"
echo "localStorage.removeItem('godsEyeView.v6.panelPos.cctv-panel'); location.reload();"
echo "OpenSky auth mode: ${OPENSKY_AUTH_MODE}"
if [[ -n "${OPENSKY_CREDENTIALS_FILE}" ]]; then
if [[ -f "${OPENSKY_CREDENTIALS_FILE}" ]]; then
echo "OpenSky credentials file: ${OPENSKY_CREDENTIALS_FILE}"
else
echo "OpenSky credentials file: ${OPENSKY_CREDENTIALS_FILE} (missing)"
fi
fi
case "${OPENSKY_AUTH_MODE}" in
basic)
if [[ -n "${OPENSKY_USERNAME}" && -n "${OPENSKY_PASSWORD}" ]]; then
echo "OpenSky basic auth: configured"
else
echo "OpenSky basic auth: missing credentials"
fi
;;
oauth)
if [[ -n "${OPENSKY_CLIENT_ID}" && -n "${OPENSKY_CLIENT_SECRET}" ]]; then
echo "OpenSky OAuth: configured"
else
echo "OpenSky OAuth: missing client credentials"
fi
;;
auto)
if [[ -n "${OPENSKY_CLIENT_ID}" && -n "${OPENSKY_CLIENT_SECRET}" ]]; then
echo "OpenSky auto auth: OAuth configured"
elif [[ -n "${OPENSKY_USERNAME}" && -n "${OPENSKY_PASSWORD}" ]]; then
echo "OpenSky auto auth: basic configured"
else
echo "OpenSky auto auth: no credentials found"
fi
;;
anon)
echo "OpenSky auth: disabled (anonymous mode)"
;;
esac
[[ -n "${OPENAI_API_KEY}" ]] && echo "OpenAI key (voice + HUD summary): configured" || echo "OpenAI key (voice + HUD summary): not set — GEV MIC disabled"
[[ -n "${AISSTREAM_API_KEY}" ]] && echo "AISStream key (live vessels): configured" || echo "AISStream key (live vessels): not set — ships layer empty"
[[ -n "${CESIUM_ION_TOKEN}" ]] && echo "Cesium ion token (Bing map stacks): configured" || echo "Cesium ion token (Bing map stacks): not set — Google 3D/OSM only"
[[ -n "${TOMTOM_API_KEY}" ]] && echo "TomTom key (live traffic flow): configured" || echo "TomTom key (live traffic flow): not set — simulated traffic"
[[ -n "${FIRMS_MAP_KEY}" ]] && echo "NASA FIRMS key (live fires): configured" || echo "NASA FIRMS key (live fires): not set — fires layer requires a key"
[[ -n "${LL2_API_TOKEN}" ]] && echo "Launch Library 2 token: configured" || echo "Launch Library 2 token: not set — using public access"
# Build the dev server environment explicitly. A value that resolved to
# nothing is left UNSET instead of being exported empty: Vite backfills its
# own .env values only for variables it finds undefined, so an empty export
# would shadow a key the user did configure in .env.
DEV_ENV=()
DEV_UNSET=()
put_env() {
DEV_ENV+=("$1=$2")
}
put_env_if_set() {
if [[ -n "$2" ]]; then
DEV_ENV+=("$1=$2")
else
# Leaving it out is not enough: the child inherits this shell's
# environment, so an empty export made in the PARENT would pass straight
# through and shadow .env just the same. Remove it from the child outright.
DEV_UNSET+=(-u "$1")
fi
}
put_env GOOGLE_MAPS_API_KEY "${GOOGLE_MAPS_API_KEY}"
put_env CCTV_AUSTIN_MAX_SOURCES "${CCTV_AUSTIN_MAX_SOURCES}"
# Empty is the documented Caltrans kill switch, so this one is passed as-is.
put_env CCTV_CALTRANS_DISTRICTS "${CCTV_CALTRANS_DISTRICTS}"
put_env CCTV_CALTRANS_MAX_SOURCES "${CCTV_CALTRANS_MAX_SOURCES}"
put_env CCTV_TFL_ENABLED "${CCTV_TFL_ENABLED}"
put_env CCTV_TFL_MAX_SOURCES "${CCTV_TFL_MAX_SOURCES}"
put_env_if_set TFL_APP_KEY "${TFL_APP_KEY:-}"
put_env CCTV_MAX_SOURCES "${CCTV_MAX_SOURCES}"
put_env OPENSKY_AUTH_MODE "${OPENSKY_AUTH_MODE}"
put_env_if_set OPENSKY_CREDENTIALS_FILE "${OPENSKY_CREDENTIALS_FILE}"
put_env_if_set OPENSKY_CLIENT_ID "${OPENSKY_CLIENT_ID}"
put_env_if_set OPENSKY_CLIENT_SECRET "${OPENSKY_CLIENT_SECRET}"
put_env_if_set OPENSKY_USERNAME "${OPENSKY_USERNAME}"
put_env_if_set OPENSKY_PASSWORD "${OPENSKY_PASSWORD}"
put_env_if_set OPENAI_API_KEY "${OPENAI_API_KEY}"
put_env_if_set AISSTREAM_API_KEY "${AISSTREAM_API_KEY}"
put_env_if_set CESIUM_ION_TOKEN "${CESIUM_ION_TOKEN}"
put_env_if_set TOMTOM_API_KEY "${TOMTOM_API_KEY}"
put_env_if_set FIRMS_MAP_KEY "${FIRMS_MAP_KEY}"
put_env_if_set LL2_API_TOKEN "${LL2_API_TOKEN}"
env ${DEV_UNSET[@]+"${DEV_UNSET[@]}"} "${DEV_ENV[@]}" "${DEV_COMMAND[@]}" --host "${HOST}" --port "${PORT}" --force
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PORT="${PORT:-4173}"
# Local-only by default; set HOST=0.0.0.0 explicitly to expose on the LAN.
HOST="${HOST:-localhost}"
GOOGLE_MAPS_API_KEY_ENV="${GOOGLE_MAPS_API_KEY:-}"
GOOGLE_MAPS_API_KEY_KEYCHAIN=""
GOOGLE_MAPS_API_KEY_SOURCE=""
if command -v security >/dev/null 2>&1; then
for acct in "api-key" "default" "key"; do
GOOGLE_MAPS_API_KEY_KEYCHAIN="$(security find-generic-password -s "google-maps-api" -a "${acct}" -w 2>/dev/null || true)"
if [[ -n "${GOOGLE_MAPS_API_KEY_KEYCHAIN}" ]]; then
GOOGLE_MAPS_API_KEY_SOURCE="keychain:${acct}"
break
fi
done
fi
if [[ -n "${GOOGLE_MAPS_API_KEY_KEYCHAIN}" ]]; then
GOOGLE_MAPS_API_KEY="${GOOGLE_MAPS_API_KEY_KEYCHAIN}"
elif [[ -n "${GOOGLE_MAPS_API_KEY_ENV}" ]]; then
GOOGLE_MAPS_API_KEY="${GOOGLE_MAPS_API_KEY_ENV}"
GOOGLE_MAPS_API_KEY_SOURCE="env"
else
GOOGLE_MAPS_API_KEY=""
fi
if [[ -z "${GOOGLE_MAPS_API_KEY}" ]]; then
echo "error: Google Maps API key missing."
echo "set GOOGLE_MAPS_API_KEY in env, or add Keychain item: service=google-maps-api account=api-key"
exit 1
fi
read_keychain_secret() {
local service="$1"
local account="$2"
security find-generic-password -s "$service" -a "$account" -w 2>/dev/null || true
}
load_opensky_oauth_from_file() {
local file_path="$1"
if [[ -z "${file_path}" || ! -f "${file_path}" ]]; then
return
fi
if ! command -v node >/dev/null 2>&1; then
echo "warning: node not found; cannot parse OPENSKY_CREDENTIALS_FILE"
return
fi
local parsed
parsed="$(node -e "const fs=require('fs');const p=process.argv[1];try{const raw=JSON.parse(fs.readFileSync(p,'utf8'));const id=String(raw.clientId??raw.client_id??'').trim();const secret=String(raw.clientSecret??raw.client_secret??'').trim();process.stdout.write(id+'\\t'+secret);}catch{process.exit(1)}" "${file_path}" 2>/dev/null || true)"
if [[ "${parsed}" != *$'\t'* ]]; then
return
fi
local parsed_client_id="${parsed%%$'\t'*}"
local parsed_client_secret="${parsed#*$'\t'}"
if [[ -z "${OPENSKY_CLIENT_ID}" && -n "${parsed_client_id}" ]]; then
OPENSKY_CLIENT_ID="${parsed_client_id}"
fi
if [[ -z "${OPENSKY_CLIENT_SECRET}" && -n "${parsed_client_secret}" ]]; then
OPENSKY_CLIENT_SECRET="${parsed_client_secret}"
fi
}
resolve_opensky_credentials() {
OPENSKY_AUTH_MODE="$(printf '%s' "${OPENSKY_AUTH_MODE:-oauth}" | tr '[:upper:]' '[:lower:]')"
OPENSKY_CREDENTIALS_FILE="${OPENSKY_CREDENTIALS_FILE:-}"
case "${OPENSKY_AUTH_MODE}" in
basic|oauth|auto|anon) ;;
*)
echo "warning: invalid OPENSKY_AUTH_MODE='${OPENSKY_AUTH_MODE}', defaulting to 'oauth'"
OPENSKY_AUTH_MODE="oauth"
;;
esac
OPENSKY_CLIENT_ID="${OPENSKY_CLIENT_ID:-}"
OPENSKY_CLIENT_SECRET="${OPENSKY_CLIENT_SECRET:-}"
OPENSKY_USERNAME="${OPENSKY_USERNAME:-}"
OPENSKY_PASSWORD="${OPENSKY_PASSWORD:-}"
if [[ "${OPENSKY_AUTH_MODE}" == "basic" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_USERNAME}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "username" "user" "login" "email" "default"; do
OPENSKY_USERNAME="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_USERNAME}" ]]; then
break 2
fi
done
done
fi
if [[ "${OPENSKY_AUTH_MODE}" == "basic" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_PASSWORD}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "password" "pass" "token" "secret" "default"; do
OPENSKY_PASSWORD="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_PASSWORD}" ]]; then
break 2
fi
done
done
fi
if [[ "${OPENSKY_AUTH_MODE}" == "oauth" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -n "${OPENSKY_CREDENTIALS_FILE}" ]]; then
load_opensky_oauth_from_file "${OPENSKY_CREDENTIALS_FILE}"
fi
if [[ "${OPENSKY_AUTH_MODE}" == "oauth" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_CLIENT_ID}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "client_id" "client-id" "client" "api-key"; do
OPENSKY_CLIENT_ID="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_CLIENT_ID}" ]]; then
break 2
fi
done
done
fi
if [[ "${OPENSKY_AUTH_MODE}" == "oauth" || "${OPENSKY_AUTH_MODE}" == "auto" ]] && [[ -z "${OPENSKY_CLIENT_SECRET}" ]]; then
for svc in "opensky-network" "opensky"; do
for acct in "client_secret" "client-secret" "secret"; do
OPENSKY_CLIENT_SECRET="$(read_keychain_secret "${svc}" "${acct}")"
if [[ -n "${OPENSKY_CLIENT_SECRET}" ]]; then
break 2
fi
done
done
fi
case "${OPENSKY_AUTH_MODE}" in
basic)
OPENSKY_CLIENT_ID=""
OPENSKY_CLIENT_SECRET=""
;;
oauth)
OPENSKY_USERNAME=""
OPENSKY_PASSWORD=""
;;
anon)
OPENSKY_CLIENT_ID=""
OPENSKY_CLIENT_SECRET=""
OPENSKY_USERNAME=""
OPENSKY_PASSWORD=""
;;
esac
}
resolve_opensky_credentials
CCTV_AUSTIN_MAX_SOURCES="${CCTV_AUSTIN_MAX_SOURCES:-36}"
CCTV_MAX_SOURCES="${CCTV_MAX_SOURCES:-48}"
echo "Starting God's Eye View dev server..."
echo "URL: http://localhost:${PORT}/"
echo "Google Maps key source: ${GOOGLE_MAPS_API_KEY_SOURCE}"
echo "OpenSky auth mode: ${OPENSKY_AUTH_MODE}"
if [[ -n "${OPENSKY_CREDENTIALS_FILE}" ]]; then
if [[ -f "${OPENSKY_CREDENTIALS_FILE}" ]]; then
echo "OpenSky credentials file: ${OPENSKY_CREDENTIALS_FILE}"
else
echo "OpenSky credentials file: ${OPENSKY_CREDENTIALS_FILE} (missing)"
fi
fi
case "${OPENSKY_AUTH_MODE}" in
basic)
if [[ -n "${OPENSKY_USERNAME}" && -n "${OPENSKY_PASSWORD}" ]]; then
echo "OpenSky basic auth: configured"
else
echo "OpenSky basic auth: missing credentials"
fi
;;
oauth)
if [[ -n "${OPENSKY_CLIENT_ID}" && -n "${OPENSKY_CLIENT_SECRET}" ]]; then
echo "OpenSky OAuth: configured"
else
echo "OpenSky OAuth: missing client credentials"
fi
;;
auto)
if [[ -n "${OPENSKY_CLIENT_ID}" && -n "${OPENSKY_CLIENT_SECRET}" ]]; then
echo "OpenSky auto auth: OAuth configured"
elif [[ -n "${OPENSKY_USERNAME}" && -n "${OPENSKY_PASSWORD}" ]]; then
echo "OpenSky auto auth: basic configured"
else
echo "OpenSky auto auth: no credentials found"
fi
;;
anon)
echo "OpenSky auth: disabled (anonymous mode)"
;;
esac
GOOGLE_MAPS_API_KEY="${GOOGLE_MAPS_API_KEY}" \
CCTV_AUSTIN_MAX_SOURCES="${CCTV_AUSTIN_MAX_SOURCES}" \
CCTV_MAX_SOURCES="${CCTV_MAX_SOURCES}" \
OPENSKY_AUTH_MODE="${OPENSKY_AUTH_MODE}" \
OPENSKY_CREDENTIALS_FILE="${OPENSKY_CREDENTIALS_FILE}" \
OPENSKY_CLIENT_ID="${OPENSKY_CLIENT_ID}" \
OPENSKY_CLIENT_SECRET="${OPENSKY_CLIENT_SECRET}" \
OPENSKY_USERNAME="${OPENSKY_USERNAME}" \
OPENSKY_PASSWORD="${OPENSKY_PASSWORD}" \
npm run dev -- --host "${HOST}" --port "${PORT}"
+7
View File
@@ -0,0 +1,7 @@
# Voice QA fixture
`full-globe-turn-on-radio.wav` is an owner-generated fake-microphone fixture for the credentialed AI-to-Radio acceptance test. Its embedded WAV metadata is the provenance record for the generator, model, voice, spoken text, usage terms, and audio settings; inspect it with `ffprobe -show_entries format_tags -of json scripts/fixtures/voice/full-globe-turn-on-radio.wav`.
- Provided and approved for repository QA use by the user on 2026-08-05.
- SHA-256: `b57af70db1922b72fec2c6c58348ccd3309e10aa1e8edec2890277dff26cc7bb`
- Run: `node scripts/qa-voice-wav.mjs http://localhost:4189`
Binary file not shown.
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env node
import assert from 'node:assert/strict';
import {
advanceSpriteFocus,
} from '../src/data/focusDeemphasis.js';
import {
applyAircraftBillboardTreatment,
} from '../src/data/aircraftRecession.js';
if (typeof global.gc !== 'function') {
throw new Error('focus-allocation-check.mjs requires node --expose-gc');
}
const CALLS = 100_000;
const TRIALS = 9;
const WARMUP_CALLS = 200_000;
const PURE_CALL_BUDGET = 16;
const PRODUCTION_SPRITE_TICK_BUDGET = 212;
function median(values) {
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.floor(sorted.length / 2)];
}
function bytesPerCall(fn) {
for (let index = 0; index < WARMUP_CALLS; index += 1) fn();
const samples = [];
for (let trial = 0; trial < TRIALS; trial += 1) {
global.gc();
const before = process.memoryUsage().heapUsed;
let checksum = 0;
for (let index = 0; index < CALLS; index += 1) checksum += fn();
const after = process.memoryUsage().heapUsed;
// Keep the returned scalar observable so V8 cannot delete the calls.
assert.ok(Number.isFinite(checksum));
samples.push((after - before) / CALLS);
}
const rawMedian = median(samples);
return { rawMedian, samples };
}
function netOfLoopBaseline(observed, baseline) {
// V8's callback/checksum loop itself retains roughly one boxed-number word
// per call until the next collection. Subtract the identically measured
// no-op loop so this gate reports allocation attributable to the treatment.
const rawMedian = Math.max(0, observed.rawMedian - baseline.rawMedian);
return {
rawMedian,
roundedMedian: Math.round(rawMedian),
observedRawMedian: observed.rawMedian,
baselineRawMedian: baseline.rawMedian,
samples: observed.samples,
};
}
const target = {
screenRect: { left: 0, top: 0, right: 10, bottom: 10 },
paddingPx: 18,
cameraDistance: 1000,
};
const sprite = {};
const focusInput = {
screenPosition: { x: 5, y: 5 },
cameraDistance: 1200,
nowMs: 1000,
target,
};
const baseColor = { withAlpha: (alpha) => ({ alpha }) };
const billboard = { scale: 1, color: { alpha: 1 } };
const aircraftInput = {
billboard,
baseScale: 1,
baseAlpha: 1,
baseColor,
focusFactor: 1,
cameraDistanceM: 100,
cameraHeightM: 1000,
};
const baseline = bytesPerCall(() => focusInput.nowMs);
const focus = netOfLoopBaseline(
bytesPerCall(() => advanceSpriteFocus(sprite, focusInput).factor),
baseline,
);
const aircraft = netOfLoopBaseline(
bytesPerCall(() => applyAircraftBillboardTreatment(aircraftInput).alpha),
baseline,
);
const productionSpriteTick = netOfLoopBaseline(
bytesPerCall(() => {
// Match the hot civilian/military sprite path: the caller constructs fresh
// option literals, consumes the shared focus result immediately, and then
// constructs the composed aircraft-treatment options for the same sprite.
const focusResult = advanceSpriteFocus(sprite, {
screenPosition: focusInput.screenPosition,
cameraDistance: focusInput.cameraDistance,
nowMs: focusInput.nowMs,
target,
});
const treatment = applyAircraftBillboardTreatment({
billboard,
baseScale: aircraftInput.baseScale,
baseAlpha: aircraftInput.baseAlpha,
baseColor,
focusFactor: focusResult.factor,
cameraDistanceM: aircraftInput.cameraDistanceM,
cameraHeightM: aircraftInput.cameraHeightM,
});
return focusResult.factor + treatment.alpha;
}),
baseline,
);
assert.ok(
focus.roundedMedian <= PURE_CALL_BUDGET,
`advanceSpriteFocus allocated ${focus.rawMedian.toFixed(3)} B/call median`,
);
assert.ok(
aircraft.roundedMedian <= PURE_CALL_BUDGET,
`applyAircraftBillboardTreatment allocated ${aircraft.rawMedian.toFixed(3)} B/call median`,
);
assert.ok(
productionSpriteTick.roundedMedian <= PRODUCTION_SPRITE_TICK_BUDGET,
`production sprite tick allocated ${productionSpriteTick.rawMedian.toFixed(3)} B/call median`,
);
console.log(JSON.stringify({
callsPerTrial: CALLS,
trials: TRIALS,
budgets: {
pureCall: PURE_CALL_BUDGET,
productionSpriteTick: PRODUCTION_SPRITE_TICK_BUDGET,
},
loopBaseline: baseline,
advanceSpriteFocus: focus,
applyAircraftBillboardTreatment: aircraft,
productionSpriteTick,
}));
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT_DIR}"
if ! command -v security >/dev/null 2>&1; then
echo "error: macOS 'security' command not found"
exit 1
fi
if ! command -v node >/dev/null 2>&1; then
echo "error: node is required to parse JSON credentials"
exit 1
fi
CREDENTIALS_FILE="${1:-${OPENSKY_CREDENTIALS_FILE:-}}"
if [[ -z "${CREDENTIALS_FILE}" ]]; then
echo "usage: ./scripts/opensky-import-client.sh /path/to/credentials.json"
echo "or set OPENSKY_CREDENTIALS_FILE and run ./scripts/opensky-import-client.sh"
exit 1
fi
if [[ ! -f "${CREDENTIALS_FILE}" ]]; then
echo "error: credentials file not found: ${CREDENTIALS_FILE}"
exit 1
fi
PARSED="$(node -e "const fs=require('fs');const p=process.argv[1];const raw=JSON.parse(fs.readFileSync(p,'utf8'));const id=String(raw.clientId??raw.client_id??'').trim();const secret=String(raw.clientSecret??raw.client_secret??'').trim();if(!id||!secret){process.exit(2)};process.stdout.write(id+'\t'+secret);" "${CREDENTIALS_FILE}" 2>/dev/null || true)"
if [[ "${PARSED}" != *$'\t'* ]]; then
echo "error: could not parse clientId/clientSecret from ${CREDENTIALS_FILE}"
exit 1
fi
CLIENT_ID="${PARSED%%$'\t'*}"
CLIENT_SECRET="${PARSED#*$'\t'}"
if [[ -z "${CLIENT_ID}" || -z "${CLIENT_SECRET}" ]]; then
echo "error: credentials file is missing client id or client secret"
exit 1
fi
security add-generic-password -U -s "opensky-network" -a "client_id" -w "${CLIENT_ID}" >/dev/null
security add-generic-password -U -s "opensky-network" -a "client_secret" -w "${CLIENT_SECRET}" >/dev/null
echo "OpenSky OAuth client credentials stored in Keychain:"
echo " service=opensky-network account=client_id"
echo " service=opensky-network account=client_secret"
+398
View File
@@ -0,0 +1,398 @@
/**
* qa-attribution-b12.mjs — visual + state proof for Batch 12 (data attribution).
*
* Public attribution checks:
* H10 — the Google/Cesium credit MUST stay visible in clean-view AND
* recording modes (those are the modes used to record demos).
* H11 — every data layer's required attribution must surface in the
* expandable bottom-left "Data attribution" lightbox.
*
* This script drives the REAL app headless and proves:
* (i) per-layer credits are registered in viewer.creditDisplay (H11),
* and appear in the "Data attribution" lightbox when opened;
* (ii) enabling datacenters + submarine cables keeps their credits present;
* (iii) toggling clean-view keeps #cesium-credits visible (screenshot);
* (iv) toggling recording-mode keeps #cesium-credits visible (screenshot).
*
* Run: node scripts/qa-attribution-b12.mjs --url http://localhost:4300
*/
import puppeteer from 'puppeteer';
import { existsSync, mkdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const argv = process.argv.slice(2);
function getOpt(flag, def) {
const i = argv.indexOf(flag);
return i >= 0 && argv[i + 1] ? argv[i + 1] : def;
}
const APP_URL = getOpt('--url', 'http://localhost:4300');
const APP_ORIGIN = new URL(APP_URL).origin;
const SHOT_DIR = resolve(__dirname, '..', 'qa-shots', 'b12');
mkdirSync(SHOT_DIR, { recursive: true });
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
return CHROME_EXECUTABLE_CANDIDATES.find((candidate) => {
try { return existsSync(candidate); } catch { return false; }
}) || null;
}
let passed = 0;
let failed = 0;
function check(name, ok, detail) {
const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
if (ok) passed++;
else failed++;
}
// Substrings that MUST be present across the registered per-layer credits.
const REQUIRED_CREDIT_SUBSTRINGS = [
'OpenStreetMap contributors', // ODbL — datacenters/dams/roads
'adsb.lol', // ODbL — military traces
'TeleGeography', // CC BY-NC-SA — cables
'NASA FIRMS', // fires
'CelesTrak', // satellites
'U.S. Geological Survey', // earthquakes
'OpenSky Network', // flights
'AISStream', // vessels
'City of Austin', // CCTV
'Radio Browser', // internet-radio directory
];
async function main() {
const chromeExecutable = findChromeExecutable();
const browser = await puppeteer.launch({
headless: 'new',
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.setRequestInterception(true);
page.on('request', (request) => {
const url = new URL(request.url());
if (url.origin === APP_ORIGIN && url.pathname === '/api/openai/hud-summary') {
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ summary: 'QA globe ready' }),
});
return;
}
request.continue();
});
const consoleErrors = [];
const failedResponses = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
const sourceUrl = msg.location()?.url || '';
consoleErrors.push(sourceUrl ? `${msg.text()} [${sourceUrl}]` : msg.text());
}
});
page.on('response', (response) => {
if (response.status() >= 500) {
failedResponses.push(`HTTP ${response.status()} ${response.url()}`);
}
});
console.log(`\n qa-attribution-b12 → ${APP_URL}\n`);
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
// Wait for the app + viewer.creditDisplay to be live.
await page.waitForFunction(
() => window.__godsEyeView && window.__godsEyeView.viewer && window.__godsEyeView.viewer.creditDisplay,
{ timeout: 60000 },
);
// Give Cesium a few frames to render the on-screen credit line (logo + link).
await new Promise((r) => setTimeout(r, 2500));
// ── (i) H11: static credits registered ────────────────────────────
console.log('H11 — per-layer credits registered in viewer.creditDisplay');
const registeredHtml = await page.evaluate(() => {
const cd = window.__godsEyeView.viewer.creditDisplay;
const statics = cd._staticCredits || [];
return statics.map((c) => c.html);
});
check(
'creditDisplay has static per-layer credits',
registeredHtml.length >= 10,
`${registeredHtml.length} static credits registered`,
);
for (const sub of REQUIRED_CREDIT_SUBSTRINGS) {
const found = registeredHtml.some((h) => h.includes(sub));
check(`credit present: "${sub}"`, found, found ? '' : 'MISSING');
}
// ── (i) H11: credits render in the "Data attribution" lightbox ─────
console.log('\nH11 — "Data attribution" lightbox surfaces the credits');
// Force a render frame so the credit display flushes, then open the lightbox.
const lightboxState = await page.evaluate(async () => {
const viewer = window.__godsEyeView.viewer;
viewer.scene.requestRender();
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
const cd = viewer.creditDisplay;
cd.showLightbox();
viewer.scene.requestRender();
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
const box = document.querySelector('.cesium-credit-lightbox');
const list = box?.querySelector(':scope > ul');
const title = box?.querySelector('.cesium-credit-lightbox-title');
const close = box?.querySelector('.cesium-credit-lightbox-close');
const boxRect = box?.getBoundingClientRect();
const listRect = list?.getBoundingClientRect();
const overlay = document.querySelector('.cesium-credit-lightbox-overlay');
const worldOverlay = document.getElementById('world-overlay-root');
document.querySelector('[data-qa-attribution-stack-probe]')?.remove();
const stackProbe = document.createElement('div');
stackProbe.dataset.qaAttributionStackProbe = 'true';
stackProbe.textContent = 'QA WORLD LABEL — STACK PROBE';
Object.assign(stackProbe.style, {
position: 'absolute',
left: `${Math.max(8, (boxRect?.left || 0) - 90)}px`,
top: `${Math.max(8, (boxRect?.top || 0) + 150)}px`,
width: '180px',
padding: '5px 8px',
color: '#8ffcff',
background: 'rgba(4, 18, 28, 0.94)',
borderLeft: '3px solid #19d9e8',
font: '11px monospace',
whiteSpace: 'nowrap',
pointerEvents: 'none',
});
worldOverlay?.appendChild(stackProbe);
const stackProbeRect = stackProbe.getBoundingClientRect();
const lastItem = list?.lastElementChild;
if (list) list.scrollTop = list.scrollHeight;
const lastRect = lastItem?.getBoundingClientRect();
return {
html: box?.innerHTML || '',
itemCount: list?.children.length || 0,
linkCount: list?.querySelectorAll('a[href]').length || 0,
everyLinkHasDestination: [...(list?.querySelectorAll('a') || [])]
.every((link) => Boolean(link.getAttribute('href'))),
boxHeight: boxRect?.height || 0,
viewportHeight: window.innerHeight,
listClientHeight: list?.clientHeight || 0,
listScrollHeight: list?.scrollHeight || 0,
listClientWidth: list?.clientWidth || 0,
listScrollWidth: list?.scrollWidth || 0,
listOverflowY: list ? getComputedStyle(list).overflowY : '',
titleVisible: Boolean(title?.getBoundingClientRect().height),
closeVisible: Boolean(close?.getBoundingClientRect().height),
overlayZIndex: Number(getComputedStyle(overlay).zIndex),
worldOverlayZIndex: Number(getComputedStyle(worldOverlay).zIndex),
stackProbeIntersectsModal: Boolean(boxRect
&& stackProbeRect.left < boxRect.left
&& stackProbeRect.right > boxRect.left
&& stackProbeRect.top < boxRect.bottom
&& stackProbeRect.bottom > boxRect.top),
stackProbeExtendsOutsideModal: Boolean(boxRect && stackProbeRect.left < boxRect.left),
lastItemReachable: Boolean(lastRect && listRect
&& lastRect.bottom <= listRect.bottom + 1
&& lastRect.top >= listRect.top - 1),
};
});
const lightboxHtml = lightboxState.html;
const lightboxHits = REQUIRED_CREDIT_SUBSTRINGS.filter((s) => lightboxHtml.includes(s));
check(
'lightbox renders the per-layer credits',
lightboxHits.length >= REQUIRED_CREDIT_SUBSTRINGS.length - 1,
`${lightboxHits.length}/${REQUIRED_CREDIT_SUBSTRINGS.length} required strings visible in lightbox`,
);
check(
'desktop lightbox stays compact with a fixed title and close control',
lightboxState.boxHeight > 0
&& lightboxState.boxHeight <= lightboxState.viewportHeight * 0.71
&& lightboxState.titleVisible
&& lightboxState.closeVisible,
`box=${Math.round(lightboxState.boxHeight)}px viewport=${lightboxState.viewportHeight}px`,
);
check(
'complete credit list scrolls vertically without horizontal overflow',
lightboxState.itemCount >= registeredHtml.length
&& lightboxState.linkCount > 0
&& lightboxState.everyLinkHasDestination
&& lightboxState.listScrollHeight > lightboxState.listClientHeight
&& /auto|scroll/.test(lightboxState.listOverflowY)
&& lightboxState.listScrollWidth <= lightboxState.listClientWidth + 1
&& lightboxState.lastItemReachable,
`${lightboxState.itemCount} items, ${lightboxState.linkCount} links, list=${lightboxState.listClientHeight}/${lightboxState.listScrollHeight}px`,
);
check(
'attribution modal stacks above shared world labels',
Number.isFinite(lightboxState.overlayZIndex)
&& Number.isFinite(lightboxState.worldOverlayZIndex)
&& lightboxState.overlayZIndex > lightboxState.worldOverlayZIndex
&& lightboxState.stackProbeIntersectsModal
&& lightboxState.stackProbeExtendsOutsideModal,
`modal z=${lightboxState.overlayZIndex}, world labels z=${lightboxState.worldOverlayZIndex}, overlap=${lightboxState.stackProbeIntersectsModal}`,
);
await page.evaluate(() => window.__godsEyeView.viewer.creditDisplay.hideLightbox());
await page.screenshot({ path: resolve(SHOT_DIR, 'attribution-stacking-before-desktop.png') });
await page.evaluate(() => {
window.__godsEyeView.viewer.creditDisplay.showLightbox();
const list = document.querySelector('.cesium-credit-lightbox > ul');
if (list) list.scrollTop = 0;
});
await page.screenshot({ path: resolve(SHOT_DIR, 'attribution-lightbox-desktop.png') });
await page.evaluate(() => {
window.__godsEyeView.viewer.creditDisplay.hideLightbox();
document.querySelector('[data-qa-attribution-stack-probe]')?.remove();
});
await page.setViewport({ width: 560, height: 760, deviceScaleFactor: 1 });
await page.evaluate(async () => {
const viewer = window.__godsEyeView.viewer;
viewer.resize();
viewer.scene.requestRender();
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
});
const mobileLightbox = await page.evaluate(() => {
const viewer = window.__godsEyeView.viewer;
viewer.creditDisplay.showLightbox();
const box = document.querySelector('.cesium-credit-lightbox');
const list = box?.querySelector(':scope > ul');
const rect = box?.getBoundingClientRect();
if (list) list.scrollTop = list.scrollHeight;
const listRect = list?.getBoundingClientRect();
const lastRect = list?.lastElementChild?.getBoundingClientRect();
return {
left: rect?.left || 0,
right: rect?.right || 0,
top: rect?.top || 0,
bottom: rect?.bottom || 0,
viewportWidth: innerWidth,
viewportHeight: innerHeight,
lastItemReachable: Boolean(lastRect && listRect
&& lastRect.bottom <= listRect.bottom + 1),
};
});
check(
'mobile lightbox remains full-screen edge-to-edge and reaches the final credit',
Math.abs(mobileLightbox.left) <= 1
&& Math.abs(mobileLightbox.right - mobileLightbox.viewportWidth) <= 1
&& Math.abs(mobileLightbox.top) <= 1
&& Math.abs(mobileLightbox.bottom - mobileLightbox.viewportHeight) <= 1
&& mobileLightbox.lastItemReachable,
`box=${Math.round(mobileLightbox.left)},${Math.round(mobileLightbox.top)}–${Math.round(mobileLightbox.right)},${Math.round(mobileLightbox.bottom)}`,
);
await page.screenshot({ path: resolve(SHOT_DIR, 'attribution-lightbox-mobile.png') });
await page.evaluate(() => window.__godsEyeView.viewer.creditDisplay.hideLightbox());
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
// ── (ii) enable datacenters + cables; credits still present ────────
console.log('\nH11 — enabling datacenters + submarine cables');
const layerIds = await page.evaluate(() => {
const dm = window.__godsEyeView.dataManager;
return [...dm.layers.keys()];
});
// Find the datacenter + cable layer ids by fuzzy match on the id string.
const dcId = layerIds.find((id) => /datacenter/i.test(id));
const cableId = layerIds.find((id) => /cable|submarine|telegeo/i.test(id));
check('found datacenter + cable layer ids', !!dcId && !!cableId, `dc=${dcId} cable=${cableId}`);
if (dcId) await page.evaluate((id) => window.__godsEyeView.dataManager.setEnabled(id, true), dcId);
if (cableId) await page.evaluate((id) => window.__godsEyeView.dataManager.setEnabled(id, true), cableId);
await new Promise((r) => setTimeout(r, 800));
const afterEnableHtml = await page.evaluate(() =>
(window.__godsEyeView.viewer.creditDisplay._staticCredits || []).map((c) => c.html),
);
check(
'datacenter credit (OSM/ODbL) still present after enable',
afterEnableHtml.some((h) => h.includes('OpenStreetMap contributors')),
'',
);
check(
'cable credit (TeleGeography) still present after enable',
afterEnableHtml.some((h) => h.includes('TeleGeography')),
'',
);
// helper: is #cesium-credits visible (line rendered, not display:none)?
const creditVisibility = async () =>
page.evaluate(() => {
const el = document.getElementById('cesium-credits');
if (!el) return { present: false };
const cs = getComputedStyle(el);
const rect = el.getBoundingClientRect();
return {
present: true,
display: cs.display,
visibility: cs.visibility,
opacity: cs.opacity,
width: rect.width,
height: rect.height,
text: (el.textContent || '').trim().slice(0, 120),
hasLogo: !!el.querySelector('.cesium-credit-logoContainer, img'),
};
});
// ── (iii) clean-view: credit line STILL visible ───────────────────
console.log('\nH10 — clean-view keeps the credit line visible');
await page.evaluate(() => document.body.classList.add('ui-clean-view'));
await new Promise((r) => setTimeout(r, 400));
const cleanVis = await creditVisibility();
await page.screenshot({ path: resolve(SHOT_DIR, 'clean-view.png') });
check(
'clean-view: #cesium-credits not display:none',
cleanVis.present && cleanVis.display !== 'none' && cleanVis.visibility !== 'hidden',
`display=${cleanVis.display} visibility=${cleanVis.visibility} w=${Math.round(cleanVis.width)} h=${Math.round(cleanVis.height)}`,
);
check(
'clean-view: "Data attribution" link to per-layer popover still present',
!!cleanVis.text && /Data attribution/i.test(cleanVis.text),
`credit text = "${cleanVis.text}"`,
);
await page.evaluate(() => document.body.classList.remove('ui-clean-view'));
// ── (iv) recording-mode: credit line STILL visible ────────────────
console.log('\nH10 — recording-mode keeps the credit line visible');
await page.evaluate(() => document.body.classList.add('recording-mode'));
await new Promise((r) => setTimeout(r, 400));
const recVis = await creditVisibility();
await page.screenshot({ path: resolve(SHOT_DIR, 'recording-mode.png') });
check(
'recording-mode: #cesium-credits not display:none',
recVis.present && recVis.display !== 'none' && recVis.visibility !== 'hidden',
`display=${recVis.display} visibility=${recVis.visibility} w=${Math.round(recVis.width)} h=${Math.round(recVis.height)}`,
);
await page.evaluate(() => document.body.classList.remove('recording-mode'));
// baseline (normal) screenshot for comparison
await new Promise((r) => setTimeout(r, 300));
await page.screenshot({ path: resolve(SHOT_DIR, 'normal.png') });
check(
'no console errors during QA',
consoleErrors.length === 0,
consoleErrors.slice(0, 3).join(' | ') || 'clean',
);
check(
'no HTTP 5xx responses during QA',
failedResponses.length === 0,
failedResponses.slice(0, 3).join(' | ') || 'clean',
);
await browser.close();
console.log('\n────────────────────────────────────────────────────────────');
console.log(` RESULT: ${passed} passed, ${failed} failed`);
console.log(` Screenshots: ${SHOT_DIR}`);
console.log('────────────────────────────────────────────────────────────\n');
process.exit(failed === 0 ? 0 : 1);
}
main().catch((e) => {
console.error('qa-attribution-b12 crashed:', e);
process.exit(1);
});
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env node
/**
* qa-cables-overlay — submarine-cable label path measurement + contract gate.
*
* Two jobs:
* 1. MEASURE (both builds, `--legacy` on the pre-migration build): with the
* cables layer ON at a fixed mid-Atlantic camera, capture
* a. per-frame `scene.render` cost over a ~10 s driven-orbit window
* (mean / p50 / p95 / max — relative numbers, SwiftShader surface),
* b. toggle-ON → first-labels-visible latency,
* c. entity / native-label / host-entry counts,
* d. parked-idle postRender fires over 5 s (render-governor honesty).
* 2. GATE (unified build only): assert the migration contract —
* zero native `LabelGraphics` on cable reference entities, host entries
* present for the source, idle stays near-zero, and a clean OFF→ON cycle
* (no orphan host entries after disable).
*
* Usage: node scripts/qa-cables-overlay.mjs [--url http://localhost:4214] [--legacy]
* Requires a running dev server. Headless; rAF throttling disabled so the
* frame clock is honest (hidden-pane gotcha).
*/
import puppeteer from 'puppeteer';
const argv = process.argv;
const url = argv.includes('--url') ? argv[argv.indexOf('--url') + 1] : 'http://localhost:4214';
const legacy = argv.includes('--legacy');
// --control: never enable the layer; measure the empty-scene orbit cost so the
// cables layer's share of frame time is attributable.
const control = argv.includes('--control');
const LAYER_ID = 'telegeography-submarine-cables';
const results = [];
function check(name, pass, detail) {
results.push({ name, pass });
const tag = pass ? 'PASS' : 'FAIL';
console.log(` [${tag}] ${name}${detail !== undefined ? ` — ${JSON.stringify(detail)}` : ''}`);
}
function report(name, detail) {
console.log(` [MEAS] ${name} — ${JSON.stringify(detail)}`);
}
const browser = await puppeteer.launch({
headless: 'new',
protocolTimeout: 300_000,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--window-size=1440,900',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-background-timer-throttling',
],
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 860 });
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 90_000 });
await new Promise((r) => setTimeout(r, 12_000)); // boot flyTo + deferred init
// Park mid-Atlantic (many cables + both coasts' landings in range) and
// disable every layer so the cables layer is measured in isolation.
await page.evaluate(async () => {
const gev = window.__godsEyeView;
const v = gev.viewer;
v.camera.cancelFlight();
const ell = v.scene.globe.ellipsoid;
v.camera.setView({
destination: ell.cartographicToCartesian({
longitude: -40 * Math.PI / 180, latitude: 35 * Math.PI / 180, height: 4_500_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
for (const [id, entry] of gev.dataManager.layers) {
if (entry.enabled) { try { await gev.dataManager.setEnabled(id, false, { origin: 'user' }); } catch { /* measured via counts */ } }
}
});
await new Promise((r) => setTimeout(r, 6_000)); // tiles + fades settle
// ── b. toggle-ON → labels visible ─────────────────────────────────────
if (!control) {
const labelLatency = await page.evaluate(async (layerId, isLegacy) => {
const gev = window.__godsEyeView;
const t0 = performance.now();
await gev.dataManager.setEnabled(layerId, true, { origin: 'user' });
const deadline = t0 + 60_000;
const labelsUp = () => {
if (isLegacy) {
const stats = gev.dataManager.layers.get(layerId)?.module?.getStats?.() || {};
return (stats.referenceLabelCount || 0) > 0;
}
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
return (diag.paintedBySource?.[layerId] || 0) > 0;
};
while (performance.now() < deadline) {
if (labelsUp()) return { ms: Math.round(performance.now() - t0), timedOut: false };
gev.viewer.scene.requestRender?.();
await new Promise((r) => setTimeout(r, 100));
}
return { ms: 60_000, timedOut: true };
}, LAYER_ID, legacy);
report('toggle-ON -> labels visible (ms)', labelLatency);
check('labels became visible after enable', labelLatency.timedOut === false, labelLatency);
await new Promise((r) => setTimeout(r, 3_000)); // let load/labels settle
}
// ── c. entity / label / host counts ───────────────────────────────────
const counts = await page.evaluate((layerId) => {
const gev = window.__godsEyeView;
const v = gev.viewer;
const out = {
dataSources: {}, nativeLabels: 0, dynamicPositionEntities: 0,
referenceEntities: 0, hostEntries: 0, hostPainted: 0,
};
for (let i = 0; i < v.dataSources.length; i++) {
const ds = v.dataSources.get(i);
if (!/TeleGeography/.test(ds.name || '')) continue;
const list = ds.entities.values;
out.dataSources[ds.name] = list.length;
for (const e of list) {
if (e.label !== undefined) out.nativeLabels++;
if (e.position && e.position.isConstant === false) out.dynamicPositionEntities++;
if (e.polyline?.positions && e.polyline.positions.isConstant === false) {
out.dynamicPositionEntities++;
}
}
if (/References/.test(ds.name)) out.referenceEntities = list.length;
}
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
out.hostEntries = diag.entriesBySource?.[layerId] || 0;
out.hostPainted = diag.paintedBySource?.[layerId] || 0;
const stats = gev.dataManager.layers.get(layerId)?.module?.getStats?.() || {};
out.referenceLabelCount = stats.referenceLabelCount ?? null;
return out;
}, LAYER_ID);
report(control ? 'counts (control, cables OFF)' : 'counts with cables ON', counts);
// ── a. per-frame scene.render cost over a ~10 s driven orbit ─────────
const frameCost = await page.evaluate(() => new Promise((resolve) => {
const v = window.__godsEyeView.viewer;
const scene = v.scene;
const durations = [];
const originalRender = scene.render;
scene.render = function patchedRender(...args) {
const started = performance.now();
const result = originalRender.apply(this, args);
durations.push(performance.now() - started);
return result;
};
const t0 = performance.now();
const tick = () => {
// Slow orbit: every frame is a camera change, so Cesium renders every
// rAF — the honest "user is interacting" cost of the layer.
v.camera.rotateRight(0.0004);
if (performance.now() - t0 < 10_000) requestAnimationFrame(tick);
else {
scene.render = originalRender;
durations.sort((a, b) => a - b);
const n = durations.length;
const sum = durations.reduce((s, d) => s + d, 0);
resolve({
frames: n,
meanMs: +(sum / Math.max(1, n)).toFixed(2),
p50Ms: +(durations[Math.floor(n * 0.5)] || 0).toFixed(2),
p95Ms: +(durations[Math.floor(n * 0.95)] || 0).toFixed(2),
maxMs: +(durations[n - 1] || 0).toFixed(2),
effectiveFps: +(n / 10).toFixed(1),
});
}
};
requestAnimationFrame(tick);
}));
report(control
? 'scene.render cost, control (cables OFF), 10s orbit'
: 'scene.render cost, cables ON, 10s orbit', frameCost);
// ── d. parked idle honesty (labels must not force continuous render) ──
await new Promise((r) => setTimeout(r, 4_000)); // orbit stop + fades settle
const idle = await page.evaluate(() => new Promise((resolve) => {
const scene = window.__godsEyeView.viewer.scene;
let renders = 0;
const remove = scene.postRender.addEventListener(() => { renders += 1; });
setTimeout(() => { remove(); resolve({ renders }); }, 5_000);
}));
report(control
? 'parked idle, control (postRender fires / 5s)'
: 'parked idle with cables ON (postRender fires / 5s)', idle);
if (!legacy && !control) {
// ── unified-build contract gates ────────────────────────────────────
check('zero native LabelGraphics on cable entities', counts.nativeLabels === 0, counts.nativeLabels);
check('zero per-frame CallbackProperty cable entities', counts.dynamicPositionEntities === 0, counts.dynamicPositionEntities);
check('host has cable entries', counts.hostEntries > 0, counts.hostEntries);
check('host painted cable labels', counts.hostPainted > 0, counts.hostPainted);
// The cables source never republishes an unchanged cohort (unit pin), and
// since the 2026-08-18 host fix the occluder observer is scoped (body
// childList filtered to inventory chrome; attribute observation
// element-only) and the right-rail allocator writes-if-changed — so a
// parked camera measures 0 postRender fires / 5 s with cables ON, equal
// to the empty-scene control (pre-fix: ~56-61). The headroom below covers
// an occasional genuine chrome transition (loading-chip flip + its 100 ms
// occluder-refresh echo) landing inside the window; ticking churn (~10)
// or the old observer leak (~56+) must fail.
check('parked idle stays near zero (≤6 / 5s)', idle.renders <= 6, idle);
// OFF must clear the host source (no orphan labels), ON must restore.
const cycle = await page.evaluate(async (layerId) => {
const gev = window.__godsEyeView;
await gev.dataManager.setEnabled(layerId, false, { origin: 'user' });
gev.viewer.scene.requestRender?.();
await new Promise((r) => setTimeout(r, 1_200));
const offDiag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
const offEntries = offDiag.entriesBySource?.[layerId] || 0;
const offPainted = offDiag.paintedBySource?.[layerId] || 0;
await gev.dataManager.setEnabled(layerId, true, { origin: 'user' });
const t0 = performance.now();
let onPainted = 0;
while (performance.now() - t0 < 20_000) {
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
onPainted = diag.paintedBySource?.[layerId] || 0;
if (onPainted > 0) break;
gev.viewer.scene.requestRender?.();
await new Promise((r) => setTimeout(r, 100));
}
return { offEntries, offPainted, onPainted };
}, LAYER_ID);
check('disable clears host entries (no orphans)', cycle.offEntries === 0 && cycle.offPainted === 0, cycle);
check('re-enable restores painted labels', cycle.onPainted > 0, cycle);
}
} finally {
await browser.close();
}
const passed = results.filter((r) => r.pass).length;
console.log(`\nqa-cables-overlay: ${passed}/${results.length} passed${legacy ? ' (legacy measurement mode)' : ''}${control ? ' (control mode)' : ''}`);
console.log(`RESULT: ${passed} passed, ${results.length - passed} failed, 0 skipped`);
process.exit(passed === results.length ? 0 : 1);
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env node
/**
* qa-cables-render-probe — replicates the owner-side perf probe methodology
* for the cables layer (relative numbers; SwiftShader when run headless):
*
* 1. PARKED: timed full `viewer.render()` loop (40 calls) with cables ON,
* then OFF, at the fixed mid-Atlantic camera. Reports mean/max.
* 2. PAN-STOP: 3 cycles of { setView to an offset, raise camera.moveEnd,
* wait 650 ms, 20 timed `viewer.render()` calls }. Reports per-cycle
* mean/max and the worst frame across cycles, cables ON vs OFF.
*
* Usage: node scripts/qa-cables-render-probe.mjs [--url http://localhost:4216]
*/
import puppeteer from 'puppeteer';
const argv = process.argv;
const url = argv.includes('--url') ? argv[argv.indexOf('--url') + 1] : 'http://localhost:4216';
const LAYER_ID = 'telegeography-submarine-cables';
const browser = await puppeteer.launch({
headless: 'new',
protocolTimeout: 300_000,
args: [
'--no-sandbox', '--disable-setuid-sandbox', '--window-size=1440,900',
'--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding',
'--disable-background-timer-throttling',
],
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 860 });
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 90_000 });
await new Promise((r) => setTimeout(r, 12_000));
await page.evaluate(async () => {
const gev = window.__godsEyeView;
gev.viewer.camera.cancelFlight();
const ell = gev.viewer.scene.globe.ellipsoid;
gev.viewer.camera.setView({
destination: ell.cartographicToCartesian({
longitude: -40 * Math.PI / 180, latitude: 35 * Math.PI / 180, height: 4_500_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
for (const [id, entry] of gev.dataManager.layers) {
if (entry.enabled) { try { await gev.dataManager.setEnabled(id, false, { origin: 'user' }); } catch { /* probe */ } }
}
});
await new Promise((r) => setTimeout(r, 5_000));
const stats = (values) => ({
mean: +(values.reduce((s, v) => s + v, 0) / Math.max(1, values.length)).toFixed(2),
max: +Math.max(...values).toFixed(2),
});
const measure = async (label, enabled) => {
await page.evaluate(async (id, on) => {
const gev = window.__godsEyeView;
await gev.dataManager.setEnabled(id, on, { origin: 'user' });
}, LAYER_ID, enabled);
await new Promise((r) => setTimeout(r, enabled ? 6_000 : 3_000)); // load/teardown + settle
// 1. PARKED timed renders.
const parked = await page.evaluate(() => {
const v = window.__godsEyeView.viewer;
const durations = [];
for (let i = 0; i < 40; i++) {
const t0 = performance.now();
v.render();
durations.push(performance.now() - t0);
}
return durations;
});
// 2. PAN-STOP cycles: move, raise moveEnd, wait 650 ms, 20 timed renders.
const cycles = [];
for (let cycle = 0; cycle < 3; cycle++) {
const frames = await page.evaluate(async (step) => {
const v = window.__godsEyeView.viewer;
const ell = v.scene.globe.ellipsoid;
v.camera.setView({
destination: ell.cartographicToCartesian({
longitude: (-40 + step * 3) * Math.PI / 180,
latitude: 35 * Math.PI / 180,
height: 4_500_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
v.camera.moveEnd.raiseEvent();
await new Promise((resolve) => setTimeout(resolve, 650));
const durations = [];
for (let i = 0; i < 20; i++) {
const t0 = performance.now();
v.render();
durations.push(performance.now() - t0);
}
return durations;
}, cycle + 1);
cycles.push(stats(frames));
}
const worst = Math.max(...cycles.map((c) => c.max));
console.log(` [${label}] parked render: ${JSON.stringify(stats(parked))}`);
console.log(` [${label}] pan-stop cycles: ${JSON.stringify(cycles)} worstFrame=${worst}`);
return { parked: stats(parked), cycles, worst };
};
const on = await measure('cables ON ', true);
const off = await measure('cables OFF', false);
console.log(`\nparked delta (ON-OFF mean): ${(on.parked.mean - off.parked.mean).toFixed(2)}ms`);
console.log(`pan-stop worst: ON ${on.worst}ms vs OFF ${off.worst}ms`);
} finally {
await browser.close();
}
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env node
/**
* qa-cables-shot — capture the cables layer at two fixed cameras for
* before/after visual-identity comparison. Writes to gitignored qa-shots/.
* Usage: node scripts/qa-cables-shot.mjs [--url http://localhost:4214] [--tag before]
*/
import puppeteer from 'puppeteer';
import { mkdirSync } from 'node:fs';
const argv = process.argv;
const url = argv.includes('--url') ? argv[argv.indexOf('--url') + 1] : 'http://localhost:4214';
const tag = argv.includes('--tag') ? argv[argv.indexOf('--tag') + 1] : 'shot';
const LAYER_ID = 'telegeography-submarine-cables';
mkdirSync(new URL('../qa-shots', import.meta.url), { recursive: true });
const browser = await puppeteer.launch({
headless: 'new',
protocolTimeout: 300_000,
args: [
'--no-sandbox', '--disable-setuid-sandbox', '--window-size=1440,900',
'--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding',
'--disable-background-timer-throttling',
],
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 860 });
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 90_000 });
await new Promise((r) => setTimeout(r, 12_000));
await page.evaluate(async (layerId) => {
const gev = window.__godsEyeView;
gev.viewer.camera.cancelFlight();
for (const [id, entry] of gev.dataManager.layers) {
if (entry.enabled && id !== layerId) {
try { await gev.dataManager.setEnabled(id, false, { origin: 'user' }); } catch { /* shot only */ }
}
}
await gev.dataManager.setEnabled(layerId, true, { origin: 'user' });
}, LAYER_ID);
const views = [
{ name: 'atlantic', lon: -40, lat: 35, height: 4_500_000 },
{ name: 'ny-coast', lon: -73.5, lat: 40.4, height: 500_000 },
];
for (const view of views) {
await page.evaluate((v) => {
const viewer = window.__godsEyeView.viewer;
const ell = viewer.scene.globe.ellipsoid;
viewer.camera.setView({
destination: ell.cartographicToCartesian({
longitude: v.lon * Math.PI / 180, latitude: v.lat * Math.PI / 180, height: v.height,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
}, view);
// Let the sweep + labels settle with frames flowing.
await page.evaluate(() => new Promise((resolve) => {
const v = window.__godsEyeView.viewer;
let ticks = 0;
const tick = () => {
v.scene.requestRender?.();
if (++ticks < 240) requestAnimationFrame(tick); else resolve();
};
requestAnimationFrame(tick);
}));
await new Promise((r) => setTimeout(r, 1_000));
const path = new URL(`../qa-shots/cables-${tag}-${view.name}.png`, import.meta.url).pathname;
await page.screenshot({ path });
console.log(`saved ${path}`);
}
} finally {
await browser.close();
}
File diff suppressed because it is too large. Load diff
+555
View File
@@ -0,0 +1,555 @@
#!/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);
});
File diff suppressed because it is too large. Load diff
+582
View File
@@ -0,0 +1,582 @@
#!/usr/bin/env node
/**
* qa-enrich-ambient.mjs — headless proof for AMBIENT fleet type enrichment
* (fleet-enrichment widening of skylight Task 14).
*
* Field data (2026-07-02): OpenSky's live category field is 0/"no info" for
* ~94% of planes, so ambient classification defaulted nearly the whole fleet
* to the airliner silhouette. This harness reproduces exactly that case —
* 12 synthetic planes, ALL category 0 — and proves that the per-poll
* on-screen enrichment sweep (`_sweepAmbientEnrichment` in flights.js) gives
* them real types AMBIENTLY (nothing is ever tracked, 3D stays off), while
* honoring the politeness bounds.
*
* Fetch shim (installed before app code, same pattern as qa-sprites-b5.mjs):
* - /api/opensky → 12 straight-flying planes, category 0
* - /api/adsbdb/type/:hex → varied REAL type codes (C172, B744, DH8D, H60,
* F16, GLID, B77W, A320, B738, PC12, R44) + one
* found:false miss; responses are HELD until the
* harness releases them (deterministic baseline),
* then served with 400 ms latency; every request
* start/inflight count is logged for the bounds
* assertions.
*
* Machine assertions:
* E1 ingest : all 12 synthetic planes in the flights layer
* E2 baseline : BEFORE any enrichment answer, all 12 billboards carry the
* default airliner glyph at scale 1 (the real-world problem)
* E3 requests : the sweep requested all 12 hexes, each exactly once
* E4 diversity : ≥5 distinct glyph data-URIs displayed ambiently after
* enrichment (expected: 8)
* E5 glyphs : every billboard's image === aircraftIcon(classify(typeCode))
* E6 scales : every billboard's scale === CLASS_SCALE_2D[klass]
* E7 bounds : ≤4 concurrent requests, ≥185 ms between dispatches (≤5/s
* drip), total ≤300 (budget ceiling)
* E8 next poll : another poll neither reverts glyphs nor re-requests hexes
* E10 exhaust : the ROLLING ambient budget exhausts — new on-screen planes
* stop being requested once the bucket is empty (shrunk via
* the __GEV_ENRICH_AMBIENT_QA seam; a real 5-min window can't
* be waited out headlessly)
* E11 resume : after a refill window passes, ambient enrichment RESUMES
* for the still-unrequested planes (the 2026-07-03 field bug:
* the old one-shot session cap never refilled, so hours-long
* sessions showed airliner monoculture in every new region)
* E12 clean : no console errors
*
* Visual output (qa-shots/enrich-ambient/, untracked): before/after top-down
* spreads of the row.
*
* Run: node scripts/qa-enrich-ambient.mjs --url http://localhost:4304
* Exits non-zero if any assertion fails. Never commits anything.
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { classifyAircraft, CLASS_SCALE_2D } from '../src/data/aircraftClass.js';
import { aircraftIcon } from '../src/data/aircraftIcons.js';
// ---------------------------------------------------------------------------
// Args (same shape as qa-sprites-b5.mjs)
// ---------------------------------------------------------------------------
const argv = process.argv.slice(2);
const getFlag = (name) => argv.includes(name);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const APP_URL = getOpt('--url', 'http://localhost:4173');
const HEADFUL = getFlag('--headful');
const SHOT_DIR = path.resolve('qa-shots/enrich-ambient');
// Client-side bounds under test (flights.js constants):
const MAX_INFLIGHT = 4; // ENRICH_MAX_INFLIGHT
const DISPATCH_GAP_MS = 200; // ENRICH_DISPATCH_GAP_MS (≤5 req/s)
const GAP_TOLERANCE_MS = 15; // clock-quantization allowance on the gap check
const SESSION_CAP = 300; // ENRICH_AMBIENT_BUDGET_CEIL (rolling-bucket ceiling)
// Rolling-budget QA seam (window.__GEV_ENRICH_AMBIENT_QA in flights.js):
// shrunk so the exhaust→refill cycle is observable headlessly. windowMs starts
// effectively-infinite (1 h) so E10 exhausts deterministically (no refill can
// land mid-phase); E11 then shortens it in-page so refill windows have
// "passed" and the bucket refills (clamped at ceil). ceil=20 leaves 8 tokens
// after the 12 baseline planes — E10's batch exhausts those 8 and stalls.
const BUDGET_QA = { ceil: 20, refillTokens: 6, windowMs: 3600000 };
const RESUME_WINDOW_MS = 1000; // E11 swaps windowMs to this to unlock refills
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try { if (fs.existsSync(candidate)) return candidate; } catch { /* skip */ }
}
return null;
}
const results = [];
function record(name, ok, detail) {
results.push({ name, ok, detail });
const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
// ---------------------------------------------------------------------------
// Synthetic fleet: one east-west row near Austin, ALL category 0 ("no info" —
// the 94% real-world case). adsbdb type answers cover 8 distinct classes;
// aa000c answers found:false (negative-cache case → stays airliner).
// ---------------------------------------------------------------------------
const ROW_LAT = 30.30;
const ROW_LON0 = -97.82;
const ROW_STEP_DEG = 0.012; // ~1.15 km — 12 planes span ~12.7 km, fits a 20 km-up frame
/** hex → adsbdb icao_type (null = adsbdb miss / negative cache). */
const TYPES = {
aa0001: 'C172', // light
aa0002: 'B744', // quadjet
aa0003: 'DH8D', // turboprop
aa0004: 'H60', // helicopter
aa0005: 'F16', // fastjet
aa0006: 'GLID', // glider
aa0007: 'B77W', // widebody
aa0008: 'A320', // airliner
aa0009: 'B738', // airliner (known code outside the special sets)
aa000a: 'PC12', // turboprop
aa000b: 'R44', // helicopter
aa000c: null, // adsbdb 404 → found:false → stays airliner, never re-asked
};
const SPEC = {
timeOffsetSec: 0,
responseDelayMs: 400, // adsbdb latency so concurrency is actually observable
budgetQa: BUDGET_QA,
types: TYPES,
planes: Object.keys(TYPES).map((hex, i) => ({
icao: hex,
callsign: `AMB${String(i + 1).padStart(3, '0')}`,
lon0: ROW_LON0 + i * ROW_STEP_DEG,
lat0: ROW_LAT,
courseDeg: 0,
speedMps: 70,
altM: 2400 + i * 40,
})),
};
// Node-side expectations (same classifier inputs the layer sees).
const expected = new Map(Object.entries(TYPES).map(([hex, tc]) => [
hex, classifyAircraft(tc ? { typeCode: tc } : { category: 0 }),
]));
const AIRLINER_ICON = aircraftIcon(classifyAircraft({ category: 0 })); // pre-enrichment default
// ---------------------------------------------------------------------------
async function main() {
console.log(`\nAmbient fleet type-enrichment QA (fleet-enrichment widening)`);
console.log(` App URL : ${APP_URL}\n`);
console.log(` Expected classes: ${[...expected.entries()].map(([k, v]) => `${k}:${v}`).join(' ')}\n`);
try {
const res = await fetch(APP_URL, { method: 'GET' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
console.error(`\x1b[31mDev server not reachable at ${APP_URL} (${e.message}).\x1b[0m`);
process.exit(2);
}
fs.mkdirSync(SHOT_DIR, { recursive: true });
const chromeExecutable = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
'--use-angle=swiftshader',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1280,800',
],
});
const consoleErrors = [];
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
page.on('console', (msg) => {
if (msg.type() === 'error') {
const text = msg.text();
if (!/Failed to load resource.*404/i.test(text)) consoleErrors.push(text);
}
});
page.on('pageerror', (err) => consoleErrors.push(`pageerror: ${err.message}`));
// ---- Fetch shim: synthetic fleet + instrumented, holdable adsbdb -------
await page.evaluateOnNewDocument((spec) => {
window.__ENR = spec;
window.__ENR.epochMs = Date.now();
// Rolling-budget QA seam — must exist BEFORE flights.js initializes so
// the shrunk knobs are read from the very first sweep (see BUDGET_QA).
window.__GEV_ENRICH_AMBIENT_QA = { ...spec.budgetQa };
// Request log: starts (hex + performance.now), live/max concurrency,
// hold gate (responses park until the harness releases them so the
// pre-enrichment baseline is deterministic).
window.__ENRICH_LOG = { starts: [], inflight: 0, maxInflight: 0, held: true, holds: [] };
window.__ENRICH_RELEASE = () => {
window.__ENRICH_LOG.held = false;
window.__ENRICH_LOG.holds.splice(0).forEach((fn) => fn());
};
// Straight line from (lon0, lat0) along courseDeg at speedMps.
window.__ENR.stateAt = (p, tSec) => {
const cRad = (p.courseDeg * Math.PI) / 180;
const dist = p.speedMps * tSec;
const east = dist * Math.sin(cRad);
const north = dist * Math.cos(cRad);
const lat = p.lat0 + north / 111320;
const lon = p.lon0 + east / (111320 * Math.cos((p.lat0 * Math.PI) / 180));
return { lon, lat, course: p.courseDeg, speedMps: p.speedMps };
};
const realFetch = window.fetch.bind(window);
const jsonResponse = (obj) => new Response(JSON.stringify(obj), {
status: 200, headers: { 'Content-Type': 'application/json' },
});
window.fetch = (input, init) => {
const url = typeof input === 'string' ? input : (input && input.url) || '';
const S = window.__ENR;
const nowSec = Date.now() / 1000 + (S.timeOffsetSec || 0);
const tRel = nowSec - S.epochMs / 1000;
if (url.includes('/api/adsbdb/type/')) {
const hex = url.split('/').pop().split('?')[0].toLowerCase();
const L = window.__ENRICH_LOG;
L.starts.push({ hex, t: performance.now() });
L.inflight += 1;
L.maxInflight = Math.max(L.maxInflight, L.inflight);
const tc = S.types[hex];
const body = tc
? { found: true, typeCode: tc, typeName: `Synthetic ${tc}`, registration: `N${hex.slice(-3).toUpperCase()}` }
: { found: false };
return new Promise((resolve) => {
const finish = () => setTimeout(() => { L.inflight -= 1; resolve(jsonResponse(body)); }, S.responseDelayMs);
if (L.held) L.holds.push(finish); else finish();
});
}
if (url.includes('/api/adsbdb/')) return Promise.resolve(jsonResponse({ found: false }));
if (url.includes('/api/opensky-track')) return Promise.resolve(jsonResponse({ path: [] }));
if (url.includes('/api/adsblol/trace')) {
return Promise.resolve(jsonResponse({ timestamp: Math.floor(nowSec), trace: [] }));
}
if (url.includes('/api/adsblol/mil')) {
return Promise.resolve(jsonResponse({ msg: 'No error', now: Date.now(), ac: [] }));
}
if (url.includes('/api/opensky')) {
const states = S.planes.map((f) => {
const s = S.stateAt(f, tRel);
return [
f.icao, f.callsign, 'Synthetica',
Math.floor(nowSec), Math.floor(nowSec),
s.lon, s.lat, f.altM,
false, // on_ground
s.speedMps, s.course,
0, null, null, null, false, 0,
0, // state[17] — category 0 = "no info" (the 94% case)
];
});
return Promise.resolve(jsonResponse({ time: Math.floor(nowSec), states }));
}
return realFetch(input, init);
};
}, SPEC);
console.log('Loading app...');
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() => window.__godsEyeView && window.__godsEyeView.viewer && window.__godsEyeView.dataManager,
{ timeout: 60000, polling: 200 }
);
console.log(' App globals ready.');
// ---- In-page billboard probe (same walk as qa-sprites-b5.mjs) ----------
await page.evaluate(() => {
window.__collectBillboards = function () {
const v = window.__godsEyeView.viewer;
const out = [];
const walk = (coll) => {
const n = coll.length;
for (let i = 0; i < n; i++) {
let p;
try { p = coll.get(i); } catch { continue; }
if (!p) continue;
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
if (p.image !== undefined && p.alignedAxis !== undefined) {
out.push({ id: p.id, image: p.image, scale: p.scale, show: p.show });
}
}
};
walk(v.scene.primitives);
return out;
};
});
// ---- Frame the row FIRST (top-down, all 12 on-screen), then prime ------
// The ambient sweep runs inside update(), so the camera must already be
// framing the fleet when the layer polls — otherwise the frustum test
// (correctly) skips off-screen planes.
const centerLon = ROW_LON0 + ((SPEC.planes.length - 1) / 2) * ROW_STEP_DEG;
await page.evaluate(({ lat, lon, height }) => {
const v = window.__godsEyeView.viewer;
v.camera.cancelFlight(); // the boot fly-to-Austin otherwise stomps setView
const C3 = v.camera.position.constructor;
v.camera.setView({
destination: C3.fromDegrees(lon, lat, height),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
}, { lat: ROW_LAT, lon: centerLon, height: 20000 });
console.log('Priming straight-flight history through the render delay (30 s)...');
const primed = await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
const v = window.__godsEyeView.viewer;
window.__ENR.timeOffsetSec = -32;
await dm.setEnabled('flights', true);
const fl = dm.layers.get('flights').module;
for (const off of [-24, -16, -8, 0]) {
window.__ENR.timeOffsetSec = off;
await fl.update(v);
}
window.__ENR.timeOffsetSec = 0;
window.__ENR_DRIVER = setInterval(() => { fl.update(v); }, 15000);
return { count: fl.getStats().count, starts: window.__ENRICH_LOG.starts.length };
});
console.log(` flights count=${primed.count} | adsbdb requests started (held): ${primed.starts}`);
record('E1 ingest: all synthetic category-0 planes in the flights layer',
primed.count === SPEC.planes.length, `count=${primed.count}/${SPEC.planes.length}`);
if (primed.count === 0) { finish(); return; }
// ========================================================================
// E2 — deterministic pre-enrichment baseline (responses still held)
// ========================================================================
console.log('\nE2 — pre-enrichment baseline (adsbdb responses held)');
const before = await page.evaluate(() => window.__collectBillboards());
const beforeById = new Map(before.map((b) => [b.id, b]));
const baselineBad = [];
for (const hex of Object.keys(TYPES)) {
const bb = beforeById.get(hex);
if (!bb) baselineBad.push(`${hex}:missing`);
else if (bb.image !== AIRLINER_ICON) baselineBad.push(`${hex}:not-default-glyph`);
else if (Math.abs(bb.scale - (CLASS_SCALE_2D.airliner || 1)) > 1e-9) baselineBad.push(`${hex}:scale=${bb.scale}`);
}
record('E2 baseline: all 12 category-0 planes default to the airliner glyph',
baselineBad.length === 0,
baselineBad.length ? baselineBad.join(' ') : 'all 12 identical airliner silhouettes (the field-data problem)');
await sleep(1200); // let a fleet tick land rotations before the "before" shot
await page.screenshot({ path: path.join(SHOT_DIR, 'enrich-before-monoculture.png') });
console.log(' saved enrich-before-monoculture.png (12 identical airliner glyphs)');
// ========================================================================
// Release adsbdb + wait for the drip to finish, then for glyph swaps
// ========================================================================
console.log('\nReleasing adsbdb responses; waiting for the bounded drip to drain...');
await page.evaluate(() => window.__ENRICH_RELEASE());
const drained = await page.waitForFunction(
(want) => window.__ENRICH_LOG.starts.length >= want && window.__ENRICH_LOG.inflight === 0,
{ timeout: 30000, polling: 100 }, SPEC.planes.length
).then(() => true).catch(() => false);
if (!drained) console.log(' \x1b[33mdrip did not fully drain within 30 s\x1b[0m');
// Give the enrichment callbacks a beat to apply billboard swaps.
await page.waitForFunction(() => {
const images = new Set(window.__collectBillboards().map((b) => b.image));
return images.size >= 5;
}, { timeout: 10000, polling: 200 }).catch(() => { /* asserted below */ });
// ========================================================================
// E3–E6 — requests + ambient glyph/scale swaps (nothing tracked, 3D off)
// ========================================================================
console.log('\nE3–E6 — ambient enrichment results');
const log1 = await page.evaluate(() => ({
starts: window.__ENRICH_LOG.starts.map((s) => ({ hex: s.hex, t: s.t })),
maxInflight: window.__ENRICH_LOG.maxInflight,
}));
const startHexes = log1.starts.map((s) => s.hex);
const uniqueHexes = new Set(startHexes);
const allRequested = Object.keys(TYPES).every((h) => uniqueHexes.has(h));
record('E3 requests: sweep requested all 12 hexes, each exactly once',
allRequested && startHexes.length === Object.keys(TYPES).length,
`${startHexes.length} requests, ${uniqueHexes.size} unique`);
const after = await page.evaluate(() => window.__collectBillboards());
const afterById = new Map(after.map((b) => [b.id, b]));
const syntheticImages = new Set(
Object.keys(TYPES).map((hex) => afterById.get(hex)?.image).filter(Boolean)
);
record('E4 diversity: ≥5 distinct glyph data-URIs displayed ambiently',
syntheticImages.size >= 5, `${syntheticImages.size} distinct glyphs (expected 8)`);
const glyphBad = [];
const scaleBad = [];
for (const [hex, klass] of expected) {
const bb = afterById.get(hex);
const wantImg = aircraftIcon(klass);
const wantScale = CLASS_SCALE_2D[klass] || 1;
if (!bb || bb.image !== wantImg) glyphBad.push(`${hex}(${klass})${bb ? ':wrong-image' : ':missing'}`);
if (bb && Math.abs(bb.scale - wantScale) > 1e-9) scaleBad.push(`${hex}: ${bb.scale} != ${wantScale}`);
}
record('E5 glyphs: every billboard matches aircraftIcon(classify(typeCode))',
glyphBad.length === 0, glyphBad.length ? glyphBad.join(' ') : `${expected.size} matched (incl. the found:false miss staying airliner)`);
record('E6 scales: every billboard has its per-class CLASS_SCALE_2D',
scaleBad.length === 0, scaleBad.length ? scaleBad.join(' ') : 'all scales exact (composes with scaleByDistance)');
await page.screenshot({ path: path.join(SHOT_DIR, 'enrich-after-diverse.png') });
console.log(' saved enrich-after-diverse.png (type-diverse row, still untracked)');
// ========================================================================
// E7 — politeness bounds (concurrency, drip rate, session cap)
// ========================================================================
console.log('\nE7 — client-side bounds');
const gaps = [];
for (let i = 1; i < log1.starts.length; i++) gaps.push(log1.starts[i].t - log1.starts[i - 1].t);
const minGap = gaps.length ? Math.min(...gaps) : Infinity;
// Max dispatches in any sliding 1 s window (reported for context).
let maxPerSec = 0;
for (let i = 0; i < log1.starts.length; i++) {
let n = 0;
for (let j = i; j < log1.starts.length && log1.starts[j].t - log1.starts[i].t < 1000; j++) n++;
maxPerSec = Math.max(maxPerSec, n);
}
const boundsOk = log1.maxInflight <= MAX_INFLIGHT
&& minGap >= (DISPATCH_GAP_MS - GAP_TOLERANCE_MS)
&& log1.starts.length <= SESSION_CAP;
record(`E7 bounds: ≤${MAX_INFLIGHT} concurrent, ≥${DISPATCH_GAP_MS}ms drip gap, ≤${SESSION_CAP}/session`,
boundsOk,
`maxConcurrent=${log1.maxInflight} minGap=${Number.isFinite(minGap) ? minGap.toFixed(1) : 'n/a'}ms `
+ `maxIn1s=${maxPerSec} total=${log1.starts.length}`);
// ========================================================================
// E8 — next poll: klass persists (poll re-derive keeps enriched types) and
// the session dedupe never re-requests an answered/negative hex
// ========================================================================
console.log('\nE8 — next poll after enrichment');
await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
const v = window.__godsEyeView.viewer;
await dm.layers.get('flights').module.update(v);
});
await sleep(500);
const afterPoll = await page.evaluate(() => ({
billboards: window.__collectBillboards(),
starts: window.__ENRICH_LOG.starts.length,
}));
const afterPollById = new Map(afterPoll.billboards.map((b) => [b.id, b]));
const pollBad = [];
for (const [hex, klass] of expected) {
const bb = afterPollById.get(hex);
if (!bb || bb.image !== aircraftIcon(klass)) pollBad.push(`${hex}(${klass})${bb ? ':reverted' : ':missing'}`);
else if (Math.abs(bb.scale - (CLASS_SCALE_2D[klass] || 1)) > 1e-9) pollBad.push(`${hex}:scale-reverted`);
}
const noReRequests = afterPoll.starts === Object.keys(TYPES).length;
record('E8 next poll: enriched glyphs persist and no hex is re-requested',
pollBad.length === 0 && noReRequests,
pollBad.length ? pollBad.join(' ') : `glyphs stable, requests still ${afterPoll.starts}`);
// ========================================================================
// E10 — rolling-budget exhaustion (2026-07-03 field bug, red/green): the
// old ONE-SHOT session cap burned out and never refilled, so every new
// region after the first polls showed airliner monoculture. With the seam
// ceil=20 and 12 tokens spent above, only 8 remain — a batch of 26 fresh
// on-screen planes must stall at exactly 8 new requests, and further polls
// must enqueue NOTHING while the bucket is empty (windowMs is 1 h here, so
// no refill can land mid-phase). Pre-fix code fails this phase: there is
// no rolling bucket to exhaust (the seam is unread and the 300 cap simply
// admits the whole batch).
// ========================================================================
console.log('\nE10 — rolling budget exhausts (batch of 26 new planes, 8 tokens left)');
const BATCH_COUNT = 26;
const batchHexes = Array.from({ length: BATCH_COUNT }, (_, i) => `ab00${(i + 1).toString(16).padStart(2, '0')}`);
const startsBeforeBatch = await page.evaluate(() => window.__ENRICH_LOG.starts.length);
await page.evaluate(async ({ hexes, rowLat, rowLon0, stepDeg }) => {
const dm = window.__godsEyeView.dataManager;
const v = window.__godsEyeView.viewer;
// Two fresh rows just north of the original one — still inside the
// top-down 20 km frame, so the sweep's frustum test keeps them.
// STATIONARY (speed 0): moving planes drift north out of the frustum at
// run-time-dependent rates, which made this phase timing-sensitive.
hexes.forEach((hex, i) => {
window.__ENR.planes.push({
icao: hex,
callsign: `BGT${String(i + 1).padStart(3, '0')}`,
lon0: rowLon0 + (i % 13) * stepDeg,
lat0: rowLat + (i < 13 ? 0.02 : 0.04),
courseDeg: 0,
speedMps: 0,
altM: 2600 + i * 15,
});
});
const fl = dm.layers.get('flights').module;
await fl.update(v); // sweep enqueues the last 8 tokens
await fl.update(v); // bucket empty — must enqueue nothing more
}, { hexes: batchHexes, rowLat: ROW_LAT, rowLon0: ROW_LON0, stepDeg: ROW_STEP_DEG });
// Wait for the drip to drain whatever was enqueued, then settle.
await page.waitForFunction(
(min) => window.__ENRICH_LOG.starts.length >= min && window.__ENRICH_LOG.inflight === 0,
{ timeout: 20000, polling: 100 }, startsBeforeBatch + (BUDGET_QA.ceil - startsBeforeBatch)
).catch(() => { /* asserted below */ });
await sleep(1500); // any illegal post-exhaustion enqueue would dispatch within ~200 ms
// One more poll while exhausted — still nothing new.
await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
await dm.layers.get('flights').module.update(window.__godsEyeView.viewer);
});
await sleep(1000);
const exhausted = await page.evaluate(() => ({
starts: window.__ENRICH_LOG.starts.length,
inflight: window.__ENRICH_LOG.inflight,
}));
record(`E10 exhaust: requests stall at the budget ceiling (${BUDGET_QA.ceil}) with unrequested planes on-screen`,
exhausted.starts === BUDGET_QA.ceil && exhausted.inflight === 0,
`starts=${exhausted.starts} (want ${BUDGET_QA.ceil}: ${startsBeforeBatch} baseline + ${BUDGET_QA.ceil - startsBeforeBatch} tokens; `
+ `${BATCH_COUNT - (BUDGET_QA.ceil - startsBeforeBatch)} planes left waiting) inflight=${exhausted.inflight}`);
// ========================================================================
// E11 — refill window passes → enrichment RESUMES. Shorten windowMs so the
// time since the bucket's refill anchor now spans whole windows; the next
// sweep refills (clamped at ceil) and the waiting planes get requested.
// ========================================================================
console.log('\nE11 — refill window passes; ambient enrichment resumes');
await page.evaluate((winMs) => { window.__GEV_ENRICH_AMBIENT_QA.windowMs = winMs; }, RESUME_WINDOW_MS);
await sleep(RESUME_WINDOW_MS + 200); // a full (shortened) refill window elapses
await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
await dm.layers.get('flights').module.update(window.__godsEyeView.viewer);
});
const expectedTotal = startsBeforeBatch + BATCH_COUNT; // every batch plane eventually requested
const resumed = await page.waitForFunction(
(want) => window.__ENRICH_LOG.starts.length >= want && window.__ENRICH_LOG.inflight === 0,
{ timeout: 25000, polling: 100 }, expectedTotal
).then(() => true).catch(() => false);
const resumeLog = await page.evaluate(() => window.__ENRICH_LOG.starts.map((s) => s.hex));
const batchRequested = batchHexes.filter((h) => resumeLog.includes(h)).length;
const dupes = resumeLog.length !== new Set(resumeLog).size;
record('E11 resume: enrichment resumes after a refill window (all waiting planes requested, once each)',
resumed && batchRequested === BATCH_COUNT && resumeLog.length === expectedTotal && !dupes,
`starts=${resumeLog.length} (want ${expectedTotal}) batchRequested=${batchRequested}/${BATCH_COUNT} dupes=${dupes}`);
record('E12: no console errors during QA run', consoleErrors.length === 0,
consoleErrors.length ? `${consoleErrors.length}: ${consoleErrors.slice(0, 3).join(' | ')}` : 'clean');
finish();
} finally {
await browser.close();
}
function finish() {
const failed = results.filter((r) => r.ok === false);
const passed = results.filter((r) => r.ok === true);
console.log(`\n${'─'.repeat(60)}`);
console.log(` RESULT: ${passed.length} passed, ${failed.length} failed`);
console.log(`${'─'.repeat(60)}\n`);
process.exitCode = failed.length > 0 ? 1 : 0;
}
}
main().catch((err) => {
console.error('\n\x1b[31mQA harness error:\x1b[0m', err && err.stack ? err.stack : err);
process.exit(2);
});
+570
View File
@@ -0,0 +1,570 @@
#!/usr/bin/env node
/**
* qa-failstate-b10.mjs
*
* Targeted proof for Batch 10 — the "silent-failure" theme (finding H3 + H9).
* Drives the REAL app in headless Chromium and asserts that dead/failed feeds
* are SURFACED to the user instead of a healthy-looking empty state.
*
* Node-side request interception (page.setRequestInterception) fabricates the
* upstream failures so the run is deterministic and never depends on live
* AISStream / CelesTrak availability:
*
* (i) AIS invalid key — /api/ais-live is answered with
* {rows:[],status:'error',error:'invalid key'}. Assert the layer's
* getStats().error is set (feed down) — NOT a clean 'just now · 0'.
*
* (ii) CelesTrak outage — first serve a good catalog so there's something to
* preserve, then flip ALL /api/celestrak/* groups to HTTP 503. Toggle
* satellites off/on. Assert the catalog is NOT wiped to 0 AND
* getStats().error is set.
*
* (iii) DETECT with no data — enable panoptic detection with NO data layers.
* Assert the shared #world-overlay-canvas is non-empty (mode banner drawn),
* so DETECT reads "armed, nothing in view" instead of a blank canvas.
* The banner is developer telemetry, hidden from users by default, so this
* harness loads the app with ?detectDebug=1 — without it the pixel check
* would pass on scanlines alone and no longer assert what it names.
* (Absent-by-default is pinned in src/data/detectionHost.test.mjs.)
*
* Run: node scripts/qa-failstate-b10.mjs --url http://localhost:4300
* Exits non-zero if any assertion fails. DOES NOT COMMIT anything.
*/
import puppeteer from 'puppeteer';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
const argv = process.argv.slice(2);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const getFlag = (name) => argv.includes(name);
const APP_URL = getOpt('--url', 'http://localhost:4300');
/**
* Check (iii) asserts the detection MODE BANNER is drawn on a zero-object frame.
* That banner is developer telemetry and is hidden from users by default, so the
* harness must ask for it explicitly — otherwise the pixel check silently passes
* on scanlines alone and the assertion name stops meaning anything.
*/
function withDetectDebug(url) {
const parsed = new URL(url);
parsed.searchParams.set('detectDebug', '1');
return parsed.toString();
}
const HEADFUL = getFlag('--headful');
const ARTIFACT_DIR = getOpt('--artifact-dir', null);
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try {
if (fs.existsSync(candidate)) return candidate;
} catch { /* ignore */ }
}
return null;
}
const results = [];
function record(name, ok, detail) {
results.push({ name, ok, detail });
const tag = ok === null ? '\x1b[33mINCONCLUSIVE\x1b[0m' : ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function readLayerControl(page, layerId, expectedLabel) {
await page.waitForFunction(
(id, label) => {
const button = document.querySelector(`[data-layer-id="${id}"] .data-toggle-btn`);
return button?.textContent?.trim() === label;
},
{ timeout: 5000 },
layerId,
expectedLabel,
);
return page.evaluate((id) => {
const row = document.querySelector(`[data-layer-id="${id}"]`);
const button = row?.querySelector('.data-toggle-btn');
const meta = row?.querySelector('.data-toggle-meta');
return {
label: button?.textContent?.trim() || '',
feedState: button?.dataset?.feedState || '',
ariaLabel: button?.getAttribute('aria-label') || '',
meta: meta?.textContent?.trim() || '',
};
}, layerId);
}
async function captureLayerControl(page, layerId, filename) {
if (!ARTIFACT_DIR) return;
fs.mkdirSync(ARTIFACT_DIR, { recursive: true });
const dataPanel = await page.$('#data-panel');
const panelCollapsed = await dataPanel?.evaluate((element) => element.classList.contains('collapsed'));
if (panelCollapsed) await page.click('#data-panel .panel-collapse-btn');
const row = await page.$(`[data-layer-id="${layerId}"]`);
if (!row) throw new Error(`Layer control not found for screenshot: ${layerId}`);
await row.evaluate((element) => element.scrollIntoView({ block: 'center' }));
await page.waitForFunction((id) => {
const element = document.querySelector(`[data-layer-id="${id}"]`);
const rect = element?.getBoundingClientRect();
return !!rect && rect.width > 0 && rect.height > 0;
}, { timeout: 5_000 }, layerId);
await sleep(100);
await row.screenshot({ path: path.join(ARTIFACT_DIR, filename) });
}
// A minimal but valid TLE for one satellite (ISS), so the "good catalog" pass
// builds a non-zero catalog we can later prove is preserved across the outage.
// parseTLE in satellites.js expects `NAME\n1 ...\n2 ...` triplets.
const GOOD_TLE = [
'ISS (ZARYA)',
'1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927',
'2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537',
'NOAA 19',
'1 33591U 09005A 08264.51782528 -.00000045 00000-0 35116-4 0 9999',
'2 33591 99.1949 123.4567 0013000 200.0000 160.0000 14.12345678123456',
].join('\n');
async function main() {
console.log('\nSilent-Failure Proof (Batch 10 — H3 + H9)');
console.log(` App URL : ${APP_URL}`);
console.log(` Mode : ${HEADFUL ? 'headful' : 'headless'}\n`);
try {
const res = await fetch(APP_URL, { method: 'GET' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
console.error(`\x1b[31mDev server not reachable at ${APP_URL} (${e.message}).\x1b[0m`);
process.exit(2);
}
const chromeExecutable = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
'--use-angle=swiftshader',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1280,800',
],
});
let exitCode = 0;
// Node-side controllable interception mode, flipped mid-run.
// celestrak: 'good' → serve GOOD_TLE for every group
// celestrak: 'partial' → fail one group while preserving a usable catalog
// celestrak: 'down' → 503 for every group (total outage)
const mode = { celestrak: 'good' };
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.setRequestInterception(true);
page.on('request', (req) => {
const url = req.url();
// (i) AIS invalid-key: the exact payload the spec calls for.
if (url.includes('/api/ais-live') && !url.includes('/track')) {
req.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ rows: [], status: 'error', error: 'invalid key' }),
});
return;
}
// (ii) CelesTrak proxy — /api/celestrak/<group>
if (url.includes('/api/celestrak/')) {
if (mode.celestrak === 'down'
|| (mode.celestrak === 'partial' && url.includes('/api/celestrak/stations'))) {
req.respond({ status: 503, contentType: 'text/plain', body: 'upstream unavailable' });
} else {
req.respond({ status: 200, contentType: 'text/plain', body: GOOD_TLE });
}
return;
}
req.continue();
});
console.log('Loading app...');
await page.goto(withDetectDebug(APP_URL), { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() => window.__godsEyeView
&& window.__godsEyeView.viewer
&& window.__godsEyeView.dataManager
&& window.__godsEyeView.styleManager,
{ timeout: 60000 },
);
await sleep(1500);
// ── (0) Manager-owned periodic refresh → shared work/failure/recovery ──
console.log('\n(0) Universal periodic refresh feedback...');
const refreshFeedback = await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
const ids = ['flights', 'military', 'satellites'];
const styleManager = window.__godsEyeView.styleManager;
const outcomes = [];
const readBanner = () => ({
hidden: document.getElementById('global-loading-status')?.hidden,
state: document.getElementById('global-loading-status')?.dataset?.state || '',
label: document.getElementById('global-loading-label')?.textContent?.trim() || '',
detail: document.getElementById('global-loading-detail')?.textContent?.trim() || '',
});
for (const id of ids) {
const entry = dm.layers.get(id);
if (!entry) {
outcomes.push({ id, missing: true });
continue;
}
const original = {
enabled: entry.enabled,
initialized: entry.initialized,
lifecycleState: entry.lifecycleState,
update: entry.module.update,
getStats: entry.module.getStats,
managerRefreshError: entry.managerRefreshError,
};
const baseStats = typeof original.getStats === 'function'
? original.getStats.call(entry.module)
: { count: 0, lastUpdate: null };
let rejectRefresh;
try {
entry.enabled = true;
entry.initialized = true;
entry.lifecycleState = 'enabled';
entry.managerRefreshError = null;
entry.module.getStats = () => ({
...baseStats,
error: null,
lastError: null,
available: true,
unavailable: false,
loading: false,
refreshing: false,
});
entry.module.update = () => new Promise((resolve, reject) => {
rejectRefresh = reject;
});
const pendingFailure = dm._runPeriodicUpdate(id, entry);
styleManager._updateGlobalLoadingFeedback(performance.now());
await new Promise((resolve) => setTimeout(resolve, 220));
styleManager._updateGlobalLoadingFeedback(performance.now());
const working = readBanner();
rejectRefresh(new Error(`${id} QA refresh failure`));
await pendingFailure;
styleManager._updateGlobalLoadingFeedback(performance.now());
const failed = readBanner();
const managerError = entry.managerRefreshError;
entry.module.update = async () => true;
const pendingRecovery = dm._runPeriodicUpdate(id, entry);
styleManager._updateGlobalLoadingFeedback(performance.now());
await pendingRecovery;
styleManager._updateGlobalLoadingFeedback(performance.now());
outcomes.push({
id,
name: entry.module.name,
working,
failed,
managerError,
recoveredError: entry.managerRefreshError,
lifecycleState: entry.lifecycleState,
enabled: entry.enabled,
});
} finally {
entry.module.update = original.update;
entry.module.getStats = original.getStats;
entry.enabled = original.enabled;
entry.initialized = original.initialized;
entry.lifecycleState = original.lifecycleState;
entry.managerRefreshError = original.managerRefreshError;
}
}
return outcomes;
});
for (const outcome of refreshFeedback) {
const passed = !outcome.missing
&& outcome.working.hidden === false
&& outcome.working.label === 'REFRESHING LIVE DATA'
&& outcome.working.detail.includes(outcome.name)
&& outcome.failed.hidden === false
&& outcome.failed.label === 'LOAD FAILED'
&& outcome.failed.state === 'error'
&& outcome.managerError?.includes('QA refresh failure')
&& outcome.recoveredError === null
&& outcome.lifecycleState === 'enabled'
&& outcome.enabled === true;
record(
`${outcome.name || outcome.id}: periodic refresh reports work, failure, and recovery`,
passed,
JSON.stringify(outcome),
);
if (!passed) exitCode = 1;
}
// ── (i) AIS invalid key → surfaced error, not a clean empty ──────────────
console.log('\n(i) AIS invalid-key: enabling ais-live-vessels...');
const aisStats = await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
await dm.setEnabled('ais-live-vessels', true);
const mod = dm.layers.get('ais-live-vessels').module;
// DataLayerManager intentionally treats enable() as a synchronous
// lifecycle hook, while this layer starts its first poll asynchronously
// from enable(). Wait on the layer's public loading state instead of a
// machine-speed-dependent fixed delay so the assertion always observes
// the injected response, not an in-flight request.
const deadline = Date.now() + 5000;
while (mod.getStats().loading && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 50));
}
return mod.getStats();
});
{
const hasError = typeof aisStats.error === 'string' && aisStats.error.length > 0;
const cleanEmpty = !hasError && (aisStats.count === 0);
record(
'AIS: dead feed surfaces an error (not clean empty)',
hasError && !cleanEmpty,
`stats=${JSON.stringify({ error: aisStats.error, count: aisStats.count, lastUpdate: aisStats.lastUpdate })}`,
);
if (!hasError) exitCode = 1;
}
const aisControl = await readLayerControl(page, 'ais-live-vessels', 'UNAVAILABLE');
const aisChipHonest = aisControl.feedState === 'unavailable'
&& aisControl.ariaLabel === 'Live AIS Vessels: UNAVAILABLE';
const aisMetaHonest = /^UNAVAILABLE · AISStream · /i.test(aisControl.meta)
&& aisStats.error
&& aisControl.meta.includes(aisStats.error);
record(
'AIS: layer control reads UNAVAILABLE',
aisChipHonest,
JSON.stringify(aisControl),
);
record(
'AIS: layer metadata names the upstream error',
aisMetaHonest,
`meta=${JSON.stringify(aisControl.meta)}`,
);
if (!aisChipHonest || !aisMetaHonest) exitCode = 1;
await captureLayerControl(page, 'ais-live-vessels', 'failstate-ais-unavailable.png');
// ── (ii) CelesTrak outage on re-enable → catalog NOT wiped, error set ────
console.log('\n(ii) Satellites: building a good catalog first...');
const goodStats = await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
await dm.setEnabled('satellites', true);
await new Promise((r) => setTimeout(r, 800));
const mod = dm.layers.get('satellites').module;
return mod.getStats();
});
record(
'Satellites: good catalog loaded before outage (baseline)',
goodStats.count > 0,
`count=${goodStats.count} error=${goodStats.error ?? null}`,
);
if (!(goodStats.count > 0)) {
// Without a baseline the preservation assertion is meaningless.
record('Satellites: catalog preserved across total outage', null, 'no baseline catalog to preserve');
} else {
console.log(' Failing one CelesTrak group and verifying the visible degraded state...');
mode.celestrak = 'partial';
const partialStats = await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
await dm.setEnabled('satellites', false);
await new Promise((r) => setTimeout(r, 200));
await dm.setEnabled('satellites', true);
await new Promise((r) => setTimeout(r, 800));
return dm.layers.get('satellites').module.getStats();
});
const partialControl = await readLayerControl(page, 'satellites', 'DEGRADED');
const partialChipHonest = partialStats.count > 0
&& /1 CelesTrak group unavailable/i.test(partialStats.error || '')
&& partialControl.feedState === 'degraded'
&& partialControl.ariaLabel === 'Satellites: DEGRADED';
const partialMetaHonest = /^DEGRADED · CelesTrak · /i.test(partialControl.meta)
&& /1 CelesTrak group unavailable/i.test(partialControl.meta);
record(
'Satellites: partial outage renders a DEGRADED chip',
partialChipHonest,
JSON.stringify({ stats: partialStats, control: partialControl }),
);
record(
'Satellites: degraded metadata names partial coverage',
partialMetaHonest,
`meta=${JSON.stringify(partialControl.meta)}`,
);
if (!partialChipHonest || !partialMetaHonest) exitCode = 1;
await captureLayerControl(page, 'satellites', 'failstate-satellites-degraded.png');
console.log(' Flipping ALL CelesTrak groups to 503 and toggling satellites off→on...');
mode.celestrak = 'down';
const outageStats = await page.evaluate(async (baseline) => {
const dm = window.__godsEyeView.dataManager;
// Toggle off then on — the re-enable's update() re-fetches (now all 503).
await dm.setEnabled('satellites', false);
await new Promise((r) => setTimeout(r, 200));
await dm.setEnabled('satellites', true);
await new Promise((r) => setTimeout(r, 800));
const mod = dm.layers.get('satellites').module;
return { stats: mod.getStats(), baseline };
}, goodStats.count);
const s = outageStats.stats;
const notWiped = s.count > 0; // catalog preserved, not blanked to 0
const errorSet = typeof s.error === 'string' && s.error.length > 0;
const outageControl = await readLayerControl(page, 'satellites', 'UNAVAILABLE');
const outageChipHonest = outageControl.feedState === 'unavailable'
&& outageControl.ariaLabel === 'Satellites: UNAVAILABLE';
const outageMetaHonest = /^UNAVAILABLE · CelesTrak · /i.test(outageControl.meta)
&& outageControl.meta.includes(s.error || '');
record(
'Satellites: catalog NOT wiped to 0 on total outage',
notWiped,
`count=${s.count} (baseline=${outageStats.baseline})`,
);
record(
'Satellites: outage surfaces stats.error',
errorSet,
`error=${JSON.stringify(s.error)} lastUpdate=${s.lastUpdate}`,
);
record(
'Satellites: total outage renders an UNAVAILABLE chip',
outageChipHonest,
JSON.stringify(outageControl),
);
record(
'Satellites: unavailable metadata names the outage',
outageMetaHonest,
`meta=${JSON.stringify(outageControl.meta)}`,
);
if (!notWiped || !errorSet || !outageChipHonest || !outageMetaHonest) exitCode = 1;
await captureLayerControl(page, 'satellites', 'failstate-satellites-unavailable.png');
}
// ── (iii) DETECT with no data layers → mode banner drawn (non-blank) ─────
console.log('\n(iii) DETECT with no data layers: disabling data layers, enabling panoptic...');
const detect = await page.evaluate(async () => {
const gev = window.__godsEyeView;
const dm = gev.dataManager;
// Turn OFF every data layer so detection collects zero objects.
for (const [id, entry] of dm.layers) {
if (entry.enabled) { try { await dm.setEnabled(id, false); } catch { /* ignore */ } }
}
// Enable panoptic detection via the styleManager facade (the UI path).
gev.styleManager.setDetection({ enabled: true, mode: 'panoptic' });
const canvas = document.getElementById('world-overlay-canvas');
if (!canvas) return { present: false };
const viewer = gev.viewer;
const tileset = gev.tileset;
const priorDefaultLoop = viewer.useDefaultRenderLoop;
const priorTilesetShow = tileset?.show;
let nonEmpty = 0;
let solid = 0;
let sampled = 0;
viewer.useDefaultRenderLoop = false;
if (tileset) tileset.show = false;
try {
const deadline = performance.now() + 5000;
while (performance.now() < deadline && solid <= 20) {
viewer.render();
await new Promise((resolve) => setTimeout(resolve, 50));
const w = canvas.width, h = canvas.height;
const off = document.createElement('canvas');
off.width = w; off.height = h;
const octx = off.getContext('2d');
// The detection pass (banner included) paints on the host-owned
// blend-isolation surface beneath the shared canvas — composite
// both so the sample sees whichever surface carries the banner.
const detSurface = document.getElementById('world-overlay-detection-surface');
if (detSurface) octx.drawImage(detSurface, 0, 0);
octx.drawImage(canvas, 0, 0);
const dpr = window.devicePixelRatio || 1;
const bw = Math.min(w, Math.round(260 * dpr));
const bh = Math.min(h, Math.round(48 * dpr));
const data = octx.getImageData(0, 0, bw, bh).data;
sampled = bw * bh;
nonEmpty = 0;
solid = 0;
for (let i = 3; i < data.length; i += 4) {
if (data[i] > 4) nonEmpty++;
// The banner paints labelBg at full opacity (alpha 0.66-0.78 => 168+).
// Scanlines paint the SAME color but scaled by _theme.scanline, and
// only on every fourth row, so they never reach this threshold. This
// is what separates "the banner is drawn" from "something is drawn".
if (data[i] > 128) solid++;
}
}
} finally {
if (tileset) tileset.show = priorTilesetShow;
viewer.useDefaultRenderLoop = priorDefaultLoop;
viewer.scene.requestRender();
}
const detState = gev.styleManager.getDetectionState?.() || null;
return { present: true, nonEmpty, solid, sampled, mode: detState?.detectionMode ?? null };
});
if (!detect.present) {
record('DETECT: mode banner drawn on 0 objects', false, 'world-overlay canvas absent');
exitCode = 1;
} else {
// Solid pixels only: a non-empty canvas proves SOMETHING painted, which
// faint scanlines alone satisfy. The banner's opaque background is what
// this check is named for, so that is what it asserts.
const bannerDrawn = detect.solid > 20;
record(
'DETECT: mode banner drawn on 0 objects (opaque banner fill present)',
bannerDrawn,
`solidPx=${detect.solid} nonEmptyPx=${detect.nonEmpty}/${detect.sampled} mode=${detect.mode}`,
);
if (!bannerDrawn) exitCode = 1;
}
} catch (e) {
console.error('\x1b[31mHarness error:\x1b[0m', e);
exitCode = 3;
} finally {
await browser.close();
}
const pass = results.filter((r) => r.ok === true).length;
const fail = results.filter((r) => r.ok === false).length;
const inconclusive = results.filter((r) => r.ok === null).length;
console.log('\n' + '─'.repeat(60));
console.log(` RESULT: ${pass} passed, ${fail} failed, ${inconclusive} inconclusive`);
console.log('─'.repeat(60) + '\n');
process.exit(exitCode || (fail > 0 ? 1 : 0));
}
main().catch((e) => {
console.error(e);
process.exit(3);
});
+481
View File
@@ -0,0 +1,481 @@
#!/usr/bin/env node
/**
* qa-firms.mjs — headless proof for the LIVE NASA FIRMS fires layer.
*
* Drives the REAL app in headless Chromium against a dev server with a
* FIRMS key (default :4420). The live section rides the proxy's 30-min disk
* cache (zero upstream quota risk per run); the degraded states are
* fabricated with request interception so they're deterministic.
*
* (i) LIVE — layer loads >1000 detections, no error, label 'LIVE ·',
* /api/firms/status reports the key + transaction telemetry.
* (ii) CARDS — the shared world-overlay canvas reports painted FIRMS
* entries and is non-blank at a fire cluster (detections LOD) AND at
* global zoom (cell cards); aggregate cells expose no action target.
* (iii) KEYLESS — /api/firms intercepted to 503 {error:'no_key'}:
* stats.error 'KEY REQUIRED', zero fires, no crash.
* (iv) STALE — /api/firms intercepted to a stale:true payload with
* fresh-enough detections: stats.stale true, error 'STALE · …',
* fires still render (old data beats a wiped map).
* (v) ACTION (synthetic) — one deterministic intercepted detection's
* accessible card selects exactly that stable fire and transfers the
* camera through the shared UI navigation policy.
*
* Visual proof saved to qa-shots/ (gitignored).
*
* Run: node scripts/qa-firms.mjs --url http://localhost:4420
* Exits non-zero on any FAIL. Does not commit anything.
*/
import puppeteer from 'puppeteer';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
const SHOTS_DIR = path.join(REPO_ROOT, 'qa-shots');
const argv = process.argv.slice(2);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const APP_URL = getOpt('--url', 'http://localhost:4420');
const HEADFUL = argv.includes('--headful');
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try { if (fs.existsSync(candidate)) return candidate; } catch { /* ignore */ }
}
return null;
}
const results = [];
function record(name, ok, detail) {
results.push({ name, ok, detail });
const tag = ok === null ? '\x1b[33mINCONCLUSIVE\x1b[0m' : ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/** Non-transparent pixels plus source-scoped host diagnostics. */
async function cardCanvasInk(page) {
return page.evaluate(() => {
const canvas = document.getElementById('world-overlay-canvas');
const diagnostics = window.__gevWorldOverlay?.getDiagnostics?.();
const painted = diagnostics?.paintedBySource?.firms || 0;
const entries = diagnostics?.entriesBySource?.firms || 0;
if (!canvas) return {
present: false, ink: 0, painted, entries,
candidates: diagnostics?.candidateCount || 0,
projected: diagnostics?.projectedCount || 0,
};
const ctx = canvas.getContext('2d');
const { width, height } = canvas;
if (!width || !height) return {
present: true, ink: 0, painted, entries,
candidates: diagnostics?.candidateCount || 0,
projected: diagnostics?.projectedCount || 0,
};
const data = ctx.getImageData(0, 0, width, height).data;
let ink = 0;
for (let i = 3; i < data.length; i += 4) if (data[i] > 8) ink++;
return {
present: true, ink, painted, entries,
candidates: diagnostics?.candidateCount || 0,
projected: diagnostics?.projectedCount || 0,
};
});
}
/** Wait for two consecutive painted frames instead of sampling a render gap. */
async function waitForCardCanvasInk(page, { timeoutMs = 12000 } = {}) {
const deadline = Date.now() + timeoutMs;
let consecutive = 0;
let sample = null;
while (Date.now() < deadline) {
await page.evaluate(() => window.__godsEyeView?.viewer?.scene?.requestRender?.());
await sleep(150);
sample = await cardCanvasInk(page);
if (sample.present && sample.entries > 0 && sample.painted > 0 && sample.ink > 500) {
consecutive += 1;
if (consecutive >= 2) return sample;
} else {
consecutive = 0;
}
}
return sample || {
present: false, ink: 0, painted: 0, entries: 0, candidates: 0, projected: 0,
};
}
/** Currently painted FIRMS actions mirrored for keyboard/assistive input. */
async function firmsActionSnapshot(page) {
return page.evaluate(() => {
const buttons = [...document.querySelectorAll(
'#world-overlay-action-list button[data-overlay-action-key]',
)].filter((button) => String(button.dataset.overlayActionKey || '').startsWith('firms\u0000'));
return {
count: buttons.length,
selectedCount: buttons.filter((button) => button.getAttribute('aria-pressed') === 'true').length,
labels: buttons.map((button) => button.getAttribute('aria-label') || button.textContent || ''),
};
});
}
/** Wait until the accessible mirror reflects the requested FIRMS action count. */
async function waitForFirmsActionCount(page, expected, { timeoutMs = 12000 } = {}) {
const deadline = Date.now() + timeoutMs;
let snapshot = await firmsActionSnapshot(page);
while (Date.now() < deadline && snapshot.count !== expected) {
await page.evaluate(() => window.__godsEyeView?.viewer?.scene?.requestRender?.());
await sleep(175);
snapshot = await firmsActionSnapshot(page);
}
return snapshot;
}
/** Boot the app and enable the fires layer, polling until settled. */
async function bootAndEnable(page, { timeoutS = 45 } = {}) {
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
{ timeout: 60000 },
);
await sleep(1500);
return page.evaluate(async (tS) => {
const dm = window.__godsEyeView.dataManager;
await dm.setEnabled('local-firms', true);
const mod = dm.layers.get('local-firms').module;
let s = null;
for (let i = 0; i < tS; i++) {
await new Promise((r) => setTimeout(r, 1000));
s = mod.getStats();
if ((s.count > 0 || s.error) && !s.loading) break;
}
return s;
}, timeoutS);
}
/** Teleport the camera (duck-typed cartographic — no Cesium global). */
async function setView(page, lon, lat, height) {
await page.evaluate((lo, la, h) => {
const gev = window.__godsEyeView;
const ell = gev.viewer.scene.globe.ellipsoid;
const d2r = Math.PI / 180;
// The app's intro flyTo animation clobbers a setView issued mid-flight.
try { gev.viewer.camera.cancelFlight(); } catch { /* no flight active */ }
gev.viewer.camera.setView({
destination: ell.cartographicToCartesian({ longitude: lo * d2r, latitude: la * d2r, height: h }),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
gev.viewer.scene.requestRender?.();
}, lon, lat, height);
}
/** Fabricated stale-but-recent proxy payload for section (iv). */
function stalePayload() {
const now = Date.now();
const acq = new Date(now - 2 * 3600000);
const acqDate = acq.toISOString().slice(0, 10);
const acqTime = `${String(acq.getUTCHours()).padStart(2, '0')}${String(acq.getUTCMinutes()).padStart(2, '0')}`;
const mk = (lat, lon, frp, confidence) => ({
lat, lon, frp, confidence, brightness: 330, brightnessTi5: 290,
daynight: 'N', acqDate, acqTime, satellite: 'N20', instrument: 'VIIRS',
});
return {
fetchedAt: now - 2 * 3600000,
stale: true,
ttlMs: 1800000,
sources: [{ source: 'VIIRS_NOAA20_NRT', count: 3, ok: false }],
count: 3,
fires: [mk(61.9, -122.9, 900, 'h'), mk(61.95, -122.8, 45, 'n'), mk(61.85, -123.0, 4, 'l')],
};
}
async function main() {
console.log('\nLive NASA FIRMS Proof (qa-firms)');
console.log(` App URL : ${APP_URL}\n`);
try {
const res = await fetch(APP_URL);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
console.error(`\x1b[31mDev server not reachable at ${APP_URL} (${e.message}).\x1b[0m`);
process.exit(2);
}
const status = await fetch(`${APP_URL}/api/firms/status`).then((r) => r.json()).catch(() => null);
if (!status?.hasKey) {
console.error('\x1b[31mServer has no FIRMS key — run against the keyed dev server (:4420).\x1b[0m');
process.exit(2);
}
fs.mkdirSync(SHOTS_DIR, { recursive: true });
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(findChromeExecutable() ? { executablePath: findChromeExecutable() } : {}),
args: [
'--no-sandbox', '--disable-setuid-sandbox', '--use-gl=angle', '--use-angle=swiftshader',
'--disable-dev-shm-usage', '--disable-web-security',
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
'--window-size=1440,900',
],
});
let exitCode = 0;
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
// ── (i) LIVE feed ────────────────────────────────────────────────────────
console.log('(i) LIVE — loading the fires layer through the cached proxy...');
const live = await bootAndEnable(page);
{
const ok = live.count > 1000 && !live.error && String(live.loadingLabel || '').startsWith('LIVE');
record('LIVE: >1000 detections, no error, LIVE label', ok,
`count=${live.count} error=${JSON.stringify(live.error)} label=${JSON.stringify(live.loadingLabel)}`);
record('LIVE: /api/firms/status has key + transaction telemetry',
status.hasKey === true && (status.transactions === null || Number.isFinite(status.transactions?.used)),
`transactions=${JSON.stringify(status.transactions)} count=${status.count}`);
if (!ok) exitCode = 1;
}
// ── (ii) CARDS — canvas overlay draws at both LODs ──────────────────────
console.log('\n(ii) CARDS — tactical card overlay ink at both LODs...');
{
const strongest = await page.evaluate(() => {
const mod = window.__godsEyeView.dataManager.layers.get('local-firms').module;
return mod.getStrongestFire();
});
if (!strongest) {
record('CARDS: detections-LOD cards drawn', false, 'no strongest fire available');
exitCode = 1;
} else {
await setView(page, strongest.longitude, strongest.latitude, 60000);
const det = await waitForCardCanvasInk(page);
record('CARDS: detections-LOD cards drawn (host source + canvas ink)',
det.present && det.painted > 0 && det.ink > 500,
`entries=${det.entries} candidates=${det.candidates} projected=${det.projected} `
+ `painted=${det.painted} inkPx=${det.ink} @ strongest fire FRP ${strongest.frp}`);
if (!(det.present && det.painted > 0 && det.ink > 500)) exitCode = 1;
await page.screenshot({ path: path.join(SHOTS_DIR, 'firms-cards-detections.png') });
// Global LOD band starts at 9,000 km; its labelDistance is 12,000 km
// (cards fade to zero beyond it, matching the legacy labels) — so the
// cell-card proof must sit inside that window.
await setView(page, strongest.longitude, strongest.latitude, 10000000);
// Do not let the previous detections frame satisfy the ink predicate
// before the 650 ms LOD watcher has swapped in aggregate cell cards.
const aggregateActions = await waitForFirmsActionCount(page, 0);
const cells = await waitForCardCanvasInk(page);
record('CARDS: global-LOD cell cards drawn (host source + canvas ink)',
cells.present && cells.painted > 0 && cells.ink > 500,
`entries=${cells.entries} candidates=${cells.candidates} projected=${cells.projected} `
+ `painted=${cells.painted} inkPx=${cells.ink}`);
if (!(cells.present && cells.painted > 0 && cells.ink > 500)) exitCode = 1;
record('CARDS: aggregate cells expose no FIRMS action target',
aggregateActions.count === 0,
`actionButtons=${aggregateActions.count}`);
if (aggregateActions.count !== 0) exitCode = 1;
await page.screenshot({ path: path.join(SHOTS_DIR, 'firms-global-cells.png') });
}
}
// ── (iii) KEYLESS (intercepted) ──────────────────────────────────────────
console.log('\n(iii) KEYLESS — intercepted 503 no_key...');
await page.setRequestInterception(true);
let interceptMode = 'keyless';
page.on('request', (req) => {
const url = req.url();
if (url.includes('/api/firms/status')) {
req.respond({
status: 200, contentType: 'application/json',
body: JSON.stringify(interceptMode === 'keyless'
? { hasKey: false }
: { hasKey: true, lastFetch: Date.now() - 7200000, count: 3, stale: true, ttlMs: 1800000, transactions: null }),
});
return;
}
if (url.includes('/api/firms')) {
if (interceptMode === 'keyless') {
req.respond({ status: 503, contentType: 'application/json', body: JSON.stringify({ error: 'no_key' }) });
} else {
req.respond({ status: 200, contentType: 'application/json', body: JSON.stringify(stalePayload()) });
}
return;
}
try { req.continue(); } catch { /* already handled */ }
});
const keyless = await bootAndEnable(page, { timeoutS: 20 });
{
const ok = keyless.error === 'KEY REQUIRED' && keyless.count === 0;
record('KEYLESS: error "KEY REQUIRED", zero fires, no crash', ok,
`error=${JSON.stringify(keyless.error)} count=${keyless.count} label=${JSON.stringify(keyless.loadingLabel)}`);
if (!ok) exitCode = 1;
await page.screenshot({ path: path.join(SHOTS_DIR, 'firms-keyless.png') });
}
// ── (iv) STALE (intercepted) ─────────────────────────────────────────────
console.log('\n(iv) STALE — intercepted stale:true payload...');
interceptMode = 'stale';
const stale = await bootAndEnable(page, { timeoutS: 20 });
{
const ok = stale.stale === true
&& String(stale.error || '').startsWith('STALE')
&& stale.count === 3;
record('STALE: stats.stale, "STALE · cached" error, fires still render', ok,
`stale=${stale.stale} error=${JSON.stringify(stale.error)} count=${stale.count}`);
if (!ok) exitCode = 1;
await page.screenshot({ path: path.join(SHOTS_DIR, 'firms-stale.png') });
}
// ── (v) ACTION (deterministic intercepted detection) ────────────────────────
console.log('\n(v) ACTION — synthetic intercepted fire card through shared camera policy...');
await setView(page, -122.9, 61.9, 60000);
const actionCards = await waitForCardCanvasInk(page);
const beforeActions = await firmsActionSnapshot(page);
if (!(actionCards.present && actionCards.painted > 0 && beforeActions.count > 0)) {
record('ACTION (synthetic): individual fire action is available', false,
`painted=${actionCards.painted} actionButtons=${beforeActions.count}`);
exitCode = 1;
} else {
const prepared = await page.evaluate(() => {
const gev = window.__godsEyeView;
const mod = gev.dataManager.layers.get('local-firms').module;
const target = mod.getDetectableObjects({ maxCount: 1 })[0];
if (!target?.position) return null;
const sentinel = gev.viewer.entities.add({
id: 'qa-firms-prior-camera-owner',
position: target.position,
});
gev.viewer.trackedEntity = sentinel;
const camera = gev.viewer.camera;
const originalFly = camera.flyToBoundingSphere;
const proof = {
sentinel,
originalFly,
flightCount: 0,
trackedAtFlight: 'not-called',
request: null,
generationBefore: gev.styleManager._navigationGeneration,
};
proof.onRequest = (event) => {
proof.request = {
kind: event.detail?.kind || null,
id: event.detail?.id || null,
};
};
window.addEventListener('gev:world-request-focus', proof.onRequest);
camera.flyToBoundingSphere = function qaFirmsObservedFlight(...args) {
proof.flightCount += 1;
proof.trackedAtFlight = gev.viewer.trackedEntity?.id || null;
return originalFly.apply(this, args);
};
window.__qaFirmsActionProof = proof;
return { generationBefore: proof.generationBefore };
});
if (!prepared) {
record('ACTION (synthetic): individual fire action is available', false,
'no deterministic fire position');
exitCode = 1;
} else {
const clicked = await page.evaluate(() => {
const button = [...document.querySelectorAll(
'#world-overlay-action-list button[data-overlay-action-key]',
)].find((candidate) => String(candidate.dataset.overlayActionKey || '').startsWith('firms\u0000'));
button?.click();
return Boolean(button);
});
for (let i = 0; i < 30; i += 1) {
const settled = await page.evaluate(() => {
window.__godsEyeView?.viewer?.scene?.requestRender?.();
return (window.__qaFirmsActionProof?.flightCount || 0) > 0;
});
if (settled) break;
await sleep(100);
}
await sleep(250);
const proof = await page.evaluate(() => {
const gev = window.__godsEyeView;
const state = window.__qaFirmsActionProof;
const actionButtons = [...document.querySelectorAll(
'#world-overlay-action-list button[data-overlay-action-key]',
)].filter((button) => String(button.dataset.overlayActionKey || '').startsWith('firms\u0000'));
const selectedEntityId = window.__gevContextStore?.selectedEntityId || null;
const result = {
flightCount: state?.flightCount || 0,
trackedAtFlight: state?.trackedAtFlight,
trackingReleased: gev.viewer.trackedEntity == null,
generationBefore: state?.generationBefore,
generationAfter: gev.styleManager._navigationGeneration,
request: state?.request || null,
selectedEntityId,
selectedActionCount: actionButtons.filter(
(button) => button.getAttribute('aria-pressed') === 'true',
).length,
};
if (state) {
gev.viewer.camera.flyToBoundingSphere = state.originalFly;
window.removeEventListener('gev:world-request-focus', state.onRequest);
if (state.sentinel) gev.viewer.entities.remove(state.sentinel);
}
delete window.__qaFirmsActionProof;
return result;
});
const actionOk = clicked
&& proof.flightCount === 1
&& proof.trackedAtFlight == null
&& proof.trackingReleased
&& proof.generationAfter === proof.generationBefore + 1
&& proof.request?.kind === 'fire'
&& proof.request?.id === proof.selectedEntityId
&& proof.selectedActionCount === 1;
record('ACTION (synthetic): stable fire selects once and shared policy owns flight', actionOk,
`clicked=${clicked} flights=${proof.flightCount} trackedAtFlight=${JSON.stringify(proof.trackedAtFlight)} `
+ `generation=${proof.generationBefore}->${proof.generationAfter} `
+ `request=${JSON.stringify(proof.request)} selected=${JSON.stringify(proof.selectedEntityId)} `
+ `selectedActions=${proof.selectedActionCount}`);
if (!actionOk) exitCode = 1;
await page.screenshot({ path: path.join(SHOTS_DIR, 'firms-synthetic-fire-action.png') });
}
}
} catch (e) {
console.error('\x1b[31mHarness error:\x1b[0m', e);
exitCode = 3;
} finally {
await browser.close();
}
const pass = results.filter((r) => r.ok === true).length;
const fail = results.filter((r) => r.ok === false).length;
console.log('\n' + '─'.repeat(60));
console.log(` RESULT: ${pass} passed, ${fail} failed`);
console.log(` Shots : ${SHOTS_DIR}/firms-*.png`);
console.log('─'.repeat(60) + '\n');
process.exit(exitCode || (fail > 0 ? 1 : 0));
}
main().catch((e) => { console.error(e); process.exit(3); });
+519
View File
@@ -0,0 +1,519 @@
#!/usr/bin/env node
/**
* Are the first-run launcher's pins actually load-bearing?
*
* A pin that only goes red when you delete the whole feature proves very little.
* This reverts each decision INDIVIDUALLY — the smallest edit that reintroduces
* the original defect or contradicts the product rule — and requires
* src/firstRunExperience.test.mjs to go red for it. Every entry names what it
* restores, so the count is reproducible rather than asserted in a commit
* message.
*
* node scripts/qa-firstrun-mutations.mjs
*
* Every touched file is restored on exit, including on failure.
*
* NOTE: two mutations edit vite.config.js, and a running dev server watches that
* file and restarts on every write. Writes are therefore content-guarded below
* so the file is touched exactly twice per mutation instead of on every
* iteration — enough that a dev server survives, but expect it to restart. If
* you are mid-QA on a live server, run this before or after, not during.
*/
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const TESTS = 'src/firstRunExperience.test.mjs';
const FILES = {
module: path.join(ROOT, 'src', 'firstRunExperience.js'),
html: path.join(ROOT, 'index.html'),
css: path.join(ROOT, 'style.css'),
vite: path.join(ROOT, 'vite.config.js'),
main: path.join(ROOT, 'src', 'main.js'),
ui: path.join(ROOT, 'src', 'ui.js'),
docs: path.join(ROOT, 'docs', 'CURRENT-STATE.md'),
};
/** @type {Array<{defect: string, file: keyof FILES, from: string, to: string}>} */
const MUTATIONS = [
// ── Show policy (product decision: session-scoped dismiss vs durable checkbox) ──
{
defect: 'dismissing writes the DURABLE key, so the launcher never returns',
file: 'module',
from: "writeStored('session', sessionStorageRef, FIRST_RUN_SESSION_KEY, 'dismissed');",
to: "writeStored('local', sessionStorageRef, FIRST_RUN_STORAGE_KEY, 'suppressed');",
},
{
defect: 'the checkbox is ignored, so nothing can ever suppress the launcher',
file: 'module',
from: " ? writeStored('local', storage, FIRST_RUN_STORAGE_KEY, 'suppressed')",
to: ' ? false',
},
{
defect: 'unticking the box cannot take the suppression back',
file: 'module',
from: " : removeStored('local', storage, FIRST_RUN_STORAGE_KEY);",
to: ' : true;',
},
{
defect: 'the session flag is never read, so it re-nags on every reload',
file: 'module',
from: "if (readStored('session', sessionStorageRef, FIRST_RUN_SESSION_KEY) === 'dismissed') return false;",
to: '// session check removed',
},
{
defect: 'the durable flag is never read, so the checkbox does nothing',
file: 'module',
from: "if (readStored('local', storage, FIRST_RUN_STORAGE_KEY) === 'suppressed') return false;",
to: '// durable check removed',
},
{
defect: '?welcome=1 no longer outranks suppression, so demos cannot replay it',
file: 'module',
from: "if (params.get('welcome') === '1') return true;",
to: '// replay hatch removed',
},
{
defect: '?welcome=0 stops suppressing',
file: 'module',
from: "if (params.get('welcome') === '0') return false;",
to: '// suppression param removed',
},
{
defect: 'a share link gets the launcher over the view its author chose',
file: 'module',
from: 'if (hasShareState) return false;',
to: '// share bypass removed',
},
{
defect: 'storage is read from a DEFAULT PARAMETER, outside every guard',
file: 'module',
from: 'export function rememberFirstRunSessionDismissed(sessionStorageRef) {',
to: 'export function rememberFirstRunSessionDismissed(sessionStorageRef = globalThis.sessionStorage) {',
},
{
defect: 'shouldShowFirstRun resolves storage in its parameter list again',
file: 'module',
from: 'export function shouldShowFirstRun({\n hasShareState = false,\n storage,\n sessionStorageRef,',
to: 'export function shouldShowFirstRun({\n hasShareState = false,\n storage = globalThis.localStorage,\n sessionStorageRef = globalThis.sessionStorage,',
},
{
defect: 'the guarded resolver stops catching, so a hostile getter escapes',
file: 'module',
from: " try {\n return kind === 'session' ? globalThis.sessionStorage : globalThis.localStorage;\n } catch {\n // Privacy-restricted storage should not make first launch silent.\n return null;\n }",
to: " return kind === 'session' ? globalThis.sessionStorage : globalThis.localStorage;",
},
{
defect: 'a blocked getItem throws instead of reading as "nothing stored"',
file: 'module',
from: ' return resolveStore(kind, injected)?.getItem?.(key) ?? null;\n } catch {\n return null;\n }',
to: ' return resolveStore(kind, injected)?.getItem?.(key) ?? null;\n } finally {\n // no catch\n }',
},
// ── ESC arbitration ───────────────────────────────────────────────────────
{
defect: 'the key handler trusts the CLASS, so an invisible card eats ESC',
file: 'module',
from: 'if (closing || !isTopmost()) return;',
to: 'if (closing) return;',
},
{
defect: 'the launcher stops yielding and fights Cockpit/Scenes for ESC',
file: 'module',
from: 'if (revealed && blocked) yieldToExclusiveSurface();\n else if (!revealed && !blocked) reveal();',
to: 'if (!revealed) reveal();',
},
{
defect: 'nothing watches for another surface taking the screen',
file: 'module',
from: "surfaceObserver?.observe(documentRef.body, { attributes: true, attributeFilter: ['class'] });",
to: '/* observer never attached */',
},
{
defect: 'cockpit drops off the exclusive list and can stack with the launcher',
file: 'module',
from: "export const EXCLUSIVE_SURFACE_CLASSES = Object.freeze([\n 'cockpit-mode',",
to: 'export const EXCLUSIVE_SURFACE_CLASSES = Object.freeze([',
},
{
defect: 'a scene hides the launcher in CSS but the JS never learns of it',
file: 'css',
from: 'body.scene-playback-mode #first-run-launcher {',
to: 'body.scene-playback-mode-typo #first-run-launcher {',
},
{
defect: 'yielding steals focus back from the surface that just took over',
file: 'module',
from: 'dismiss({ restoreFocus: false });',
to: 'dismiss();',
},
{
defect: 'isTopmost stops hit-testing, so an unclassed overlay buries the card again',
file: 'module',
from: ' && root.getClientRects().length > 0\n && !coveredByOverlay();',
to: ' && root.getClientRects().length > 0;',
},
{
defect: 'an inconclusive hit test reads as COVERED, so ESC quietly stops working',
file: 'module',
from: ' return Boolean(hit) && !root.contains(hit);',
to: ' return !root.contains(hit);',
},
{
defect: 'the launcher stops honouring a key another surface already claimed',
file: 'module',
from: ' if (event.defaultPrevented) return;',
to: ' /* belt removed */',
},
{
// Anchored past the call so it cannot land on the cockpit disclosure's own
// stopImmediatePropagation() a few lines below and prove nothing about this.
defect: 'the radio disclosure returns to stopPropagation, so one ESC does two things',
file: 'ui',
from: ' event.stopImmediatePropagation();\n setRadioDisclosure(false, { returnFocus: true });',
to: ' event.stopPropagation();\n setRadioDisclosure(false, { returnFocus: true });',
},
{
defect: 'the "no timer" decision is deleted, so the next editor re-litigates it blind',
file: 'module',
from: ' * ACCEPTED, DELIBERATELY NOT TIMED OUT: a surface class that never clears',
to: ' * (note removed)',
},
{
defect: 'a bounded reveal timer punches the card through a recording in progress',
file: 'module',
from: ' const syncToExclusiveSurfaces = () => {\n if (closing) return;',
to: ' const syncToExclusiveSurfaces = () => {\n if (closing) return;\n globalThis.setTimeout?.(reveal, 45000);',
},
{
defect: 'the accepted no-show vanishes from CURRENT-STATE and reads as a bug',
file: 'docs',
from: 'a surface class that never clears means no launcher for that page',
to: 'the launcher always turns up eventually for that page',
},
// ── The checkbox may not promise what storage refused ─────────────────────
{
defect: 'a refused write leaves the box ticked, promising a suppression nobody stored',
file: 'module',
from: ' if (setFirstRunSuppressed(wanted, storage)) return;',
to: ' setFirstRunSuppressed(wanted, storage);\n return;',
},
{
defect: 'a missing storage area reports the write as saved',
file: 'module',
from: " if (typeof store?.setItem !== 'function') return false;\n store.setItem(key, value);\n return true;",
to: ' store?.setItem?.(key, value);\n return true;',
},
{
defect: 'the scroll fade promises more list on a card where everything fits',
file: 'module',
from: 'choiceList.dataset.scrollable = String(overflows);',
to: "choiceList.dataset.scrollable = 'true'; void overflows;",
},
{
defect: 'the DISPLAY rail opens on first run again, stealing the impression',
file: 'ui',
from: "if (panelId === 'pp-toggles' && stored === null) collapsed = true;",
to: "if (panelId === 'pp-toggles' && stored === null) collapsed = false;",
},
{
defect: 'the first-run rail default overrides a stored user choice',
file: 'ui',
from: "if (panelId === 'pp-toggles' && stored === null) collapsed = true;",
to: "if (panelId === 'pp-toggles') collapsed = true;",
},
// ── Mission definitions ────────────────────────────────────────────────────
{
defect: 'the removed INFRASTRUCTURE tile comes back as a one-click globe dump',
file: 'module',
from: " environmental: Object.freeze({",
to: " infrastructure: Object.freeze({\n kind: 'globe',\n layerIds: Object.freeze(['local-datacenters', 'local-dams', 'telegeography-submarine-cables']),\n busyText: 'Mapping global infrastructure…',\n }),\n environmental: Object.freeze({",
},
{
defect: 'ENVIRONMENTAL drops the keyless earthquakes that carry it without a key',
file: 'module',
from: "layerIds: Object.freeze(['earthquakes', 'local-firms']),",
to: "layerIds: Object.freeze(['local-firms']),",
},
{
defect: 'FIRMS is dropped again from the tile whose subcopy promises it',
file: 'module',
from: "layerIds: Object.freeze(['earthquakes', 'local-firms']),",
to: "layerIds: Object.freeze(['earthquakes']),",
},
{
defect: 'the tile stops promising the fires it actually turns on',
file: 'html',
from: '<small>Live earthquakes and active fires, from USGS and NASA</small>',
to: '<small>Live earthquakes worldwide, straight from USGS</small>',
},
{
defect: "the final first-run line is quietly rewritten",
file: 'html',
from: 'It feels like a forbidden cockpit—then you realize the sources are public and the data is real.',
to: "It feels like a forbidden cockpit. It isn't — every feed is public, and every contact is live.",
},
{
defect: 'the ledgered global-chip defect loses its pointer to the real fix',
file: 'module',
from: ' // means a KEY REQUIRED terminal state in src/loadingFeedback.js, a state',
to: ' // (note removed)',
},
{
defect: 'the globe missions never pull the camera out',
file: 'module',
from: ' .then(() => flyToGlobe())',
to: ' .then(() => null)',
},
{
defect: 'a refused layer is reported as a successful mission',
file: 'module',
from: 'return { ok: failedLayerIds.length === 0, choice, failedLayerIds };',
to: 'return { ok: true, choice, failedLayerIds };',
},
{
defect: 'a cancelled globe flight fails the whole mission',
file: 'module',
from: ' .catch(() => null);',
to: ' .catch(() => { throw new Error("flight failed"); });',
},
{
defect: 'Explore manually quietly runs a mission instead of nothing',
file: 'module',
from: "if (mission.kind === 'none') return { ok: true, choice };",
to: "if (mission.kind === 'none') return { ok: true, choice, ran: await setContextMode('contacts') };",
},
{
defect: 'an unknown choice is treated as a success',
file: 'module',
from: 'if (!mission) return { ok: false, choice };',
to: 'if (!mission) return { ok: true, choice };',
},
{
defect: 'the fires/quakes tile name stops being switchable from one constant',
file: 'module',
from: 'return ENVIRONMENTAL_LABELS[choice] || ENVIRONMENTAL_LABELS.ENVIRONMENTAL;',
to: 'return ENVIRONMENTAL_LABELS.ENVIRONMENTAL;',
},
// ── Defaults interplay (the ruling this whole integration turns on) ────────
{
defect: "mission layer enables stop persisting, unlike the clicks they stand for",
file: 'module',
from: "dataManager.setEnabled(layerId, true, { origin: 'user' })",
to: 'dataManager.setEnabled(layerId, true)',
},
{
defect: 'a mission hand-edits detection and kills the style auto-preset contract',
file: 'module',
from: ' flyToGlobe: () => styleManager.resetToGlobeView(),',
to: ' flyToGlobe: () => { styleManager._detectionUserOverridden = true; return styleManager.resetToGlobeView(); },',
},
{
defect: 'a mission persists a 3D-models choice nobody made',
file: 'module',
from: ' const result = await styleManager.setContextMode(mode);',
to: ' styleManager._setModels3dEnabled?.(true);\n const result = await styleManager.setContextMode(mode);',
},
{
defect: 'the Context panel is left collapsed, hiding the mode that just started',
file: 'module',
from: "styleManager.setPanelCollapsed?.('global-context-panel', false, { explicit: true });",
to: '// panel reveal removed',
},
{
defect: 'the decision table is deleted, so the next editor re-litigates it blind',
file: 'module',
from: ' * MISSION → APP STATE, AND WHAT IT IS ALLOWED TO PERSIST',
to: ' * (notes removed)',
},
// ── Accessibility and the transplant defects ──────────────────────────────
{
defect: 'disabling the focused tile drops the keyboard to <body> mid-flight',
file: 'module',
from: "for (const button of buttons) button.setAttribute('aria-disabled', String(next));",
to: 'for (const button of buttons) button.disabled = next;',
},
{
defect: 'Tab escapes into an app the visitor has not seen yet',
file: 'module',
from: "if (event.key !== 'Tab') return;",
to: 'return;',
},
{
defect: 'ESC no longer dismisses the launcher',
file: 'module',
from: "if (event.key === 'Escape') {",
to: 'if (false) {',
},
{
defect: 'focus is never returned to where the visitor left it',
file: 'module',
from: "if (typeof previouslyFocused?.focus === 'function' && previouslyFocused.isConnected) {",
to: 'if (false) {',
},
{
defect: 'app hotkeys eat the launcher keys (bubble instead of capture)',
file: 'module',
from: "documentRef.addEventListener('keydown', onKeyDown, true);",
to: "documentRef.addEventListener('keydown', onKeyDown);",
},
{
defect: 'the card stays clickable through its fade-out (double-launch)',
file: 'css',
from: ' transition: opacity 220ms ease, transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1);\n /* Inert until revealed AND again while fading out, so a click during the\n dismiss animation cannot launch a second mission behind the fade. */\n pointer-events: none;',
to: ' transition: opacity 220ms ease, transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1);',
},
{
defect: 'the launcher survives Clean-UI and recording mode',
file: 'css',
from: 'body.ui-clean-view #first-run-launcher,',
to: 'body.never-matches-anything #first-run-launcher,',
},
{
defect: 'the flex card defeats [hidden] and sits in the a11y tree pre-reveal',
file: 'css',
from: '#first-run-launcher[hidden] {\n display: none;\n}',
to: '/* [hidden] rule removed */',
},
{
defect: 'the card loses its viewport cap and clips on a short landscape phone',
file: 'css',
from: ' max-height: calc(100dvh - 1.5rem);',
to: ' /* cap removed */',
},
{
defect: 'the mission list stops scrolling, so tiles overflow the capped card',
file: 'css',
from: ' min-height: 0;\n overflow-y: auto;',
to: ' /* scroll removed */',
},
{
// Anchored to the launcher's OWN selectors: style.css has four other
// reduced-motion blocks, and a bare `@media` anchor would mutate one of
// those instead and prove nothing about this feature.
defect: "the launcher's reduced-motion opt-out is dropped",
file: 'css',
from: ' #first-run-launcher,\n .first-run-choices button,\n .first-run-arrow { transition: none; }\n /* Scrolling a tile into view must not animate either. */\n .first-run-choices { scroll-behavior: auto; }',
to: ' .first-run-nothing { transition: none; }',
},
// ── Markup and startup ordering ───────────────────────────────────────────
{
defect: 'a mission tile is dropped from the menu',
file: 'html',
from: ' <button type="button" data-first-run-choice="space-missions">',
to: ' <button type="button" data-first-run-choice-disabled="space-missions">',
},
{
defect: 'the menu order stops matching the owner\'s',
file: 'html',
from: '<button type="button" data-first-run-choice="environmental">',
to: '<button type="button" data-first-run-choice="zzz-environmental">',
},
{
defect: 'the "don\'t show again" checkbox is removed',
file: 'html',
from: '<input type="checkbox" data-first-run-suppress />',
to: '<span data-first-run-suppress-removed></span>',
},
{
defect: 'the status line stops being a polite live region',
file: 'html',
from: 'data-first-run-status role="status" aria-live="polite"',
to: 'data-first-run-status',
},
{
defect: 'the launcher is no longer announced as a dialog',
file: 'html',
from: '<aside id="first-run-launcher" role="dialog"',
to: '<aside id="first-run-launcher"',
},
{
defect: 'the launcher is revealed before the loading cover yields',
file: 'main',
from: " loadingScreen.classList.add('hidden');",
to: ' initFirstRunExperience({ styleManager, dataManager });\n loadingScreen.classList.add(\'hidden\');',
},
{
defect: 'the globe missions lose their DataManager and cannot enable layers',
file: 'main',
from: 'initFirstRunExperience({ styleManager, dataManager });',
to: 'initFirstRunExperience({ styleManager });',
},
// ── Voice: schema must not drift ──────────────────────────────────────────
{
defect: 'the voice TOOL SCHEMA is edited (a Realtime prompt-cache bust)',
file: 'vite',
from: " 'earthquakes',\n 'satellites',",
to: " 'earthquakes',\n 'infrastructure-mode',\n 'satellites',",
},
{
defect: 'the instruction mapping is dropped, so voice cannot reach the modes',
file: 'vite',
from: " 'NAMED VIEWS are shorthand",
to: " // 'NAMED VIEWS are shorthand",
},
];
const originals = new Map();
for (const [key, file] of Object.entries(FILES)) originals.set(key, fs.readFileSync(file, 'utf8'));
/** Write only on a real change: a no-op write still wakes every file watcher. */
const write = (file, next) => {
if (fs.readFileSync(file, 'utf8') !== next) fs.writeFileSync(file, next);
};
const restoreAll = () => {
for (const [key, file] of Object.entries(FILES)) write(file, originals.get(key));
};
process.on('exit', restoreAll);
let caught = 0;
const missed = [];
console.log(`\nFirst-run launcher pin strength — ${MUTATIONS.length} individual reverts\n`);
for (const { defect, file, from, to } of MUTATIONS) {
const original = originals.get(file);
if (!original.includes(from)) {
missed.push(`${defect} (ANCHOR MISSING — the mutation no longer applies)`);
console.log(` \x1b[31mSTALE\x1b[0m ${defect}`);
continue;
}
write(FILES[file], original.replace(from, to));
let red = false;
let by = '';
try {
execFileSync('node', ['--test', TESTS], { cwd: ROOT, encoding: 'utf8', stdio: 'pipe' });
} catch (error) {
red = true;
const failed = String(error.stdout || '').split('\n')
.filter((line) => line.trim().startsWith('✖') && line.includes('('))
.map((line) => line.trim().slice(2).split(' (')[0]);
by = [...new Set(failed)].slice(0, 2).join('; ');
}
write(FILES[file], original);
if (red) {
caught += 1;
console.log(` \x1b[32mRED \x1b[0m ${defect}\n caught by: ${by}`);
} else {
missed.push(defect);
console.log(` \x1b[31mGREEN\x1b[0m ${defect} <-- no pin covers this`);
}
}
restoreAll();
console.log(`\n ${caught}/${MUTATIONS.length} defects caught by the pins`);
if (missed.length) {
console.log('\n uncovered:');
for (const item of missed) console.log(` - ${item}`);
}
console.log('');
process.exitCode = missed.length ? 1 : 0;
+878
View File
@@ -0,0 +1,878 @@
#!/usr/bin/env node
/**
* qa-firstrun — the mission launcher's contract, proved in the real app.
*
* The unit suite pins the show policy and the mission table against fakes. This
* harness answers the questions only a running app can: does a mission actually
* enable those layers, does the camera actually reach the globe, does a keyless
* visitor actually get the honest FIRMS state instead of a silent empty layer,
* and does any of it disturb the reasonable-defaults startup look.
*
* It also captures the owner's taste-pass screenshots into qa-shots/firstrun/.
*
* Usage:
* node scripts/qa-firstrun.mjs --url http://localhost:4278
* node scripts/qa-firstrun.mjs --url http://localhost:4278 --teeth
*
* `--teeth` is the negative control: it suppresses the launcher's own reveal, so
* every rendered/behavioral assertion below must go RED. A green --teeth run
* means the assertions are not measuring what they claim to.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import puppeteer from 'puppeteer';
const args = process.argv.slice(2);
const getOpt = (flag, fallback) => {
const index = args.indexOf(flag);
return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
};
const APP_URL = getOpt('--url', 'http://localhost:4173').replace(/\/$/, '');
const TEETH = args.includes('--teeth');
const HEADFUL = args.includes('--headful');
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const SHOT_DIR = path.join(ROOT, 'qa-shots', 'firstrun');
const CHROME_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Version-pinned Chrome-for-Testing over the auto-updating system Chrome.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
].filter(Boolean);
const results = [];
let currentSection = 'setup';
function record(name, ok, detail) {
results.push({ name, ok, detail, section: currentSection });
console.log(` [${ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m'}] ${name}${detail ? ` — ${detail}` : ''}`);
}
/**
* Run one independent section.
*
* Sections are isolated on purpose. The first version of this harness ran as a
* single straight line, so the FIRST throw ended the run — under `--teeth` that
* meant it stopped at 9 of 38 checks and never exercised the mission, viewport
* or console assertions at all, while still claiming the control had teeth. A
* throw now fails its own section and the rest still run, which is what makes
* the per-section reachability tally below meaningful.
*/
async function section(name, body) {
currentSection = name;
console.log(`\n \x1b[2m── ${name} ──\x1b[0m`);
try {
await body();
} catch (error) {
record(`[${name}] section completed without throwing`, false, error.message);
}
}
const LAUNCHER = '#first-run-launcher';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* Load the app and wait for the launcher to settle one way or the other.
*
* `errorSink` is emptied on the way out: navigating away aborts whatever the
* previous page had in flight, and Chrome reports those aborts as
* "TypeError: Failed to fetch". Those belong to this harness, not to the app,
* so the console assertion only ever judges the page it is actually looking at.
*/
async function open(page, { hash = '', query = '', clearAll = true, clearSession = true, errorSink = null } = {}) {
await page.goto(`${APP_URL}/${query}${hash}`, { waitUntil: 'domcontentloaded' });
if (clearAll || clearSession) {
await page.evaluate((all) => {
if (all) localStorage.clear();
sessionStorage.clear();
}, clearAll);
await page.goto(`${APP_URL}/${query}${hash}`, { waitUntil: 'domcontentloaded' });
}
// The launcher is revealed after the loading cover yields (~T+1.9 s).
await page.waitForFunction(
(sel) => !!window.__godsEyeView?.styleManager
&& (document.querySelector(sel)?.classList.contains('visible') || !document.querySelector(sel)),
{ timeout: 45000 },
LAUNCHER,
).catch(() => {});
await sleep(400);
if (errorSink) errorSink.length = 0;
}
const launcherVisible = (page) => page.evaluate(
(sel) => !!document.querySelector(sel)?.classList.contains('visible'),
LAUNCHER,
);
const appState = (page) => page.evaluate(() => {
const gev = window.__godsEyeView || {};
const sm = gev.styleManager;
const dm = gev.dataManager;
const carto = sm?.viewer?.camera?.positionCartographic;
const layers = {};
const counts = {};
const all = dm?.getAll?.() || [];
for (const id of [
'local-datacenters', 'local-dams', 'telegeography-submarine-cables',
'local-firms', 'earthquakes', 'flights', 'military', 'rocket-launches', 'satellites',
]) {
layers[id] = !!dm?.isEnabled?.(id);
counts[id] = all.find((entry) => entry.id === id)?.stats?.count ?? null;
}
const firms = all.find((entry) => entry.id === 'local-firms');
return {
heightKm: carto ? Math.round(carto.height / 1000) : null,
layers,
counts,
contextMode: sm?.getContextModeState?.().mode ?? null,
firmsError: firms?.stats?.error ?? null,
firmsCount: firms?.stats?.count ?? null,
detectionOverridden: sm?._detectionUserOverridden ?? null,
durable: localStorage.getItem('gev:first-run-mission:v1'),
session: sessionStorage.getItem('gev:first-run-mission-session:v1'),
layerStateBlob: localStorage.getItem('gev:layer-state:v2'),
allocation: localStorage.getItem('gev:detection-allocation:v1'),
};
});
/**
* Click a mission tile, wait for the launcher to close, then let the layers it
* asked for actually settle. Asserting (or screenshotting) before the feeds have
* drawn measures the harness's own impatience, not the mission.
*/
/**
* Start recording every state the GLOBAL loading chip passes through. The chip
* is transient — sampling it once after a mission would miss a failure banner
* that flashed and cleared — so this watches it continuously instead.
*/
async function watchLoadingChip(page) {
await page.evaluate(() => {
window.__chipSeen = [];
const el = document.getElementById('global-loading-status');
if (!el) return;
const sample = () => {
const label = document.getElementById('global-loading-label')?.textContent?.trim() || '';
const state = el.dataset.state || '';
const hidden = el.hidden;
const last = window.__chipSeen[window.__chipSeen.length - 1];
const entry = `${hidden ? 'hidden' : 'shown'}:${state}:${label}`;
if (entry !== last) window.__chipSeen.push(entry);
};
sample();
window.__chipObserver?.disconnect();
window.__chipObserver = new MutationObserver(sample);
window.__chipObserver.observe(el, {
attributes: true, childList: true, subtree: true, characterData: true,
});
}).catch(() => {});
}
const readLoadingChip = (page) => page.evaluate(() => window.__chipSeen || []);
async function pick(page, choice, { timeout = 40000, settle = [] } = {}) {
await page.click(`[data-first-run-choice="${choice}"]`);
await page.waitForFunction(
(sel) => !document.querySelector(sel) || !document.querySelector(sel).classList.contains('visible'),
{ timeout },
LAUNCHER,
).catch(() => {});
if (settle.length) {
await page.evaluate(async (ids) => {
const dm = window.__godsEyeView?.dataManager;
await Promise.all(ids.map((id) => dm?.waitForLayerSettled?.(id)));
}, settle).catch(() => {});
}
// Plus a render beat: settled data still has to reach the framebuffer.
await sleep(settle.length ? 3500 : 1500);
}
/** Every section the run is expected to reach; the teeth verdict requires all. */
const EXPECTED_SECTIONS = [
'show-policy', 'esc-arbitration',
'mission-environmental', 'mission-contacts', 'mission-space', 'mission-explore',
'viewports', 'console',
];
/*
* Sections that are NOT expected to go red under --teeth.
*
* `console` is a hygiene check, not a launcher check: with the launcher removed
* the page legitimately still logs nothing, so demanding a red there would be
* demanding a false failure. Everything else exists to test the launcher and
* must collapse without it. This list is deliberately tiny and named — it is the
* one place a section can be excused, so an unjustified entry is visible.
*/
const HYGIENE_SECTIONS = ['console'];
/**
* ESC arbitration — the two defects that motivated the yield design, plus the
* yield itself. Driven through the real body classes, because that is exactly
* what Cockpit and the Scene director set.
*/
async function runArbitrationSection(page, { shots, consoleErrors }) {
await section('esc-arbitration', async () => {
const launcherState = () => page.evaluate((sel) => {
const node = document.querySelector(sel);
const rect = node?.getBoundingClientRect();
// Topmost, not merely present: hit-test the card's own centre the way the
// launcher does, so an overlay that leaves the box intact is visible here.
const hit = rect && rect.width > 0 && rect.height > 0
? document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2)
: null;
return {
present: !!node,
classVisible: !!node?.classList.contains('visible'),
// Real visibility: `display:none` yields zero client rects.
onScreen: !!node && node.getClientRects().length > 0,
topmost: !!(hit && node && node.contains(hit)),
session: sessionStorage.getItem('gev:first-run-mission-session:v1'),
};
}, LAUNCHER);
// ── Blocker 2 repro: a Scene starts while the launcher is up ────────────
await open(page, { query: "?welcome=1", errorSink: consoleErrors });
const beforeScene = await launcherState();
record('the launcher is up before the scene starts', beforeScene.onScreen);
await page.evaluate(() => document.body.classList.add('scene-playback-mode'));
await sleep(300);
const duringScene = await launcherState();
record(
'a scene taking the screen makes the launcher YIELD, not just hide',
beforeScene.onScreen && !duringScene.classVisible,
`onScreen=${duringScene.onScreen} classVisible=${duringScene.classVisible} — a hidden card that kept "visible" is the defect`,
);
record('yielding is session-scoped, like any other dismissal',
duringScene.session === 'dismissed', `session=${duringScene.session}`);
// The key that used to dismiss an invisible launcher must now do nothing
// to it — proven by the scene class still being set afterwards.
await page.keyboard.press('Escape');
await sleep(300);
const afterEsc = await page.evaluate(() => ({
sceneStillOn: document.body.classList.contains('scene-playback-mode'),
}));
record(
'ESC during a scene never lands on the launcher',
afterEsc.sceneStillOn,
afterEsc.sceneStillOn ? 'scene class untouched by the launcher' : 'something else consumed the key',
);
await page.evaluate(() => document.body.classList.remove('scene-playback-mode'));
await sleep(200);
const afterScene = await launcherState();
record('a yielded launcher does not pop back when the scene ends',
!afterScene.classVisible, `classVisible=${afterScene.classVisible}`);
// ── Blocker 3 repro: Cockpit engages while the launcher is up ───────────
await open(page, { query: "?welcome=1", errorSink: consoleErrors });
const beforeCockpit = await launcherState();
record('the launcher is up before cockpit engages', beforeCockpit.onScreen);
await page.evaluate(() => document.body.classList.add('cockpit-mode'));
await sleep(300);
const duringCockpit = await launcherState();
record(
'cockpit engaging makes the launcher YIELD before it can contest ESC',
beforeCockpit.onScreen && !duringCockpit.classVisible,
`classVisible=${duringCockpit.classVisible} — cockpit registers its capture listener first, so stacking must never arise`,
);
await page.evaluate(() => document.body.classList.remove('cockpit-mode'));
await sleep(200);
// ── Blocker repro: a surface that takes the screen with NO class ────────
// The attribution lightbox is full-screen at z-index 200 against the card's
// 175 and announces itself with nothing at all. The card keeps its box, so a
// getClientRects()-only check called it visible: ESC dismissed a launcher
// the visitor could not see, and burned the session flag, behind a lightbox
// that binds no ESC of its own and therefore stayed open.
const lightboxState = () => page.evaluate(() => {
const overlay = document.querySelector('.cesium-credit-lightbox-overlay');
return {
present: !!overlay,
shown: !!overlay && getComputedStyle(overlay).display !== 'none',
z: overlay ? getComputedStyle(overlay).zIndex : null,
};
});
await open(page, { query: "?welcome=1", errorSink: consoleErrors });
const beforeLightbox = await launcherState();
record('the launcher is up before the attribution lightbox opens', beforeLightbox.onScreen);
const linkClicked = await page.evaluate(() => {
const link = document.querySelector('#cesium-credits .cesium-credit-expand-link');
link?.click();
return !!link;
});
await sleep(300);
const lightboxUp = await lightboxState();
record(
'the real "Data attribution" lightbox opens above the card',
linkClicked && lightboxUp.shown && lightboxUp.z === '200',
`link=${linkClicked} shown=${lightboxUp.shown} z-index=${lightboxUp.z} vs the launcher's 175`,
);
const buried = await launcherState();
record(
'an unclassed overlay leaves the card MEASURABLE but no longer topmost',
buried.onScreen && !buried.topmost,
`onScreen=${buried.onScreen} topmost=${buried.topmost} — the class watch cannot see this surface at all`,
);
await page.keyboard.press('Escape');
await sleep(400);
const escUnderLightbox = await launcherState();
const lightboxAfterEsc = await lightboxState();
record(
'ESC under the lightbox never lands on the buried launcher',
beforeLightbox.onScreen && escUnderLightbox.classVisible,
`classVisible=${escUnderLightbox.classVisible} — the card was up, covered, and must stay up`,
);
record(
'that ESC does not burn the session flag either',
beforeLightbox.onScreen && escUnderLightbox.session !== 'dismissed',
`session=${escUnderLightbox.session}`,
);
record(
'the lightbox keeps its OWN ESC semantics (Cesium binds none, so it stays)',
lightboxAfterEsc.shown,
`overlay shown=${lightboxAfterEsc.shown}`,
);
// The guard disarms the launcher; it must never break it. Close the overlay
// the way a visitor does and the key comes straight back.
await page.evaluate(() => document.querySelector('.cesium-credit-lightbox-close')?.click());
await sleep(300);
const uncovered = await launcherState();
record('closing the lightbox hands the card back the key',
uncovered.topmost, `topmost=${uncovered.topmost}`);
await page.keyboard.press('Escape');
await sleep(600);
const afterUncoveredEsc = await launcherState();
record(
'ESC dismisses normally again once nothing is on top',
beforeLightbox.onScreen && !afterUncoveredEsc.classVisible
&& afterUncoveredEsc.session === 'dismissed',
`classVisible=${afterUncoveredEsc.classVisible} session=${afterUncoveredEsc.session}`,
);
// ── Blocker repro: a control that claims only the KEY ───────────────────
// The compact Radio disclosure closes on ESC from a capture listener bound
// long before this module exists. It called stopPropagation(), which does
// NOT stop later listeners on the same document — so one key closed the
// disclosure AND dismissed the launcher. The card is not hiding behind this
// one and must not yield to it: the key is simply already spoken for.
await open(page, { query: "?welcome=1", errorSink: consoleErrors });
const beforeRadio = await launcherState();
record('the launcher is up before the radio disclosure opens', beforeRadio.onScreen);
const radioOpened = await page.evaluate(() => {
document.getElementById('context-radio-toggle-btn')?.click();
return !!document.getElementById('context-radio-dock')?.classList.contains('disclosure-open');
});
await sleep(250);
record('the compact Radio disclosure opens', radioOpened);
const withRadio = await launcherState();
record(
'a small disclosure is a key contest, not a screen takeover — no yield',
withRadio.classVisible && withRadio.topmost,
`classVisible=${withRadio.classVisible} topmost=${withRadio.topmost}`,
);
await page.keyboard.press('Escape');
await sleep(400);
const radioAfterEsc = await page.evaluate(() => (
!!document.getElementById('context-radio-dock')?.classList.contains('disclosure-open')
));
const launcherAfterRadioEsc = await launcherState();
record('ESC closes the disclosure', radioOpened && !radioAfterEsc,
`disclosure still open=${radioAfterEsc}`);
record(
'that SAME ESC does not also dismiss the launcher',
beforeRadio.onScreen && launcherAfterRadioEsc.classVisible
&& launcherAfterRadioEsc.session !== 'dismissed',
`classVisible=${launcherAfterRadioEsc.classVisible} session=${launcherAfterRadioEsc.session} — one key, one action`,
);
// ...and the NEXT press is the launcher's, so nothing was permanently taken.
await page.keyboard.press('Escape');
await sleep(600);
const escAfterRadioClosed = await launcherState();
record(
'the next ESC belongs to the launcher again',
beforeRadio.onScreen && !escAfterRadioClosed.classVisible
&& escAfterRadioClosed.session === 'dismissed',
`classVisible=${escAfterRadioClosed.classVisible} session=${escAfterRadioClosed.session}`,
);
// ── Deferred reveal: a surface already up when the launcher would show ──
// The class goes on as soon as the app exists, which is well before the
// launcher's reveal (~T+1.9s), so init genuinely sees a screen it does not
// own. Cockpit's own exit() strips this class, so it is re-asserted right up
// to the check rather than set once and hoped for.
await page.evaluate(() => { localStorage.clear(); sessionStorage.clear(); });
await page.goto(`${APP_URL}/?welcome=1`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => !!document.body, { timeout: 45000 }).catch(() => {});
const holdCockpit = async (ms) => {
const until = Date.now() + ms;
while (Date.now() < until) {
await page.evaluate(() => document.body?.classList.add('cockpit-mode')).catch(() => {});
await sleep(200);
}
};
await holdCockpit(5000);
const withCockpitUp = await launcherState();
const classHeld = await page.evaluate(() => document.body.classList.contains('cockpit-mode'));
record(
'the launcher WAITS rather than appearing over a surface already up',
classHeld && withCockpitUp.present && !withCockpitUp.classVisible,
`cockpitClassHeld=${classHeld} present=${withCockpitUp.present} classVisible=${withCockpitUp.classVisible}`,
);
record('waiting does not burn the session flag',
withCockpitUp.session !== 'dismissed', `session=${withCockpitUp.session}`);
// ...and appears once that surface clears.
await page.evaluate(() => document.body.classList.remove('cockpit-mode'));
await sleep(800);
const afterCockpitCleared = await launcherState();
record('the launcher appears once the surface clears',
afterCockpitCleared.onScreen, `onScreen=${afterCockpitCleared.onScreen}`);
if (afterCockpitCleared.onScreen) shots.push(await shoot(page, 'arbitration-revealed-after-cockpit'));
});
}
async function shoot(page, name) {
// The teeth run deliberately has no launcher, so its frames are pictures of
// an empty loading screen. Writing them would overwrite the real evidence
// this directory exists to hold, so the control captures nothing.
if (TEETH) return `${name}.png (skipped: teeth run)`;
const file = path.join(SHOT_DIR, `${name}.png`);
await page.screenshot({ path: file });
return path.relative(ROOT, file);
}
async function main() {
console.log('\nFirst-Run Mission Launcher QA');
console.log(` App URL : ${APP_URL}`);
console.log(` Mode : ${TEETH ? 'TEETH (launcher suppressed — expect RED)' : 'normal'}\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',
'--use-angle=metal', '--enable-gpu', '--ignore-gpu-blocklist',
'--disable-dev-shm-usage', '--disable-background-timer-throttling',
'--disable-renderer-backgrounding', '--window-size=1440,900',
],
});
const shots = [];
const consoleErrors = [];
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
page.on('console', (message) => {
if (message.type() !== 'error') return;
const text = message.text();
if (!/Failed to load resource.*(404|429|503)/i.test(text)) consoleErrors.push(text);
});
page.on('pageerror', (error) => consoleErrors.push(`pageerror: ${error.message}`));
if (TEETH) {
// Negative control: the markup never arrives, so nothing can be revealed.
await page.evaluateOnNewDocument(() => {
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('first-run-launcher')?.remove();
});
});
}
await section('show-policy', async () => {
await open(page);
const freshVisible = await launcherVisible(page);
record('a fresh session gets the launcher', freshVisible);
if (freshVisible) shots.push(await shoot(page, 'launcher-desktop'));
const focused = await page.evaluate(() => document.activeElement?.dataset?.firstRunChoice ?? null);
record('focus lands on the first mission tile', focused === 'contacts', `activeElement=${focused}`);
// The card is a flex column (so its list can scroll on short viewports),
// and an author `display` on this id outranks the UA's `[hidden]` rule.
// Prove the attribute still hides it, or it sits in the a11y tree from
// page parse until reveal — and on a share link, until removal.
const hiddenHonored = await page.evaluate((sel) => {
const node = document.querySelector(sel);
if (!node) return null;
node.hidden = true;
const display = getComputedStyle(node).display;
node.hidden = false;
return display;
}, LAUNCHER);
record('the hidden attribute still hides the flex card', hiddenHonored === 'none',
`computed display while hidden = ${hiddenHonored}`);
const tiles = await page.$$eval('[data-first-run-choice]', (nodes) => nodes.map((n) => n.dataset.firstRunChoice));
record(
'four tiles in the owner\'s order',
JSON.stringify(tiles) === JSON.stringify(['contacts', 'space-missions', 'environmental', 'explore']),
tiles.join(' · '),
);
// ESC dismisses. Stated as a TRANSITION (was up, now gone) — "not visible"
// alone passes vacuously whenever the launcher never appeared at all.
await page.keyboard.press('Escape');
await sleep(600);
const state = await appState(page);
record('ESC dismisses the launcher', freshVisible && !(await launcherVisible(page)),
freshVisible ? 'visible → dismissed' : 'never appeared, so nothing was dismissed');
record('ESC writes the SESSION flag only', state.session === 'dismissed' && state.durable === null,
`session=${state.session} durable=${state.durable}`);
// Same session, reload → stays gone. Also non-vacuous: it only means
// anything if this session had seen the launcher in the first place.
await open(page, { clearAll: false, clearSession: false });
record('a reload in the same session does not re-nag',
freshVisible && !(await launcherVisible(page)),
freshVisible ? 'seen this session, stayed away on reload' : 'never appeared, so the check is empty');
// New session (sessionStorage cleared, localStorage kept) → back.
await open(page, { clearAll: false, clearSession: true });
record('the next fresh session gets it again', await launcherVisible(page));
// Checkbox → durable suppression.
await page.click('[data-first-run-suppress]');
await sleep(200);
const ticked = await appState(page);
record('the checkbox writes durable suppression immediately', ticked.durable === 'suppressed', `durable=${ticked.durable}`);
await page.keyboard.press('Escape');
await sleep(400);
await open(page, { clearAll: false, clearSession: true });
record('a suppressed profile stays quiet in later sessions',
ticked.durable === 'suppressed' && !(await launcherVisible(page)),
ticked.durable === 'suppressed' ? 'suppressed and stayed away' : 'nothing was ever suppressed');
// ?welcome=1 outranks the checkbox; ?welcome=0 suppresses a fresh session.
await open(page, { clearAll: false, clearSession: true, query: "?welcome=1", errorSink: consoleErrors });
record('?welcome=1 replays past durable suppression', await launcherVisible(page));
await open(page, { query: '?welcome=0' });
record('?welcome=0 suppresses a fresh session', !(await launcherVisible(page)));
// Share links bypass entirely.
await open(page, { hash: '#lat=30.2672&lon=-97.7431&alt=2500' });
const shareState = await page.evaluate(() => !!window.__godsEyeView?.styleManager?.hasShareState);
const shareShowed = await launcherVisible(page);
record('a share link bypasses the launcher', shareState && !shareShowed,
`hasShareState=${shareState} launcherVisible=${shareShowed}`);
});
await runArbitrationSection(page, { shots, consoleErrors });
// ── Mission outcomes ────────────────────────────────────────────────────
// One section PER MISSION: a throw in one (a tile that never rendered under
// --teeth, say) must not take the other four down with it, or the teeth
// tally reports "reached" for assertions that never ran.
let state;
let before;
await section('mission-environmental', async () => {
// Full clear, not just the session: layer enables are DURABLE, so without it
// an earlier mission's layers ride along and this stops measuring
// Environmental alone (and its screenshot stops showing it alone).
await open(page, { query: "?welcome=1", errorSink: consoleErrors });
before = await appState(page);
await watchLoadingChip(page);
await pick(page, 'environmental', { settle: ['earthquakes', 'local-firms'] });
state = await appState(page);
record('ENVIRONMENTAL leaves the detection override untouched',
state.detectionOverridden === false, `_detectionUserOverridden=${state.detectionOverridden}`);
record('a mission never auto-suppresses itself', state.durable === null, `durable=${state.durable}`);
record('a mission never writes the detection-allocation pref',
state.allocation === before.allocation, `${before.allocation} → ${state.allocation}`);
record('ENVIRONMENTAL enables BOTH of its feeds', state.layers.earthquakes && state.layers['local-firms'],
`quakes=${state.layers.earthquakes} fires=${state.layers['local-firms']}`);
record('ENVIRONMENTAL loads real quake records', (state.counts.earthquakes ?? 0) > 0,
`${state.counts.earthquakes} quakes`);
record('ENVIRONMENTAL reaches the full-earth camera', state.heightKm !== null && state.heightKm > 12000,
`${state.heightKm} km`);
/*
* The tile optimizes for the FULLY CONFIGURED app, so the assertion splits
* on what this server actually has rather than pretending one answer fits
* both. The branch is reported, so a green run always says which one it was.
*
* KEYED — both datasets must actually arrive, and a failure banner in
* that state is a real defect, so the chip IS asserted.
* KEYLESS — the LAYER ROW reports KEY REQUIRED while the global batch
* completes without presenting that deliberate configuration
* state as a failed mission.
*/
const keyless = state.firmsError === 'KEY REQUIRED';
console.log(` \x1b[2m FIRMS key state: ${keyless ? 'KEYLESS' : 'KEYED'} `
+ `(row="${state.firmsError ?? 'none'}", count=${state.firmsCount})\x1b[0m`);
if (keyless) {
record(
'KEYLESS: the FIRMS row tells the visitor the truth (KEY REQUIRED)',
state.firmsError === 'KEY REQUIRED',
`layer row reports "${state.firmsError}"`,
);
record(
'KEYLESS: the quakes half of the tile still delivers in full',
(state.counts.earthquakes ?? 0) > 0,
`${state.counts.earthquakes} quakes`,
);
const chip = await readLoadingChip(page);
const failed = chip.filter((entry) => /LOAD FAILED/i.test(entry));
record(
'KEYLESS: a missing optional FIRMS key never becomes a global load failure',
failed.length === 0,
failed.length
? `chip showed: ${failed.join(' | ')}`
: `chip states seen: ${chip.join(' → ') || 'none'}`,
);
} else {
record(
'KEYED: both datasets actually arrive',
(state.counts.earthquakes ?? 0) > 0 && (state.firmsCount ?? 0) > 0,
`${state.counts.earthquakes} quakes · ${state.firmsCount} fire detections`,
);
const chip = await readLoadingChip(page);
const failed = chip.filter((entry) => /LOAD FAILED/i.test(entry));
// A STALE served-from-cache FIRMS payload is degraded, not failed, and it
// still carries data — so the failure test is the banner, not any
// non-empty error string.
record(
'KEYED: no unexpected global failure while the mission runs',
failed.length === 0,
failed.length
? `chip showed: ${failed.join(' | ')}`
: `firms row="${state.firmsError ?? 'none'}"; chip states seen: ${chip.join(' → ') || 'none'}`,
);
}
record(
'no mission turns on the bundled infrastructure layers any more',
!state.layers['local-datacenters'] && !state.layers['local-dams']
&& !state.layers['telegeography-submarine-cables'],
'the removed tile left nothing enabling ~5,700 entities at globe scale',
);
shots.push(await shoot(page, 'mission-environmental'));
});
await section('mission-contacts', async () => {
await open(page, { query: "?welcome=1", errorSink: consoleErrors });
await pick(page, 'contacts');
state = await appState(page);
record('LIVE CONTACTS activates the Contacts context mode', state.contextMode === 'flights',
`contextMode=${state.contextMode}`);
const panelOpen = await page.evaluate(() => {
const panel = document.getElementById('global-context-panel');
return !!panel && !panel.classList.contains('collapsed');
});
record('LIVE CONTACTS reveals the Context panel', panelOpen);
record('LIVE CONTACTS still leaves the detection override untouched',
state.detectionOverridden === false, `_detectionUserOverridden=${state.detectionOverridden}`);
shots.push(await shoot(page, 'mission-contacts'));
});
await section('mission-space', async () => {
await open(page, { query: "?welcome=1", errorSink: consoleErrors });
await pick(page, 'space-missions');
state = await appState(page);
record('SPACE MISSIONS activates its context mode', state.contextMode === 'space-missions',
`contextMode=${state.contextMode}`);
shots.push(await shoot(page, 'mission-space-missions'));
});
await section('mission-explore', async () => {
await open(page, { query: "?welcome=1", errorSink: consoleErrors });
const beforeExplore = await appState(page);
await pick(page, 'explore');
state = await appState(page);
record('EXPLORE MANUALLY enables no layer and no context mode',
Object.values(state.layers).every((on) => !on) && state.contextMode === null,
Object.entries(state.layers).filter(([, on]) => on).map(([id]) => id).join(', ') || 'nothing enabled');
// The real claim is that Explore issues no camera command of its own: the
// arrival fly-in owns the camera and must still be heading down, not out.
record('EXPLORE MANUALLY never commandeers the camera',
(state.heightKm ?? 0) < 12000 && (state.heightKm ?? 1e9) <= (beforeExplore.heightKm ?? 1e9),
`${beforeExplore.heightKm} → ${state.heightKm} km (globe would be 18000)`);
shots.push(await shoot(page, 'mission-explore'));
});
// ── Viewports ───────────────────────────────────────────────────────────
await section('viewports', async () => {
// Portrait phone, LANDSCAPE phone (the short viewport that clipped the card
// at both ends before it grew a max-height), and a small desktop window.
const VIEWPORTS = [
{ name: 'mobile-375', width: 375, height: 812, dpr: 2, label: '375 x 812 portrait phone' },
{ name: 'landscape-667', width: 667, height: 390, dpr: 2, label: '667 x 390 landscape phone' },
{ name: 'desktop-800', width: 800, height: 600, dpr: 1, label: '800 x 600 small desktop' },
];
for (const vp of VIEWPORTS) {
await page.setViewport({ width: vp.width, height: vp.height, deviceScaleFactor: vp.dpr });
await open(page, { query: "?welcome=1", errorSink: consoleErrors });
const box = await page.evaluate((sel) => {
const node = document.querySelector(sel);
if (!node) return null;
const rect = node.getBoundingClientRect();
const list = node.querySelector('.first-run-choices');
const checkbox = node.querySelector('[data-first-run-suppress]');
const cb = checkbox?.getBoundingClientRect();
return {
top: Math.round(rect.top), bottom: Math.round(rect.bottom),
left: Math.round(rect.left), right: Math.round(rect.right),
vw: window.innerWidth, vh: window.innerHeight,
listScrolls: !!list && list.scrollHeight > list.clientHeight + 1,
// The checkbox and the status line live OUTSIDE the scrolling list, so
// they must be on screen at every height, scrollable list or not.
checkboxOnScreen: !!cb && cb.top >= 0 && cb.bottom <= window.innerHeight,
};
}, LAUNCHER);
const fits = !!box && box.top >= 0 && box.bottom <= box.vh
&& box.left >= 0 && box.right <= box.vw;
record(
`the launcher fits a ${vp.label} with no clipping`,
fits,
box ? `${box.left},${box.top} → ${box.right},${box.bottom} in ${box.vw}x${box.vh}${box.listScrolls ? ' (list scrolls)' : ''}` : 'absent',
);
record(`"Don't show this again" stays on screen at ${vp.width}x${vp.height}`,
!!box?.checkboxOnScreen);
// Discoverability: if the list scrolls, the LAST tile must still peek
// above the fold. A fold that lands exactly between tiles looks like a
// complete list of four, and the fifth mission is simply never found.
const peek = await page.evaluate((sel) => {
const node = document.querySelector(sel);
const list = node?.querySelector('.first-run-choices');
if (!list) return null;
list.scrollTop = 0;
const tiles = [...list.querySelectorAll('[data-first-run-choice]')];
const last = tiles[tiles.length - 1];
if (!last) return null;
const listBox = list.getBoundingClientRect();
const lastBox = last.getBoundingClientRect();
const visible = Math.max(0, Math.min(listBox.bottom, lastBox.bottom) - Math.max(listBox.top, lastBox.top));
return {
scrolls: list.scrollHeight > list.clientHeight + 1,
fraction: lastBox.height ? visible / lastBox.height : 0,
label: last.dataset.firstRunChoice,
};
}, LAUNCHER);
record(
`the last mission is discoverable at ${vp.width}x${vp.height}`,
!!peek && (!peek.scrolls || peek.fraction >= 0.25),
peek
? `${peek.label} ${(peek.fraction * 100).toFixed(0)}% visible${peek.scrolls ? ' (list scrolls)' : ' (no scroll needed)'}`
: 'absent',
);
// Every tile plus the checkbox must still be reachable by Tab when the
// list scrolls — and reaching one must bring it into view.
const reach = await page.evaluate(async (sel) => {
const node = document.querySelector(sel);
if (!node) return null;
const targets = [...node.querySelectorAll('[data-first-run-choice], [data-first-run-suppress]')];
const offscreen = [];
for (const target of targets) {
target.focus();
await new Promise((resolve) => requestAnimationFrame(resolve));
const rect = target.getBoundingClientRect();
const host = node.getBoundingClientRect();
if (document.activeElement !== target
|| rect.bottom <= host.top || rect.top >= host.bottom) {
offscreen.push(target.dataset.firstRunChoice || 'suppress-checkbox');
}
}
return { count: targets.length, offscreen };
}, LAUNCHER);
// Derived, not a magic number: every rendered tile plus the checkbox.
// A hardcoded count silently turns into a failure the day the menu
// changes, which is exactly what it did when a tile was removed.
const expectedFocusable = await page.$$eval(
'[data-first-run-choice]', (nodes) => nodes.length,
) + 1;
record(
`every tile and the checkbox are keyboard-reachable at ${vp.width}x${vp.height}`,
!!reach && reach.count === expectedFocusable && reach.offscreen.length === 0,
reach
? `${reach.count}/${expectedFocusable} focusable${reach.offscreen.length ? `, unreachable: ${reach.offscreen.join(', ')}` : ''}`
: 'absent',
);
// The reachability probe above tabs to the last tile and scrolls the list
// with it. Put it back to the top so the taste-pass shot shows what a
// visitor actually opens on.
await page.evaluate((sel) => {
const list = document.querySelector(sel)?.querySelector('.first-run-choices');
if (list) list.scrollTop = 0;
document.activeElement?.blur?.();
}, LAUNCHER);
await sleep(250);
shots.push(await shoot(page, `launcher-${vp.name}`));
}
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
});
await section('console', async () => {
record('no console errors across the whole run', consoleErrors.length === 0,
consoleErrors.slice(0, 3).join(' | ') || 'clean');
});
} finally {
await browser.close();
}
const failed = results.filter((entry) => !entry.ok);
const sections = [...new Set(results.map((entry) => entry.section))];
const tally = sections.map((name) => {
const rows = results.filter((entry) => entry.section === name);
return { name, total: rows.length, red: rows.filter((entry) => !entry.ok).length };
});
console.log(`\n Screenshots: ${shots.join(', ')}`);
console.log(`\n ${results.length - failed.length}/${results.length} checks passed\n`);
if (!TEETH) process.exit(failed.length === 0 ? 0 : 1);
/*
* NEGATIVE CONTROL VERDICT.
*
* Two things have to be true for the control to mean anything, and the old
* version checked neither properly: it exited ZERO after a single throw ended
* the run at 9 of 38 checks, so most assertions were never even reached.
*
* (a) the harness must REPORT FAILURE under the stub — a run with the
* launcher removed is a failing run, and it exits non-zero like one;
* (b) EVERY section must produce at least one red — a section that stays
* fully green without the launcher is not testing the launcher.
*
* Because (a) means the process always exits non-zero here, the control's own
* verdict is carried in the exit CODE: 1 = healthy, 2 = the control is broken.
*/
console.log(' TEETH — per-section reachability (each section must go red):');
for (const row of tally) {
const hygiene = HYGIENE_SECTIONS.includes(row.name);
const ok = row.red > 0 || hygiene;
const tag = hygiene ? '\x1b[2mHYGN\x1b[0m' : (row.red > 0 ? '\x1b[32mRED \x1b[0m' : '\x1b[31mNONE\x1b[0m');
void ok;
console.log(` [${tag}] ${row.name}: ${row.red}/${row.total} red`);
}
const toothless = tally.filter((row) => row.red === 0 && !HYGIENE_SECTIONS.includes(row.name));
const unreached = EXPECTED_SECTIONS.filter((name) => !sections.includes(name));
const healthy = failed.length > 0 && toothless.length === 0 && unreached.length === 0;
if (unreached.length) console.log(`\n UNREACHED sections: ${unreached.join(', ')}`);
if (toothless.length) console.log(` TOOTHLESS sections: ${toothless.map((row) => row.name).join(', ')}`);
console.log(healthy
? `\n TEETH HEALTHY: harness failed as required (${failed.length} red) and every one of ${tally.length} sections went red. Exit 1 by design.\n`
: '\n TEETH BROKEN: the control did not collapse the way it claims to. Exit 2.\n');
process.exit(healthy ? 1 : 2);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env node
/**
* scripts/qa-floor-hold.mjs — a grounded contact holds its floor through a
* terrain-proxy outage (field incident, 2026-08-21).
*
* Reproduces the incident end to end against the RENDERED mesh:
*
* phase A the terrain proxy is healthy. A GROUNDED contact reporting no
* altitude at all (the AXEL21 profile — a few knots, alt unknown)
* settles onto its resolved floor.
* phase B every `/api/terrain/heights` request answers 504 — the same
* "[terrain-heights-proxy] refresh incomplete" the server logged —
* and the contact taxis into cells that were never warmed.
*
* The contact must stay ON the mesh through phase B. Without the hold it falls
* to the geoid, which at an inland field is ~150 m of burial.
*
* node scripts/qa-floor-hold.mjs # real GPU (ANGLE/Metal)
* node scripts/qa-floor-hold.mjs --headful # watch it
* QA_BASE_URL=http://localhost:4257 node scripts/qa-floor-hold.mjs
*
* GPU BACKEND MATTERS. `scene.sampleHeight` answers on both backends, but
* software rasterization streams a coarser LOD. Measured directly, one probe
* point on the Austin apron read ~20.9 m under swiftshader against ~168.4 m
* under Metal — a backend comparison at one point, not to be confused with the
* same-backend run-to-run spread (122.1 m vs 20.6 m on a cell centre) that
* retired the hold chain's unvalidated tier. The
* default is the real GPU. Under `--angle=swiftshader` the rendered-mesh oracle
* is reported as unavailable rather than trusted, and the run falls back to the
* bare-earth DEM (fetched server-side, so the simulated outage cannot reach
* it) — a weaker but still decisive check, since the geoid sits ~150 m below it.
*/
import puppeteer from 'puppeteer';
import fs from 'node:fs';
import path from 'node:path';
const APP_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
const argv = Object.fromEntries(process.argv.slice(2)
.map((a) => a.replace(/^--/, '').split('=')).map(([k, v]) => [k, v ?? true]));
const ANGLE = String(argv.angle || 'metal');
const HEADFUL = !!argv.headful;
// Austin airport apron by default — the site qa-floor-verify pins, where the
// mesh sits ~150 m above the geoid and burial is unmistakable.
const SITE = {
lat: Number.isFinite(Number(argv.lat)) ? Number(argv.lat) : 30.1975,
lon: Number.isFinite(Number(argv.lon)) ? Number(argv.lon) : -97.6660,
};
const ICAO = 'aa0f01';
const OUT_DIR = 'qa-shots/floorhold';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
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}` : ''}`);
}
fs.mkdirSync(OUT_DIR, { recursive: true });
const chrome = (() => { try { return puppeteer.executablePath(); } catch { return undefined; } })();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
executablePath: chrome,
args: ['--use-gl=angle', `--use-angle=${ANGLE}`, '--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.evaluateOnNewDocument((site, icao) => {
window.__CONTACT = { icao, lat: site.lat, lon: site.lon };
window.__TERRAIN = { ok: 0, failed: 0 };
window.__TERRAIN_FAIL = false;
const realFetch = window.fetch.bind(window);
const json = (o) => new Response(JSON.stringify(o), {
status: 200, headers: { 'Content-Type': 'application/json' },
});
window.fetch = (input, init) => {
const raw = typeof input === 'string' || input instanceof URL ? String(input) : input?.url;
let url; try { url = new URL(raw, window.location.href); } catch { return realFetch(input, init); }
const app = url.origin === window.location.origin;
if (app && url.pathname === '/api/terrain/heights') {
if (window.__TERRAIN_FAIL) {
window.__TERRAIN.failed += 1;
return Promise.resolve(new Response('upstream timeout', { status: 504 }));
}
window.__TERRAIN.ok += 1;
return realFetch(input, init);
}
if (app && url.pathname === '/api/opensky') {
const c = window.__CONTACT;
const t = Math.floor(Date.now() / 1000);
// baro_altitude (7) and geo_altitude (13) both null, on_ground true.
return Promise.resolve(json({
time: t,
states: [[c.icao, 'AXEL21', 'Synthetica', t, t, c.lon, c.lat,
null, true, 1.5, 90, 0, null, null, null, false, 0]],
}));
}
if (app && url.pathname === '/api/opensky-track') return Promise.resolve(json({ path: [] }));
if (app && /^\/api\/adsbdb\//.test(url.pathname)) return Promise.resolve(json({}));
return realFetch(input, init);
};
}, SITE, ICAO);
console.log(`\nFloor hold through a terrain outage\n App : ${APP_URL}\n GPU : ANGLE/${ANGLE}\n Site : ${SITE.lat}, ${SITE.lon}\n`);
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 120000 });
await page.waitForFunction(() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
{ timeout: 150000 });
await sleep(12000); // let the boot fly-to settle before pinning
await page.evaluate((site) => { window.__SITE = site; }, SITE);
const pin = () => 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.__SITE.lon, window.__SITE.lat - 0.008, 900),
),
orientation: { heading: 0, pitch: -0.45, roll: 0 },
});
v.camera.moveEnd.raiseEvent();
});
await pin();
await page.evaluate(async () => { await window.__godsEyeView.dataManager.toggle('flights'); });
/** Reads the contact's rendered height and the rendered mesh beneath it. */
const measure = () => page.evaluate(async (icao) => {
const v = window.__godsEyeView.viewer;
const C = v.camera.positionCartographic.constructor;
const sprites = []; const models = []; let bb = null;
const walk = (coll) => {
for (let i = 0; i < coll.length; i++) {
let p; try { p = coll.get(i); } catch { continue; }
if (!p) continue;
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
if (p.image !== undefined && p.alignedAxis !== undefined) {
sprites.push(p); if (p.id === icao) bb = p;
} else if (p.boundingSphere && p.modelMatrix) models.push(p);
}
};
try { walk(v.scene.primitives); } catch { /* mid-teardown */ }
if (!bb?.position) return { error: 'contact billboard not found' };
const carto = C.fromCartesian(bb.position);
const lat = carto.latitude * 180 / Math.PI;
const lon = carto.longitude * 180 / Math.PI;
// `sampleHeight` runs an offscreen PICK pass, so it needs the scene to be
// rendering. This app parks its render loop when idle (the wave-2 governor),
// and a probe fired against a parked scene — or one whose tiles are still
// streaming after a camera move — reads `undefined`. Pump a frame through the
// app's own governor hook before each attempt, exactly as the fly_route
// harness does, and accept the coarse cell centre as a fallback point.
const pump = () => new Promise((resolve) => {
try { window.__godsEyeView.requestRender?.(); } catch { /* no governor */ }
try { v.scene.requestRender(); } catch { /* explicit-render off */ }
requestAnimationFrame(() => requestAnimationFrame(resolve));
});
let mesh = null;
for (let attempt = 0; attempt < 10 && mesh == null; attempt += 1) {
await pump();
for (const [pLat, pLon] of [[lat, lon], [+lat.toFixed(3), +lon.toFixed(3)]]) {
for (const exclude of [[...sprites, ...models], []]) {
try {
const h = v.scene.sampleHeight(C.fromDegrees(pLon, pLat, 0), exclude);
if (Number.isFinite(h)) { mesh = h; break; }
} catch { /* mid-stream */ }
}
if (mesh != null) break;
}
if (mesh == null) await new Promise((r) => setTimeout(r, 1200));
}
return {
spriteH: carto.height, meshH: mesh, shown: bb.show === true,
lat: +lat.toFixed(5), lon: +lon.toFixed(5), terrain: { ...window.__TERRAIN },
};
}, ICAO);
/** Bare-earth ground truth, fetched SERVER-SIDE so the page's simulated outage
* cannot touch it. The photoreal mesh sits ABOVE bare earth, so this is the
* weaker of the two oracles — but the geoid is ~150 m below it, so burial is
* still unmistakable. */
const truth = new Map();
async function demTruth(lat, lon) {
const key = `${lat.toFixed(4)},${lon.toFixed(4)}`;
if (truth.has(key)) return truth.get(key);
let h = null;
try {
const res = await fetch(`${APP_URL}/api/terrain/heights?points=${lon.toFixed(5)},${lat.toFixed(5)}`);
if (res.ok) {
const v = Number((await res.json())?.results?.[0]?.ellipsoid);
if (Number.isFinite(v)) h = v;
}
} catch { /* the oracle is best-effort too */ }
truth.set(key, h);
return h;
}
const samples = [];
async function sample(phase, note) {
const m = await measure();
if (m.error) { console.log(` ${phase} ${note}: ${m.error}`); return null; }
const dem = await demTruth(m.lat, m.lon);
const row = {
phase, note, ...m, demH: dem,
meshClearM: m.meshH != null ? +(m.spriteH - m.meshH).toFixed(2) : null,
demClearM: dem != null ? +(m.spriteH - dem).toFixed(2) : null,
};
samples.push(row);
console.log(` ${phase} ${note}: @${m.lat},${m.lon} sprite=${m.spriteH.toFixed(1)}`
+ ` mesh=${m.meshH?.toFixed?.(1) ?? 'n/a'} (${row.meshClearM})`
+ ` DEM=${dem?.toFixed?.(1) ?? 'n/a'} (${row.demClearM})`
+ ` terrain=${JSON.stringify(m.terrain)}`);
return row;
}
console.log('\nphase A — terrain healthy, warming cells (3 polls)');
for (let i = 0; i < 3; i++) { await sleep(32000); await pin(); }
await sample('A', 'warm');
fs.writeFileSync(path.join(OUT_DIR, `hold-A-warm-${ANGLE}.png`), await page.screenshot({ type: 'png' }));
console.log('\nphase B — every terrain request answers 504 from here');
await page.evaluate(() => { window.__TERRAIN_FAIL = true; });
for (let i = 1; i <= 4; i++) {
await page.evaluate(() => { window.__CONTACT.lon += 0.0016; }); // ~150 m east per poll
await sleep(32000);
await pin();
await sample('B', `poll+${i}`);
}
fs.writeFileSync(path.join(OUT_DIR, `hold-B-outage-${ANGLE}.png`), await page.screenshot({ type: 'png' }));
await browser.close();
console.log('');
const outage = samples.filter((s) => s.phase === 'B');
const meshRows = outage.filter((s) => s.meshClearM != null);
const demRows = outage.filter((s) => s.demClearM != null);
record('the terrain proxy really went down mid-run',
outage.length > 0 && outage[outage.length - 1].terrain.failed > 0,
`${outage[outage.length - 1]?.terrain?.failed ?? 0} requests answered 504`);
record('the contact stayed visible throughout', outage.every((s) => s.shown === true));
if (meshRows.length) {
const worst = Math.min(...meshRows.map((s) => s.meshClearM));
record('the contact never rendered below the RENDERED MESH', worst >= -0.5,
`worst clearance ${worst} m across ${meshRows.length} samples`);
} else {
record('rendered-mesh oracle available', false,
`sampleHeight never answered on ANGLE/${ANGLE} — falling back to the bare-earth DEM`);
}
if (demRows.length) {
const worst = Math.min(...demRows.map((s) => s.demClearM));
record('the contact never fell to the geoid (bare-earth DEM oracle)', worst >= -2,
`worst clearance ${worst} m above bare earth; the geoid here is ~150 m below it`);
}
fs.writeFileSync(path.join(OUT_DIR, `hold-${ANGLE}.json`), JSON.stringify(samples, null, 2));
console.log(`\n evidence: ${OUT_DIR}/hold-*-${ANGLE}.png, ${OUT_DIR}/hold-${ANGLE}.json`);
const failed = results.filter((r) => r.ok === false).length;
console.log(`\n RESULT: ${results.length - failed} passed, ${failed} failed\n`);
process.exit(failed ? 1 : 0);
+209
View File
@@ -0,0 +1,209 @@
// 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 ACTUAL visible anchor against the rendered
// mesh (sprite- and model-excluded scene.sampleHeight probes). Model-owned
// contacts deliberately keep their hidden billboard at the raw sensor datum,
// so getNearby().position is not a render-height oracle for those contacts.
// 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
// each candidate can verify against its own dev server instead of :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);
// getNearby() intentionally reports the raw hidden-billboard position for an
// untracked contact whose 3D model owns the visual. The detection surface is
// already welded to whichever primitive actually owns that visual: model
// centre, tracked visual, or billboard. Reuse that production render anchor
// here instead of treating the deliberately unfloored raw datum as buried.
const visualByIcao = new Map(layer.getDetectableObjects().map((object) => [
String(object.sourceId || '').trim().toLowerCase(),
object.position,
]));
// 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 raw = ell.cartesianToCartographic(p.position);
const visualPosition = visualByIcao.get(String(p.icao24 || '').trim().toLowerCase());
if (!visualPosition) {
out.push({
id: p.id,
icao24: p.icao24,
rawDatumAltM: +raw.height.toFixed(1),
renderAltM: null,
meshM: null,
aboveMeshM: null,
visualOffsetM: null,
missingVisualAnchor: true,
});
continue;
}
const visual = ell.cartesianToCartographic(visualPosition);
const latDeg = visual.latitude * 180 / Math.PI, lonDeg = visual.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,
icao24: p.icao24,
rawDatumAltM: +raw.height.toFixed(1),
renderAltM: +visual.height.toFixed(1),
meshM: meshH != null ? +meshH.toFixed(1) : null,
aboveMeshM: meshH != null ? +(visual.height - meshH).toFixed(1) : null,
visualOffsetM: +(visual.height - raw.height).toFixed(1),
missingVisualAnchor: false,
});
}
// 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,
missingVisualAnchors: out.filter((contact) => contact.missingVisualAnchor).length,
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);
const missingVisuals = (report.contacts || []).filter((c) => c.missingVisualAnchor);
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)`);
}
for (const missing of missingVisuals) {
console.log(` MISSING VISUAL ANCHOR ${missing.id} (${missing.icao24})`);
}
// A measured burial is always a failure. Otherwise, no plausible readings or
// any missing render anchor is inconclusive: the harness must never turn an
// unmeasured visible contact into a false pass.
const verdict = buried.length > 0
? 'FAIL'
: (lows.length === 0 || missingVisuals.length > 0 ? 'INCONCLUSIVE' : 'PASS');
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));
+321
View File
@@ -0,0 +1,321 @@
#!/usr/bin/env node
/**
* Are the floor-hold pins actually load-bearing?
*
* A pin that is red only when you delete the whole feature proves very little.
* This reverts each fix INDIVIDUALLY — the smallest edit that reintroduces the
* original defect — and requires the floor suites to go red for it. Every entry
* names the defect it restores, so the count is reproducible rather than
* asserted in a commit message.
*
* node scripts/qa-floorhold-mutations.mjs
*
* Restores every file on exit, including on failure.
*/
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const TESTS = [
'src/data/flights.test.mjs',
'src/data/groundFloor.test.mjs',
'src/data/meshFloorSampler.test.mjs',
'src/data/renderAltitude.test.mjs',
];
const FLIGHTS = 'src/data/flights.js';
const FLOOR = 'src/data/groundFloor.js';
const ALT = 'src/data/renderAltitude.js';
/** @type {Array<{defect: string, edits: Array<{file: string, from: string, to: string}>}>} */
const MUTATIONS = [
{
defect: 'the display hold is gone — a cold cell clamps nothing (the original bug)',
edits: [
{
file: FLIGHTS,
from: ' if (state.heldTier === \'own\' && !state.seeded) return state.heldM;',
to: ' if (true) return null;',
},
],
},
{
defect: 'the adjacent-cell tier is gone',
edits: [
{
file: FLIGHTS,
from: ' const near = neighborFloorM(cell);',
to: ' const near = null;',
},
],
},
{
defect: 'a borrowed floor is owned forever — no upgrade when a better one warms',
edits: [
{
file: FLIGHTS,
from: ' if (state.heldTier === \'own\' && !state.seeded) return state.heldM;',
to: ' if (state.heldTier && !state.seeded) return state.heldM;',
},
],
},
{
// The round-1 shape verbatim: memo on the RAW cell floor, checked BEFORE
// the hold chain runs. A parked contact whose cell never warms then
// returns its own unresolved answer forever.
defect: 'a stationary unresolved contact memo-returns before the chain can retry',
edits: [
{
file: FLIGHTS,
from: ' const floor = cachedGroundFloor(cell.lat, cell.lon);',
to: ' const floor = cachedGroundFloor(cell.lat, cell.lon);\n'
+ ' if (state && state.rawFloorM === floor\n'
+ ' && Cesium.Cartesian3.equals(pos, state.in)) return state.out || pos;',
},
{
file: FLIGHTS,
from: ' next.effectiveM = effective;',
to: ' next.effectiveM = effective;\n next.rawFloorM = floor;',
},
],
},
{
defect: 'the hold survives an airborne / model-owned interval',
edits: [
{
file: FLIGHTS,
from: ' _retireDisplayFloorState(icao24, nowMs);\n return pos;',
to: ' return pos;',
},
],
},
{
defect: 'the held floor is stretched without bound (no drift limit)',
edits: [
{ file: FLIGHTS, from: ' <= HELD_FLOOR_MAX_DRIFT_KM)) {', to: ' <= Infinity)) {' },
],
},
{
defect: 'a hold release SNAPS instead of easing',
edits: [
{
file: FLIGHTS,
from: ' if (next.easedM == null && wasHeld && Number.isFinite(stoodOnM)',
to: ' if (false && next.easedM == null && wasHeld && Number.isFinite(stoodOnM)',
},
],
},
{
defect: 'rises are eased too — time spent under the mesh',
edits: [
{
file: FLIGHTS,
from: ' if (!Number.isFinite(effective) || effective >= next.easedM) {',
to: ' if (!Number.isFinite(effective)) {',
},
],
},
{
defect: 'the ease escapes its release scope — every floor drop eased',
edits: [
{
file: FLIGHTS,
from: ' if (next.easedM == null && wasHeld && Number.isFinite(stoodOnM)',
to: ' if (next.easedM == null && Number.isFinite(stoodOnM)',
},
],
},
{
// The owner-playtest regression: a roof neighbour taken as ground.
defect: 'the neighbour lean takes the HIGHEST cell and floats a contact onto a roof',
edits: [
{
file: FLOOR,
from: ' if (lowest == null || h < lowest) lowest = h;',
to: ' if (lowest == null || h > lowest) lowest = h;',
},
],
},
{
defect: 'a LONE neighbour is borrowed, with nothing to check it against',
edits: [
{
file: FLOOR,
from: ' return resolved >= NEIGHBOR_FLOOR_MIN_SAMPLES ? lowest : null;',
to: ' return resolved >= 1 ? lowest : null;',
},
],
},
{
defect: 'the geoid guess outranks a held render height again (the poll-step bug)',
edits: [
{ file: ALT, from: ' if (Number.isFinite(priorRenderM)) return null;', to: ' // guard removed' },
],
},
{
defect: 'a re-probe onto a LOWER neighbour re-latches with a snap',
edits: [
{
file: FLIGHTS,
from: ' if (next.easedM == null && wasHeld && Number.isFinite(stoodOnM)\n && Number.isFinite(effective) && effective < stoodOnM) {',
to: ' if (next.easedM == null && Number.isFinite(floor) && wasHeld && Number.isFinite(stoodOnM)\n && Number.isFinite(effective) && effective < stoodOnM) {',
},
],
},
{
// The from/duration shape this replaced: a FIXED anchor interpolated by
// CUMULATIVE elapsed time. Re-evaluated against a target that moved
// mid-approach it jumps by the eased fraction of the change.
defect: 'the approach interpolates a fixed anchor, so a mid-approach retarget is a seam',
edits: [
{ file: FLIGHTS, from: ' next.easeMs = nowMs;\n const closed', to: ' const closed' },
{ file: FLIGHTS, from: ' next.easedM = value;', to: ' /* anchor stays fixed */' },
],
},
{
defect: 'a global per-tick probe budget is reintroduced over the per-contact throttle',
edits: [
{
file: FLIGHTS,
from: ' state.probeMs = nowMs;\n const near = neighborFloorM(cell);',
to: ' globalThis.__mutGlobalProbeBudget = (globalThis.__mutGlobalProbeBudget || 0) + 1;\n'
+ ' if (globalThis.__mutGlobalProbeBudget > 12) return state.heldM;\n'
+ ' state.probeMs = nowMs;\n const near = neighborFloorM(cell);',
},
],
},
{
defect: 'the per-tick step is unclamped, so a delayed tick snaps',
edits: [
{
file: FLIGHTS,
from: ' const closed = Math.min(FLOOR_EASE_MAX_STEP, 1 - Math.exp(-dtMs / FLOOR_EASE_TAU_MS));',
to: ' const closed = 1 - Math.exp(-dtMs / FLOOR_EASE_TAU_MS);',
},
],
},
{
// The first cut: delete outright. An on_ground flap through a takeoff roll
// then cold-starts the contact under the runway (field observation VIR138M).
defect: 'retiring the hold DELETES it, so an on_ground flap cold-starts',
edits: [
{
file: FLIGHTS,
from: ' if (state.retiredMs == null) {',
to: ' _displayFloorState.delete(icao24);\n if (false) {',
},
],
},
{
defect: 'the rehydration seed never expires — an hour-old floor still answers',
edits: [
{
file: FLIGHTS,
from: ' return state.retiredMs != null && nowMs - state.retiredMs > FLOOR_SEED_GRACE_MS;',
to: ' return false;',
},
],
},
{
// Expiry checked ONLY on the retire path: a contact that is parked and then
// makes no calls at all never reaches it, and rehydration cleared
// `retiredMs` before anything looked at it. Measured: parked 198 s, reused.
defect: 'seed age is never validated at rehydration, only while still retired',
edits: [
{
file: FLIGHTS,
from: ' if (_seedExpired(next, nowMs)) _dropHeldFloor(next);',
to: ' // rehydration takes whatever was parked, however old',
},
],
},
{
// The measured float: a 200 m seed reused 0.56 km away while the new
// neighbourhood read 100 m and 105 m.
defect: 'a rehydrated seed outranks fresh neighbour evidence',
edits: [
{
file: FLIGHTS,
from: ' if (state.heldTier === \'own\' && !state.seeded) return state.heldM;',
to: ' if (state.heldTier === \'own\') return state.heldM;',
},
],
},
{
defect: 'a parked floor is not marked as a memory, so nothing can demote it',
edits: [
{
file: FLIGHTS,
from: ' state.seeded = true; // what it answers with next is a memory, not a reading',
to: ' state.seeded = false;',
},
],
},
{
defect: 'the seed flag survives a live reading, so a measured floor stays demoted',
edits: [
{ file: FLIGHTS, from: ' state.seeded = false;\n return floorM;', to: ' return floorM;' },
],
},
];
const read = (f) => fs.readFileSync(path.join(ROOT, f), 'utf8');
const write = (f, s) => fs.writeFileSync(path.join(ROOT, f), s);
function runTests() {
try {
execFileSync('node', ['--test', ...TESTS], { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
return { failed: 0, names: [] };
} catch (e) {
const raw = `${e.stdout || ''}${e.stderr || ''}`;
const names = [...new Set([...raw.matchAll(/^✖ (.+?) \(/gm)].map((m) => m[1]))];
const m = raw.match(/^ℹ fail (\d+)/m);
return { failed: m ? Number(m[1]) : -1, names, raw };
}
}
const baseline = runTests();
console.log(`baseline: ${baseline.failed} failing`);
if (baseline.failed !== 0) {
console.error('the suites must be green before mutating');
if (baseline.raw) console.error(baseline.raw.slice(-2000));
process.exit(1);
}
let allRed = true;
for (const mut of MUTATIONS) {
const backups = new Map();
let applied = true;
for (const e of mut.edits) {
if (!backups.has(e.file)) backups.set(e.file, read(e.file));
const cur = read(e.file);
if (!cur.includes(e.from)) {
console.log(` [STALE] ${mut.defect} — anchor not found in ${e.file}`);
applied = false;
break;
}
write(e.file, cur.replace(e.from, e.to));
}
if (applied) {
const r = runTests();
if (r.failed > 0) {
console.log(` [RED] ${mut.defect} — ${r.failed} failing`);
for (const n of r.names) console.log(` ↳ ${n}`);
} else {
console.log(` [GREEN] ${mut.defect} — NOT PINNED`);
allRed = false;
}
} else {
allRed = false;
}
for (const [file, text] of backups) write(file, text);
}
const after = runTests();
console.log(`\nrestored: ${after.failed} failing (must be 0)`);
console.log(allRed && after.failed === 0
? `RESULT: ${MUTATIONS.length}/${MUTATIONS.length} defects pinned`
: 'RESULT: FAIL');
process.exit(allRed && after.failed === 0 ? 0 : 1);
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env node
/**
* scripts/qa-floorhold-probe-cost.mjs — what does an unrationed adjacent-cell
* probe actually cost?
*
* The display-floor hold borrows a floor from the eight cells adjacent to a
* contact's own when that one is cold (`neighborFloorM`). An early draft
* rationed those probes with a global per-tick budget and a fairness queue,
* which produced two starvation defects in a row. This measures the thing the
* scheduler was protecting, so the decision to delete it is a number rather
* than an opinion.
*
* A probe is eight synchronous `Map` reads against the shared floor cache — no
* fetch, no `sampleHeight`, nothing async. Every DEM request is driven by
* `warmGroundFloor` from the poll loop instead, already bounded there by
* DISPLAY_CORRIDOR_CELL_BUDGET and the resolver's single-flight chain.
*
* node scripts/qa-floorhold-probe-cost.mjs
*/
import {
neighborFloorM, reportMeshFloorCell, setMeshFloorPreferred, _clearMeshFloorCellsForTest,
} from '../src/data/groundFloor.js';
const CONTACTS = 200; // a dense airport view, every contact probing at once
const TICK_MS = 80; // the fleet dead-reckoning cadence
const THROTTLE_MS = 500; // NEIGHBOR_FLOOR_PROBE_MS
setMeshFloorPreferred(true);
for (const [label, warm] of [
['all cold (every neighbour a miss — the outage case)', false],
['all warm (all eight neighbours resolved)', true],
]) {
_clearMeshFloorCellsForTest();
const cells = [];
for (let i = 0; i < CONTACTS; i += 1) {
const cell = { lat: +(30 + i * 0.003).toFixed(3), lon: -97.66 };
cells.push(cell);
if (warm) {
for (let dLat = -1; dLat <= 1; dLat += 1) {
for (let dLon = -1; dLon <= 1; dLon += 1) {
if (dLat === 0 && dLon === 0) continue; // the contact's OWN cell stays cold
reportMeshFloorCell(cell.lat + dLat * 0.001, cell.lon + dLon * 0.001, 100 + i);
}
}
}
}
for (let w = 0; w < 200; w += 1) for (const c of cells) neighborFloorM(c); // settle the JIT
const runs = [];
for (let r = 0; r < 60; r += 1) {
const t0 = process.hrtime.bigint();
for (const c of cells) neighborFloorM(c);
runs.push(Number(process.hrtime.bigint() - t0) / 1e6);
}
runs.sort((a, b) => a - b);
const median = runs[Math.floor(runs.length / 2)];
const worst = runs[runs.length - 1];
console.log(`\n${label}`);
console.log(` ${CONTACTS} contacts x 8 cells on ONE tick : median ${median.toFixed(3)} ms, worst ${worst.toFixed(3)} ms`);
console.log(` share of one ${TICK_MS} ms fleet tick : ${(worst / TICK_MS * 100).toFixed(2)}%`);
console.log(` sustained under the ${THROTTLE_MS} ms throttle : ${(median * 1000 / THROTTLE_MS).toFixed(2)} ms per second`);
}
console.log('\nA global scheduler over this protects single-digit milliseconds.\n');
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env node
/**
* scripts/qa-floorhold-staircase.mjs — what a grounded contact DOES while its
* floor data arrives, tick by tick.
*
* The unit pins assert the clamp's answer for a given cache state. They cannot
* see the shape of the transition, which is what an owner actually watches: a
* contact that reaches the right height by way of a jump into midair and a
* visible stair-step down is wrong even though every individual answer is
* defensible. An field test found exactly that — planes floating at
* terminal gates — and this is the rig that reproduces it.
*
* A stationary grounded contact at a cold cell, driven at the 80 ms fleet
* cadence, with cells warming on a fixed schedule. Reports time-to-surface,
* time visibly buried, time visibly FLOATING, and the visible step count.
*
* node scripts/qa-floorhold-staircase.mjs
*
* Pure module-level: no browser, no network, no GPU.
*/
import * as Cesium from 'cesium';
import { pickRenderAltitudeM } from '../src/data/renderAltitude.js';
import {
_floorGroundedDisplayPositionForTest, _clearDisplayFloorStateForTest,
} from '../src/data/flights.js';
import {
reportMeshFloorCell, setMeshFloorPreferred, _clearMeshFloorCellsForTest, GROUND_FLOOR_LIFT_M,
} from '../src/data/groundFloor.js';
const LAT = 30.2004, LON = -97.6604; // own cell 30.200 / -97.660
const APRON = -22.0, ROOF = -4.0, START = -32.0; // ellipsoidal; SFO-ish geoid start
const TARGET = APRON + GROUND_FLOOR_LIFT_M; // -20.5
const N = { up: [30.2014, LON], down: [30.1994, LON], left: [LAT, -97.6614], right: [LAT, -97.6594] };
const SCENARIOS = [
['S1 flat apron cold start (own cell warms last)', [
[1120, () => { reportMeshFloorCell(...N.up, APRON); reportMeshFloorCell(...N.down, APRON); }],
[5200, () => reportMeshFloorCell(LAT, LON, APRON)],
]],
['S2 apron beside a terminal (one neighbour is a roof)', [
[1120, () => { reportMeshFloorCell(...N.up, ROOF); reportMeshFloorCell(...N.down, APRON); }],
[5200, () => reportMeshFloorCell(LAT, LON, APRON)],
]],
['S3 own cell never warms (roof neighbour only)', [
[1120, () => reportMeshFloorCell(...N.up, ROOF)],
]],
['S3b own cell never warms (roof + apron neighbours)', [
[1120, () => { reportMeshFloorCell(...N.up, ROOF); reportMeshFloorCell(...N.down, APRON);
reportMeshFloorCell(...N.left, APRON); }],
]],
];
const summary = [];
for (const [name, schedule] of SCENARIOS) {
_clearDisplayFloorStateForTest(); _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true);
const pos = Cesium.Cartesian3.fromDegrees(LON, LAT, START);
const samples = [];
let pending = [...schedule];
for (let t = 0; t <= 12000; t += 80) {
while (pending.length && pending[0][0] <= t) pending.shift()[1]();
const out = _floorGroundedDisplayPositionForTest({ onGround: true }, pos, false, name, 1000 + t);
samples.push([t, Cesium.Cartographic.fromCartesian(out, Cesium.Ellipsoid.WGS84).height]);
}
const settle = samples.find(([, h]) => Math.abs(h - TARGET) < 0.1);
const buried = samples.filter(([, h]) => h < APRON - 0.05).length * 80;
const floatMax = Math.max(0, ...samples.map(([, h]) => h - TARGET));
const floatMs = samples.filter(([, h]) => h > TARGET + 0.5).length * 80;
const steps = [];
for (let i = 1; i < samples.length; i += 1) {
const d = samples[i][1] - samples[i - 1][1];
if (Math.abs(d) > 0.5) steps.push([samples[i][0], d]);
}
const plateaus = new Set(samples.map(([, h]) => h.toFixed(1))).size;
console.log(`\n### ${name}`);
console.log(` target = ${TARGET.toFixed(2)} m start = ${START.toFixed(2)} m`);
console.log(` time-to-surface = ${settle ? `${settle[0]} ms` : 'NEVER'}`);
console.log(` time visibly BURIED = ${buried} ms`);
console.log(` time visibly FLOATING = ${floatMs} ms (worst +${floatMax.toFixed(1)} m)`);
console.log(` visible steps (>0.5m) = ${steps.length} (up ${steps.filter(([, d]) => d > 0).length} / down ${steps.filter(([, d]) => d < 0).length})`);
console.log(` distinct plateaus = ${plateaus}`);
summary.push(` ${name} :: settle=${settle ? settle[0] : 'NEVER'} steps=${steps.length} float=${floatMs}ms(+${floatMax.toFixed(1)}m) buried=${buried}ms`);
}
console.log(`\nSUMMARY (this tree, post-fix)\n${summary.join('\n')}\n`);
// ---------------------------------------------------------------------------
// F1 — takeoff roll with the on_ground flag FLAPPING (field observation: VIR138M
// at JFK, 45 kt, "clearly on good ground, then suddenly popped below the
// ground, then popped back up").
//
// Two mechanisms meet here. OpenSky's on_ground flag is not clean through a
// rotation, and the fix's own height source switches at the same moment: a
// grounded fix is the resolved surface, an airborne one with baro just
// appearing is baro + geoid N, which at a sea-level field IS the geoid — below
// the ground the contact is still rolling on. Whether that dip is VISIBLE is
// then entirely down to whether the display clamp still remembers a floor.
//
// EVERY tick below is counted, the airborne one included. That tick renders
// under the runway and always will: the display clamp passes airborne positions
// through by design — an airborne height is the fix-time clamp's job, and
// flooring one would put a rotating aircraft back on the ground. So this rig
// reports two numbers, not one. The grounded count is what the hold owns and it
// must be zero. The airborne count is the ACCEPTED transition residual: the
// flap's own cost at the source, one tick wide, bounded by the field's geoid
// separation (~4 m here). What the hold changed is what happens AFTER the flap
// — 12 of 22 grounded ticks buried and not recovering, versus none.
// ---------------------------------------------------------------------------
const JFK_GROUND = -28.5; // ~4 m MSL field, geoid N ~ -32.5 m
const JFK_GEOID = -32.5;
const JFK_LON = -73.78;
console.log('\n### F1 takeoff roll, on_ground flapping for one poll');
console.log(' poll-path height source at the flap:');
console.log(` grounded, own cell warm = ${pickRenderAltitudeM({ geoAltM: null, baroAltM: null, onGround: true, surfaceM: JFK_GROUND, geoidN: JFK_GEOID })} m`);
console.log(` airborne, baro appears at 0 ft = ${pickRenderAltitudeM({ geoAltM: null, baroAltM: 0, onGround: false, surfaceM: null, geoidN: JFK_GEOID })} m`);
console.log(` the source switch alone drops the fix ${(JFK_GROUND - JFK_GEOID).toFixed(1)} m, to below the runway`);
_clearDisplayFloorStateForTest(); _clearMeshFloorCellsForTest(); setMeshFloorPreferred(true);
// Only the cell it STARTED on is warm: at 23 m/s it outruns its own floor data,
// which is the whole reason the hold exists.
reportMeshFloorCell(40.64, JFK_LON, JFK_GROUND);
let rollLat = 40.64;
const rollRows = [];
for (let i = 0; i <= 22; i += 1) {
const onGround = i !== 10; // ONE airborne poll mid-roll
const pos = Cesium.Cartesian3.fromDegrees(JFK_LON, rollLat, JFK_GEOID);
const out = _floorGroundedDisplayPositionForTest({ onGround }, pos, false, 'VIR138M', 1000 + i * 80);
rollRows.push([i, onGround, Cesium.Cartographic.fromCartesian(out, Cesium.Ellipsoid.WGS84).height]);
rollLat += 0.00021; // ~23 m per 80 ms tick
}
const below = ([, , h]) => h < JFK_GROUND - 0.05;
for (const row of rollRows) {
const [i, og, h] = row;
const mark = below(row)
? (og ? ' <-- BELOW THE RUNWAY' : ' <-- below the runway (airborne pass-through)')
: '';
console.log(` tick ${String(i).padStart(2)} onGround=${og ? 'T' : 'F'} render=${h.toFixed(2)}${mark}`);
}
const groundedRows = rollRows.filter(([, og]) => og);
const airborneRows = rollRows.filter(([, og]) => !og);
const dipped = rollRows.filter(below);
console.log(`\n ticks rendering BELOW the runway: ${dipped.length} / ${rollRows.length} (every tick counted)`);
console.log(` grounded : ${groundedRows.filter(below).length} / ${groundedRows.length} <- what the hold owns; must be 0`);
console.log(` airborne : ${airborneRows.filter(below).length} / ${airborneRows.length} <- pass-through by design; accepted transition residual`);
const worst = Math.min(JFK_GROUND, ...dipped.map(([, , h]) => h));
console.log(` worst burial = ${(worst - JFK_GROUND).toFixed(2)} m (the field's geoid separation, one tick wide)`);
+605
View File
@@ -0,0 +1,605 @@
#!/usr/bin/env node
/**
* fly_route cinematic evidence — drives the REAL voice runner headlessly and
* measures the REAL camera (Cesium heading/pitch/roll + position) every
* rendered frame, so the proof is the shot the owner will watch, not our own
* internal numbers.
*
* node scripts/qa-flyroute-cinema.mjs --url http://localhost:4247
*
* Writes a screenshot sequence plus a JSON trace to qa-shots/flyroute/.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import puppeteer from 'puppeteer';
import sharp from 'sharp';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const getOpt = (flag, fallback) => {
const i = process.argv.indexOf(flag);
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
};
const APP_URL = getOpt('--url', 'http://localhost:4247');
const OUT_DIR = getOpt('--out', path.join(ROOT, 'qa-shots', 'flyroute'));
const MIRROR_DIR = getOpt('--mirror', '');
const SHOT_EVERY_MS = Number(getOpt('--shot-ms', '2000'));
const CHROME_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
].filter(Boolean);
const CHROME_EXECUTABLE = CHROME_CANDIDATES.find((c) => { try { return fs.existsSync(c); } catch { return false; } });
// A 6-waypoint downtown Austin route: north, right, left, right, left.
const ROUTE_POINTS = [
{ latitude: 30.2620, longitude: -97.7431 },
{ latitude: 30.2650, longitude: -97.7431 },
{ latitude: 30.2650, longitude: -97.7397 },
{ latitude: 30.2680, longitude: -97.7397 },
{ latitude: 30.2680, longitude: -97.7363 },
{ latitude: 30.2712, longitude: -97.7363 },
];
const results = [];
const report = (ok, name, detail = '') => {
results.push({ ok, name, detail });
const mark = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` ${mark} ${name}${detail ? ` — ${detail}` : ''}`);
};
const note = (name, detail) => {
results.push({ ok: null, name, detail });
console.log(` \x1b[33mNOTE\x1b[0m ${name}${detail ? ` — ${detail}` : ''}`);
};
const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
const wrapDeg = (deg) => ((deg + 540) % 360) - 180;
fs.mkdirSync(OUT_DIR, { recursive: true });
if (MIRROR_DIR) fs.mkdirSync(MIRROR_DIR, { recursive: true });
const browser = await puppeteer.launch({
headless: 'new',
...(CHROME_EXECUTABLE ? { executablePath: CHROME_EXECUTABLE } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
// Real GPU when the host has one: the dolly is frame-rate independent, but
// a higher sample rate makes the roll and the ease ramps far easier to see.
...(process.platform === 'darwin'
? ['--use-angle=metal', '--enable-gpu']
: ['--use-gl=angle', '--use-angle=swiftshader']),
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1500,950',
],
protocolTimeout: 240000,
});
const page = await browser.newPage();
await page.setViewport({ width: 1500, height: 950 });
page.on('pageerror', (e) => console.log(` [page error] ${String(e).slice(0, 160)}`));
// Cold-corridor proof: hold the terrain proxy back so the dolly has to survive
// a corridor that has no floor data when the flight is asked for. Without this
// the harness only ever measures a machine whose cache happens to be warm.
const TERRAIN_DELAY_MS = Number(getOpt('--terrain-delay-ms', '0'));
let terrainRequests = 0;
if (TERRAIN_DELAY_MS > 0) {
await page.setRequestInterception(true);
page.on('request', (request) => {
if (request.url().includes('/api/terrain/heights')) {
terrainRequests += 1;
setTimeout(() => { request.continue().catch(() => {}); }, TERRAIN_DELAY_MS);
return;
}
request.continue().catch(() => {});
});
}
/** Install a postRender sampler: one row per RENDERED frame. */
async function installSampler() {
await page.evaluate(() => {
const viewer = window.__godsEyeView.viewer;
window.__gevFlyTrace = { rows: [], marks: [] };
if (window.__gevFlyTraceRemove) window.__gevFlyTraceRemove();
let frame = 0;
const listener = () => {
const cam = viewer.camera;
const carto = cam.positionCartographic;
// Every 6th frame, ask the RENDERED WORLD what is under the camera. This
// is the only measurement that can prove "never below terrain": it reads
// the surface the user is actually looking at, not our own floor cache.
let surfaceM = null;
frame += 1;
if (frame % 6 === 0 && typeof viewer.scene.sampleHeight === 'function') {
try {
const probe = viewer.scene.sampleHeight(carto.clone());
if (Number.isFinite(probe)) surfaceM = probe;
} catch { /* tiles not loaded under the camera */ }
}
window.__gevFlyTrace.rows.push({
t: performance.now(),
lon: (carto.longitude * 180) / Math.PI,
lat: (carto.latitude * 180) / Math.PI,
height: carto.height,
headingDeg: (cam.heading * 180) / Math.PI,
pitchDeg: (cam.pitch * 180) / Math.PI,
rollDeg: (cam.roll * 180) / Math.PI,
surfaceM,
});
};
viewer.scene.postRender.addEventListener(listener);
window.__gevFlyTraceRemove = () => viewer.scene.postRender.removeEventListener(listener);
});
}
async function readTrace() {
return page.evaluate(() => ({
rows: window.__gevFlyTrace.rows.slice(),
marks: window.__gevFlyTrace.marks.slice(),
}));
}
/** Metres between two samples, on the ground plane plus height. */
function sampleDistanceM(a, b) {
const mPerDegLat = 111320;
const mPerDegLon = 111320 * Math.cos((a.lat * Math.PI) / 180);
const dx = (b.lon - a.lon) * mPerDegLon;
const dy = (b.lat - a.lat) * mPerDegLat;
return Math.hypot(dx, dy);
}
try {
console.log(`\nfly_route cinematic evidence — ${APP_URL}`);
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 90000 });
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__gevVoiceCommands?.runner && window.__gevAnnotations,
{ timeout: 150000, polling: 250 },
);
const run = (name, args = {}) => page.evaluate(
(n, a) => window.__gevVoiceCommands.runner(n, a), name, args,
);
// ── Setup: get over downtown Austin, draw the route ───────────────────
await run('fly_to_location', { query: 'Texas State Capitol, Austin' });
await sleep(9000);
await run('clear_annotations', {});
await sleep(500);
const drawn = await run('annotate_map', {
annotations: [{
type: 'route', mode: 'driving', label: 'cinema evidence route', points: ROUTE_POINTS,
}],
});
await sleep(6000);
const routeInfo = await page.evaluate(() => {
const route = (window.__gevAnnotations.list?.() || []).filter((a) => a.type === 'route').at(-1);
return route ? { label: route.label, waypoints: route.path?.length ?? 0 } : null;
});
report(Boolean(routeInfo?.waypoints >= 2), 'route drawn on the board',
`waypoints=${routeInfo?.waypoints} drawn=${drawn?.drawn}`);
if (!routeInfo) throw new Error('no route on the board — cannot fly it');
// ── Run 1: the full cinematic flight ──────────────────────────────────
await installSampler();
const flight = await run('fly_route', { label: 'cinema evidence', speed: 'normal' });
await page.evaluate(() => window.__gevFlyTrace.marks.push({ label: 'flight-start', t: performance.now() }));
report(flight?.ok === true, 'fly_route accepted',
`distanceM=${flight?.distanceM} durationS=${flight?.durationS} waypoints=${flight?.waypoints}`);
if (!flight?.ok) throw new Error(`fly_route refused: ${flight?.error}`);
const coldPath = await page.evaluate(async () => {
try {
const mod = await import('/src/cameraVerbs.js');
return mod.getActiveCameraMotion?.() ?? null;
} catch { return 'module-unavailable'; }
});
if (coldPath && coldPath !== 'module-unavailable') {
report(true, 'cold-path telemetry read from the live flight',
`arming=${coldPath.arming} floorKnown=${coldPath.floorKnown} viaMeshProbe=${coldPath.floorFromMeshProbe}`
+ (TERRAIN_DELAY_MS ? ` (terrain proxy held back ${TERRAIN_DELAY_MS} ms, ${terrainRequests} request(s))` : ''));
}
// Liveness is read off the CAMERA, never off a module import: under Vite the
// dev server hands a dynamic import its own module instance after any HMR
// update, whose motion slot is empty no matter what the app is doing.
const stillForMs = () => page.evaluate(() => {
const rows = window.__gevFlyTrace.rows;
if (rows.length < 3) return 0;
const last = rows.at(-1);
for (let i = rows.length - 2; i >= 0; i -= 1) {
const moved = Math.abs(rows[i].lon - last.lon) + Math.abs(rows[i].lat - last.lat)
+ (Math.abs(rows[i].height - last.height) / 1e5);
if (moved > 1e-7) return last.t - rows[i].t;
}
return last.t - rows[0].t;
});
const budgetMs = Math.min(180000, ((flight.durationS || 40) + 10) * 1000);
const shots = [];
const startedAt = Date.now();
let shotIndex = 0;
while (Date.now() - startedAt < budgetMs) {
const file = path.join(OUT_DIR, `flight-${String(shotIndex).padStart(2, '0')}.png`);
const at = await page.evaluate(() => performance.now());
await page.screenshot({ path: file });
shots.push({ file, at });
shotIndex += 1;
await sleep(SHOT_EVERY_MS);
if (Date.now() - startedAt > 5000 && (await stillForMs()) > 1500) break;
}
const trace = await readTrace();
await page.evaluate(() => window.__gevFlyTraceRemove?.());
report(shots.length >= 6, 'screenshot sequence captured', `${shots.length} frames @ ${SHOT_EVERY_MS}ms`);
// ── Measure the REAL camera ───────────────────────────────────────────
// Only the dolly's own frames count. The sampler is installed before the
// flight, so the pre-flight view and the single frame that jumps the camera
// onto the route start are dropped — neither is dolly motion.
const flightStartT = trace.marks.find((m) => m.label === 'flight-start')?.t ?? 0;
const rows = trace.rows.filter((r) => r.t > flightStartT).slice(1);
const fps = rows.length / Math.max(0.001, (rows.at(-1).t - rows[0].t) / 1000);
report(rows.length > 60, 'camera sampled every rendered frame',
`${rows.length} dolly samples (${trace.rows.length} total) at ${fps.toFixed(1)} fps`);
const rolls = rows.map((r) => wrapDeg(r.rollDeg));
const peakRoll = Math.max(...rolls.map(Math.abs));
report(peakRoll <= 10.5, 'bank never exceeds the 10° cap', `peak |roll| = ${peakRoll.toFixed(2)}°`);
report(peakRoll > 3, 'turns actually bank', `peak |roll| = ${peakRoll.toFixed(2)}°`);
report(Math.abs(rolls[0]) < 1 && Math.abs(rolls.at(-1)) < 3,
'the flight starts and finishes near wings level',
`first=${rolls[0].toFixed(2)}° last=${rolls.at(-1).toFixed(2)}°`);
// Roll follows the turn: while banked, roll sign must match heading rate.
// Sampled over ~1 s of heading change, and only where BOTH the roll and the
// turn are unambiguous (a roll-out trails its turn by design, so the tail of
// every corner is deliberately outside the window).
let agree = 0;
let disagree = 0;
const span = Math.max(2, Math.round(fps));
for (let i = span; i < rows.length; i += 1) {
const dt = (rows[i].t - rows[i - span].t) / 1000;
if (!(dt > 0)) continue;
const headingRate = wrapDeg(rows[i].headingDeg - rows[i - span].headingDeg) / dt;
const roll = wrapDeg(rows[i].rollDeg);
if (Math.abs(roll) < 2 || Math.abs(headingRate) < 3) continue;
if (Math.sign(roll) === Math.sign(headingRate)) agree += 1; else disagree += 1;
}
const agreement = agree / Math.max(1, agree + disagree);
report(agreement > 0.85, 'the camera rolls INTO the turn (right turn → right bank)',
`${(agreement * 100).toFixed(1)}% of banked samples agree (${agree}/${agree + disagree})`);
// Every rate below is measured over ~250 ms windows. A per-frame difference
// is dominated by the pairing jitter between the motion tick's own clock and
// postRender (a 36 ms frame next to an 8 ms one doubles any per-frame rate),
// which measures the harness, not the dolly.
const RATE_WINDOW_MS = 250;
const peakRate = (valueAt) => {
let worst = 0;
for (let i = 0, j = 0; i < rows.length; i += 1) {
while (j < rows.length - 1 && rows[j].t - rows[i].t < RATE_WINDOW_MS) j += 1;
const dt = (rows[j].t - rows[i].t) / 1000;
if (dt >= RATE_WINDOW_MS / 2000) worst = Math.max(worst, Math.abs(valueAt(j) - valueAt(i)) / dt);
}
return worst;
};
const peakRollRate = peakRate((i) => rolls[i]);
report(peakRollRate < 20, 'the roll enters and exits smoothly, never snaps',
`peak roll rate ${peakRollRate.toFixed(1)} °/s over ${RATE_WINDOW_MS} ms`);
// Speed: eased at both ends, no step in between. Measured over 400 ms
// windows — the per-frame delta is dominated by the pairing jitter between
// the motion tick's own clock and postRender, not by the dolly.
const cumulative = [0];
for (let i = 1; i < rows.length; i += 1) {
cumulative.push(cumulative[i - 1] + sampleDistanceM(rows[i - 1], rows[i]));
}
const speeds = [];
for (let i = 0, j = 0; i < rows.length; i += 1) {
while (j < rows.length - 1 && rows[j].t - rows[i].t < 400) j += 1;
const dt = (rows[j].t - rows[i].t) / 1000;
if (dt >= 0.3) speeds.push({ t: rows[i].t - rows[0].t, v: (cumulative[j] - cumulative[i]) / dt });
}
const window1s = (from, to) => {
const inWindow = speeds.filter((s) => s.t >= from && s.t <= to);
return inWindow.length ? inWindow.reduce((sum, s) => sum + s.v, 0) / inWindow.length : Number.NaN;
};
const peakV = Math.max(...speeds.map((s) => s.v));
const firstSecond = window1s(0, 1200);
report(firstSecond < peakV * 0.5, 'the dolly eases IN (no velocity step at the start)',
`first 1.2 s ${firstSecond.toFixed(1)} m/s vs peak ${peakV.toFixed(1)} m/s`);
// Ease-out is measured as the SHAPE of the decay, not as a terminal window.
// The sampler keeps running after the dolly stops, so a trailing average
// includes stationary frames — under which a hard stop also reports ~0 m/s
// and passes. How long the speed takes to fall from 90% to 10% of peak is
// immune to that tail: a ramp spreads it over a second or more, a hard stop
// collapses it into a single frame.
const lastAbove = (fraction) => {
for (let i = speeds.length - 1; i >= 0; i -= 1) if (speeds[i].v >= peakV * fraction) return speeds[i].t;
return Number.NaN;
};
const decayMs = lastAbove(0.1) - lastAbove(0.9);
report(decayMs > 800, 'the dolly eases OUT over a real ramp, not a hard stop',
`speed fell 90% → 10% of peak over ${decayMs.toFixed(0)} ms (a hard stop collapses to one frame)`);
const riseMs = (() => {
const first = (fraction) => speeds.find((s) => s.v >= peakV * fraction)?.t ?? Number.NaN;
return first(0.9) - first(0.1);
})();
report(riseMs > 800, 'and eases IN over one too',
`speed rose 10% → 90% of peak over ${riseMs.toFixed(0)} ms`);
report(peakV < 40 * 1.35, 'the easing keeps the shipped pace — the plateau IS the speed word',
`peak ${peakV.toFixed(1)} m/s over a 40 m/s mean (${(flight.distanceM / flight.durationS).toFixed(1)} m/s reported)`);
// Acceleration, differenced across NON-overlapping speed windows so the
// 400 ms averaging is not differentiated against itself.
let peakAccel = 0;
for (let i = 0, j = 0; i < speeds.length; i += 1) {
while (j < speeds.length - 1 && speeds[j].t - speeds[i].t < 400) j += 1;
const dt = (speeds[j].t - speeds[i].t) / 1000;
if (dt >= 0.3) peakAccel = Math.max(peakAccel, Math.abs(speeds[j].v - speeds[i].v) / dt);
}
report(peakAccel < 40, 'no velocity discontinuity anywhere on the route',
`peak |acceleration| ${peakAccel.toFixed(1)} m/s² (a hard start would read in the hundreds)`);
// Altitude shaping and terrain clearance, read off the real camera.
const heights = rows.map((r) => r.height);
const floors = await page.evaluate(async (samples) => {
try {
const mod = await import('/src/data/groundFloor.js');
return samples.map(({ lat, lon }) => mod.cachedGroundFloor(lat, lon));
} catch { return samples.map(() => null); }
}, rows.map((r) => ({ lat: r.lat, lon: r.lon })));
// The strongest terrain check available: the eye against the RENDERED
// surface under it, sampled live. Independent of our own floor cache, and
// therefore the one that would catch flying inside a building or a hillside.
const probed = rows.filter((r) => Number.isFinite(r.surfaceM));
if (probed.length > 20) {
let worst = Infinity;
let worstAt = null;
for (const row of probed) {
const clearance = row.height - row.surfaceM;
if (clearance < worst) { worst = clearance; worstAt = row; }
}
report(worst > 0, 'the eye is never inside the RENDERED world',
`min clearance over the rendered surface ${worst.toFixed(1)} m across ${probed.length} live probes`
+ (worstAt ? ` (worst at ${worstAt.lat.toFixed(5)}, ${worstAt.lon.toFixed(5)})` : ''));
} else {
note('rendered-surface clearance', `only ${probed.length} live mesh probes answered`);
}
const warm = floors.map((f, i) => (Number.isFinite(f) ? heights[i] - f : null)).filter((v) => v !== null);
if (warm.length > 20) {
const minAgl = Math.min(...warm);
const maxAgl = Math.max(...warm);
report(minAgl >= 90, 'the eye always clears the rendered floor',
`min AGL ${minAgl.toFixed(1)} m, max ${maxAgl.toFixed(1)} m over ${warm.length} warm samples`);
} else {
note('terrain clearance', `only ${warm.length} warm floor cells under the route — clearance clamp is pinned in npm test`);
}
// The floor ACQUISITION — the one frame where a cold-corridor safety seed is
// replaced by real terrain — is deliberately a single step, and it lands in
// the same moment the camera teleports onto the route start. It is measured
// separately from the shaping, which must be a swell for the whole flight.
const cruiseFrom = rows.findIndex((r) => r.t - rows[0].t > 1500);
const cruiseRows = cruiseFrom > 0 ? rows.slice(cruiseFrom) : rows;
const cruiseHeights = cruiseRows.map((r) => r.height);
// Split the two directions: a DESCENT is capped by the dolly (never drop the
// eye toward ground it is still learning about), while a CLIMB is deliberately
// uncapped — rising is the safety direction, and a terrain rise plus the
// shaping swell can legitimately exceed the descent cap.
let peakDescentMps = 0;
let peakClimbMps = 0;
for (let i = 0, j = 0; i < cruiseRows.length; i += 1) {
while (j < cruiseRows.length - 1 && cruiseRows[j].t - cruiseRows[i].t < RATE_WINDOW_MS) j += 1;
const dt = (cruiseRows[j].t - cruiseRows[i].t) / 1000;
if (dt < RATE_WINDOW_MS / 2000) continue;
const rate = (cruiseHeights[j] - cruiseHeights[i]) / dt;
if (rate < 0) peakDescentMps = Math.max(peakDescentMps, -rate);
else peakClimbMps = Math.max(peakClimbMps, rate);
}
const peakVerticalMps = Math.max(peakDescentMps, peakClimbMps);
const acquisitionM = Math.abs(heights[0] - cruiseHeights[0]);
const cruiseRangeM = Math.max(...cruiseHeights) - Math.min(...cruiseHeights);
report(cruiseRangeM > 5, 'altitude breathes rather than sitting flat',
`${cruiseRangeM.toFixed(1)} m of vertical range in cruise`);
report(cruiseRangeM < 120, 'and the cruise altitude never wanders far from its mean',
`${cruiseRangeM.toFixed(1)} m of range after a ${acquisitionM.toFixed(0)} m floor acquisition at the start`);
// Smoothness is measured on the camera's ABSOLUTE vertical motion, because
// that is what a viewer sees. It is tempting to difference AGL instead, to
// separate "our shaping" from "the hill" — but the floor is a ~111 m
// staircase, so an AGL series steps at every cell boundary even when the eye
// is gliding. Differencing it measures the quantization, not the ride
// (measured: 31.6 m/s of "AGL rate" while the eye moved at 8.9 m/s).
report(peakDescentMps < 10.5, 'the eye is never DROPPED — descent stays inside its cap',
`peak descent ${peakDescentMps.toFixed(1)} m/s (cap 10 m/s)`);
report(peakClimbMps < 20, 'and climbs stay a swell rather than a lurch',
`peak climb ${peakClimbMps.toFixed(1)} m/s against ~40 m/s of ground speed`);
const pitches = rows.map((r) => r.pitchDeg);
const pitchSpread = Math.max(...pitches) - Math.min(...pitches);
report(pitchSpread < 2, 'the look-down angle stays locked (no pitch wobble)',
`pitch ${Math.min(...pitches).toFixed(1)}°..${Math.max(...pitches).toFixed(1)}°`);
// ── Run 2: interrupt the dolly MID-BANK ───────────────────────────────
// Cutting a level camera proves nothing about levelling, so this waits for
// the live camera to actually be rolled before it grabs the controls.
await sleep(1500);
await installSampler();
const second = await run('fly_route', { label: 'cinema evidence', speed: 'normal' });
report(second?.ok === true, 'second flight starts for the interrupt case');
const liveRollDeg = () => page.evaluate(
() => (window.__godsEyeView.viewer.camera.roll * 180) / Math.PI,
);
let rollBeforeCut = 0;
for (let waited = 0; waited < 90000; waited += 400) {
rollBeforeCut = wrapDeg(await liveRollDeg());
if (Math.abs(rollBeforeCut) >= 3) break;
await sleep(400);
}
report(Math.abs(rollBeforeCut) >= 3, 'the dolly is genuinely banked before the cut',
`live camera roll ${rollBeforeCut.toFixed(2)}°`);
await page.screenshot({ path: path.join(OUT_DIR, 'interrupt-0-banked.png') });
const cut = await page.evaluate(async () => {
const viewer = window.__godsEyeView.viewer;
const canvas = viewer.scene.canvas;
// Read the motion slot BEFORE the cut too: under Vite a dynamic import can
// hand back a second module instance whose slot is always empty, and an
// "empty after" that was already empty before proves nothing.
let read = null;
let slotBefore = 'module-unavailable';
let slotAfter = 'module-unavailable';
try {
const mod = await import('/src/cameraVerbs.js');
read = () => mod.getActiveCameraMotion?.() ?? null;
slotBefore = read();
} catch { /* dev-only module read */ }
const rollBefore = (viewer.camera.roll * 180) / Math.PI;
window.__gevFlyTrace.marks.push({ label: 'pointerdown', t: performance.now() });
canvas.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }));
// Same synchronous turn as the pointerdown — no frame has rendered yet.
const rollAfter = (viewer.camera.roll * 180) / Math.PI;
if (read) slotAfter = read();
return { slotBefore, slotAfter, rollBefore, rollAfter };
});
// The camera-side proof, which needs no module identity at all: a banked
// horizon is level again inside the same synchronous turn as the pointerdown.
report(
Math.abs(wrapDeg(cut.rollBefore)) >= 3 && Math.abs(wrapDeg(cut.rollAfter)) < 0.01,
'the release levels the horizon synchronously — no tilt left behind',
`roll ${wrapDeg(cut.rollBefore).toFixed(2)}° → ${wrapDeg(cut.rollAfter).toFixed(4)}° in the same turn`,
);
if (cut.slotBefore && cut.slotBefore !== 'module-unavailable') {
report(cut.slotAfter === null, 'a manual pointerdown frees the motion slot synchronously',
`active before the cut: ${cut.slotBefore.kind}@${(cut.slotBefore.progress * 100).toFixed(0)}% → after: ${JSON.stringify(cut.slotAfter)}`);
} else {
note('motion-slot read',
'the dev server handed the harness a second module instance (HMR); the camera-side roll and freeze checks carry the proof');
}
await sleep(2500);
const rollAfterSettle = wrapDeg(await liveRollDeg());
report(Math.abs(rollAfterSettle) < 0.01, 'and the horizon STAYS level after the cut',
`roll ${rollAfterSettle.toFixed(4)}° 2.5 s later`);
await page.screenshot({ path: path.join(OUT_DIR, 'interrupt-1-level.png') });
const cutTrace = await readTrace();
await page.evaluate(() => window.__gevFlyTraceRemove?.());
const cutAt = cutTrace.marks.at(-1)?.t ?? 0;
const before = cutTrace.rows.filter((r) => r.t < cutAt);
const after = cutTrace.rows.filter((r) => r.t >= cutAt);
const movementBefore = before.length > 2
? sampleDistanceM(before.at(-3), before.at(-1)) : Number.NaN;
let movementAfter = 0;
for (let i = 1; i < after.length; i += 1) {
movementAfter = Math.max(movementAfter, sampleDistanceM(after[i - 1], after[i]));
}
report(movementBefore > 0.5 && movementAfter < 0.5,
'the camera freezes on the cut frame — no coast, no snap-back',
`moved ${movementBefore.toFixed(2)} m/frame before, max ${movementAfter.toFixed(3)} m/frame after (${after.length} frames)`);
fs.writeFileSync(
path.join(OUT_DIR, 'trace.json'),
JSON.stringify({
url: APP_URL,
flight,
shots: shots.map((s) => path.basename(s.file)),
samples: rows.length,
peakRollDeg: peakRoll,
peakRollRateDegS: peakRollRate,
rollTurnAgreement: agreement,
peakSpeedMps: peakV,
peakAccelMps2: peakAccel,
peakVerticalMps,
peakDescentMps,
peakClimbMps,
easeInMps: firstSecond,
easeInRampMs: riseMs,
easeOutRampMs: decayMs,
heightRangeM: [Math.min(...heights), Math.max(...heights)],
cruiseHeightRangeM: cruiseRangeM,
floorAcquisitionM: acquisitionM,
pitchRangeDeg: [Math.min(...pitches), Math.max(...pitches)],
finalRollDeg: rolls.at(-1),
terrainDelayMs: TERRAIN_DELAY_MS,
interrupt: {
movementBefore,
movementAfter,
framesAfter: after.length,
rollBeforeCutDeg: wrapDeg(cut.rollBefore),
rollAfterCutDeg: wrapDeg(cut.rollAfter),
rollAfterSettleDeg: rollAfterSettle,
},
series: rows.map((r) => ({
t: Number((r.t - rows[0].t).toFixed(0)),
roll: Number(wrapDeg(r.rollDeg).toFixed(3)),
heading: Number(r.headingDeg.toFixed(2)),
pitch: Number(r.pitchDeg.toFixed(2)),
height: Number(r.height.toFixed(1)),
})),
}, null, 2),
);
// Contact sheet: the sequence in one image, each tile stamped with the
// elapsed time and the roll the camera was actually holding at that moment.
const nearestRow = (at) => rows.reduce(
(best, row) => (Math.abs(row.t - at) < Math.abs(best.t - at) ? row : best), rows[0],
);
const cols = 5;
const tileW = 384;
const tileH = Math.round((950 / 1500) * tileW);
const labelH = 26;
const gap = 6;
const sheetRows = Math.ceil(shots.length / cols);
const sheetW = (cols * tileW) + ((cols + 1) * gap);
const sheetH = (sheetRows * (tileH + labelH)) + ((sheetRows + 1) * gap);
const composites = [];
const labels = [];
for (let i = 0; i < shots.length; i += 1) {
const col = i % cols;
const row = Math.floor(i / cols);
const left = gap + (col * (tileW + gap));
const top = gap + (row * (tileH + labelH + gap));
composites.push({
input: await sharp(shots[i].file).resize(tileW, tileH, { fit: 'fill' }).toBuffer(),
left,
top,
});
const near = nearestRow(shots[i].at);
const elapsed = ((near.t - rows[0].t) / 1000).toFixed(0);
const roll = wrapDeg(near.rollDeg);
const sign = roll >= 0 ? '+' : '−';
labels.push(`<text x="${left + 6}" y="${top + tileH + 18}" font-family="monospace" font-size="15" fill="#8fe9ff">`
+ `${String(i).padStart(2, '0')} t=${elapsed}s roll ${sign}${Math.abs(roll).toFixed(1)}° alt ${near.height.toFixed(0)}m</text>`);
}
composites.push({
input: Buffer.from(`<svg width="${sheetW}" height="${sheetH}">${labels.join('')}</svg>`),
left: 0,
top: 0,
});
const sheetPath = path.join(OUT_DIR, 'sequence-contact-sheet.jpg');
await sharp({ create: { width: sheetW, height: sheetH, channels: 3, background: '#05080d' } })
.composite(composites)
.jpeg({ quality: 86 })
.toFile(sheetPath);
console.log(` contact sheet → ${sheetPath}`);
if (MIRROR_DIR) {
// Mirror as JPEG: the PNG sequence is ~80 MB, which is a poor thing to
// hand a human who just wants to flip through the shot.
for (const entry of fs.readdirSync(OUT_DIR)) {
const from = path.join(OUT_DIR, entry);
if (entry.endsWith('.png')) {
await sharp(from).jpeg({ quality: 80 })
.toFile(path.join(MIRROR_DIR, entry.replace(/\.png$/, '.jpg')));
} else {
fs.copyFileSync(from, path.join(MIRROR_DIR, entry));
}
}
}
} catch (error) {
report(false, 'harness completed', String(error?.message || error).slice(0, 200));
} finally {
await browser.close();
}
const failed = results.filter((r) => r.ok === false).length;
const passed = results.filter((r) => r.ok === true).length;
console.log(`\n ${passed} passed, ${failed} failed → ${OUT_DIR}`);
process.exitCode = failed ? 1 : 0;
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env node
/**
* Are the fly_route pins actually load-bearing?
*
* A pin that is red only when you delete the whole feature proves very little.
* This reverts each fix INDIVIDUALLY — the smallest edit that reintroduces the
* original defect — and requires src/routeCinematics.test.mjs to go red for it.
* Every entry names the defect it restores, so the count is reproducible rather
* than asserted in a commit message.
*
* node scripts/qa-flyroute-mutations.mjs
*
* Restores the file on exit, including on failure.
*/
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const SOURCE = path.join(ROOT, 'src', 'cameraVerbs.js');
const TESTS = 'src/routeCinematics.test.mjs';
/** @type {Array<{defect: string, from: string, to: string}>} */
const MUTATIONS = [
{
defect: 'interrupt leaves the horizon tilted (round-1 behaviour)',
from: 'const leveled = wasRoute && !tracking ? levelCameraRoll() : false;',
to: 'const leveled = false;',
},
{
defect: 'completion freezes a bank into the final frame',
from: 'const appliedBankDeg = state.bankDeg * profile.speed;',
to: 'const appliedBankDeg = state.bankDeg;',
},
{
defect: 'heading lerps in Cartesian space and cannot cross a U-turn',
from: ' const turned = Cesium.Matrix3.multiplyByVector(rotation, state.headingDir, _frameDir);',
to: ' const turned = Cesium.Cartesian3.lerp(state.headingDir, gaze, k, _frameDir);',
},
{
defect: 'the exact-180° bank direction becomes a floating-point coin toss',
from: ' if (cosTurn <= -1 + ANTIPODAL_EPS && Math.abs(sinLeft) < ANTIPODAL_EPS) return Math.PI;',
to: ' // branch removed',
},
{
defect: 'a cold cell is read as sea level (the 1.3 km underground bug)',
from: ' state.floorM = Number.isFinite(state.floorM) ? state.floorM : state.coldSeedFloorM;',
to: ' state.floorM = carto.height;',
},
{
defect: 'the flight never warms its own corridor',
from: ' warmRouteCorridor(state, 0, ROUTE_WARM_START_M);',
to: ' // warm removed',
},
{
defect: 'the dolly races the fire-and-forget warm instead of arming for it',
from: ' if (canAnswer && !state.floorKnown && !state.departed && state.armS < ROUTE_ARM_S) {',
to: ' if (false) {',
},
{
defect: 'no mesh probe, so a permanently cold DEM is flown blind',
from: ' if (cold.length && !state.meshProbeSpent && typeof state.probeFn === \'function\') {',
to: ' if (false) {',
},
{
defect: 'the launch-altitude hold is dropped and the eye descends blind',
from: ' if (!state.floorKnown && Number.isFinite(state.safeHoldHeightM)) {',
to: ' if (false) {',
},
{
defect: 'releasing the hold onto late data drops the eye without a limit',
from: ' if (Number.isFinite(state.appliedHeightM)) {',
to: ' if (false) {',
},
{
defect: 'a late mid-flight floor is taken whole and snaps the eye',
from: ' state.floorM = routeFloorHoldM(state.floorM, sampledFloor, step);\n state.floorKnown = true;',
to: ' state.floorM = sampledFloor;\n state.floorKnown = true;',
},
{
defect: 'the lookahead floor read is dropped (clearance only underfoot)',
from: ' const warm = [here, ahead].filter((value) => Number.isFinite(value));',
to: ' const warm = [here].filter((value) => Number.isFinite(value));',
},
{
defect: 'the shaping floor adopts rises instantly and pops at cell edges',
from: ' if (!Number.isFinite(previousM)) return sampledM;',
to: ' if (!Number.isFinite(previousM) || sampledM >= previousM) return sampledM;',
},
{
defect: 'the mesh probe fires every arming frame instead of once per route',
from: ' if (cold.length && !state.meshProbeSpent && typeof state.probeFn === \'function\') {\n state.meshProbeSpent = true;',
to: ' if (cold.length && typeof state.probeFn === \'function\') {\n state.meshProbeSpent = true;',
},
{
defect: 'one warm cell resolves a corridor whose other cells are unknown',
from: ' if (cold.length && !state.meshProbeCoveredCold) return false;',
to: ' // partial-corridor guard removed',
},
{
defect: 'the probe re-reads cells the cache already answered',
from: ' const probe = state.probeFn(cold) || {};',
to: ' const probe = state.probeFn(cells) || {};',
},
{
defect: 'the hard clearance clamp reads the SMOOTHED floor, not the raw sample',
from: ' (Number.isFinite(sampledFloor) ? sampledFloor : state.floorM) + ROUTE_MIN_CLEARANCE_M,',
to: ' state.floorM + ROUTE_MIN_CLEARANCE_M,',
},
{
defect: 'levelling also RE-FRAMES the camera (destination passed with the HPR)',
from: ' cam.setView({ orientation: { heading: cam.heading, pitch: cam.pitch, roll: 0 } });',
to: ' cam.setView({ destination: Cesium.Cartesian3.fromDegrees(0, 0, 5000), orientation: { heading: cam.heading, pitch: cam.pitch, roll: 0 } });',
},
{
defect: 'the OS reduced-motion preference is never consulted',
from: ' reducedMotion: prefersReducedMotion(),',
to: ' reducedMotion: false,',
},
];
const original = fs.readFileSync(SOURCE, 'utf8');
let caught = 0;
const missed = [];
process.on('exit', () => fs.writeFileSync(SOURCE, original));
console.log(`\nfly_route pin strength — ${MUTATIONS.length} individual reverts\n`);
for (const { defect, from, to } of MUTATIONS) {
if (!original.includes(from)) {
missed.push(`${defect} (ANCHOR MISSING — the mutation no longer applies)`);
console.log(` \x1b[31mSTALE\x1b[0m ${defect}`);
continue;
}
fs.writeFileSync(SOURCE, original.replace(from, to));
let red = false;
let by = '';
try {
execFileSync('node', ['--test', TESTS], { cwd: ROOT, encoding: 'utf8', stdio: 'pipe' });
} catch (error) {
red = true;
const failed = String(error.stdout || '').split('\n')
.filter((line) => line.trim().startsWith('✖') && line.includes('('))
.map((line) => line.trim().slice(2).split(' (')[0]);
by = [...new Set(failed)].slice(0, 2).join('; ');
}
if (red) {
caught += 1;
console.log(` \x1b[32mRED \x1b[0m ${defect}\n caught by: ${by}`);
} else {
missed.push(defect);
console.log(` \x1b[31mGREEN\x1b[0m ${defect} <-- no pin covers this`);
}
}
fs.writeFileSync(SOURCE, original);
console.log(`\n ${caught}/${MUTATIONS.length} defects caught by the pins`);
if (missed.length) {
console.log('\n uncovered:');
for (const item of missed) console.log(` - ${item}`);
}
process.exitCode = missed.length ? 1 : 0;
+721
View File
@@ -0,0 +1,721 @@
#!/usr/bin/env node
/**
* Moving visual evidence for focus de-emphasis and aircraft recession.
*
* The operator owns the live dev server; this script never starts one:
* node scripts/qa-focus-evidence.mjs --url http://localhost:4173 \
* --screenshots-dir qa-shots/focus-evidence \
* --json qa-shots/focus-evidence/report.json
* node scripts/qa-focus-evidence.mjs --headful --smoke
* node scripts/qa-focus-evidence.mjs --url http://localhost:4173 \
* --params '{"focus":{"dimFloor":0.35},"horizon":{"scaleFloor":0.5}}'
* node scripts/qa-focus-evidence.mjs --headful --basemap osm
*
* The harness owns the Cesium frame clock during capture: every frame applies
* synthetic updates, advances explicit virtual time, calls scene.render(),
* then screenshots. Headless mode keeps SwiftShader for CI-relative evidence;
* --headful drops those flags and is the real-GPU visual sign-off path.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import puppeteer from 'puppeteer';
const argv = process.argv.slice(2);
const getOpt = (name, fallback = null) => {
const index = argv.indexOf(name);
return index >= 0 && argv[index + 1] ? argv[index + 1] : fallback;
};
const hasFlag = (name) => argv.includes(name);
const APP_URL = getOpt('--url', 'http://localhost:4173');
const JSON_PATH = path.resolve(getOpt('--json', 'qa-shots/focus-evidence/report.json'));
const SCREENSHOTS_DIR = path.resolve(getOpt('--screenshots-dir', 'qa-shots/focus-evidence'));
const HEADFUL = hasFlag('--headful');
const SMOKE = hasFlag('--smoke');
const MAP_STACK_IDS = Object.freeze(['photoreal', 'bing-aerial', 'bing-labels', 'osm']);
const BASEMAP = getOpt('--basemap', 'photoreal');
const VIEWPORT = Object.freeze({ width: 1440, height: 900 });
const FRAME_COUNT = SMOKE ? 6 : 30;
const FRAME_MS = 100;
const TILE_SETTLE_TIMEOUT_MS = 45_000;
const TARGET_ID = 'f0c001';
const _firstCaptureScenarios = new Set();
if (!MAP_STACK_IDS.includes(BASEMAP)) {
throw new Error(`Invalid --basemap ${BASEMAP}; expected one of ${MAP_STACK_IDS.join(', ')}`);
}
const TARGET = Object.freeze({
id: TARGET_ID,
callsign: 'FOCUS1',
longitude: -97.7431,
latitude: 30.2672,
altitudeM: 3_000,
klass: 'airliner',
});
// Owner's 13:58 horizon composition: a low, east-facing regional view whose
// distant field sits close to the geometric limb instead of a whole-globe view.
const HORIZON_CAMERA = Object.freeze([
-100, 30, 1_000_000, Math.PI / 2, -0.52,
]);
function parseParams() {
const raw = getOpt('--params');
if (!raw) return { focus: {}, horizon: {} };
let parsed;
try {
parsed = JSON.parse(raw);
} catch (error) {
throw new Error(`Invalid --params JSON: ${error.message}`);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('--params must be a JSON object');
}
if (parsed.focus || parsed.horizon) {
if ((parsed.focus && (typeof parsed.focus !== 'object' || Array.isArray(parsed.focus)))
|| (parsed.horizon && (typeof parsed.horizon !== 'object' || Array.isArray(parsed.horizon)))) {
throw new Error('--params focus and horizon values must be JSON objects');
}
return { focus: parsed.focus || {}, horizon: parsed.horizon || {} };
}
const focusKeys = new Set([
'paddingPx', 'dimFloor', 'nearerBehavior', 'hysteresisPx',
'distanceHysteresisRatio', 'attackMs', 'releaseMs', 'writeEpsilon',
]);
const horizonKeys = new Set([
'startLimbRatio', 'scaleFloor', 'alphaFloor', 'combinedAlphaFloor',
'globeViewBlendStartM', 'globeViewBlendEndM', 'globeViewHeightM',
'earthRadiusM', 'writeEpsilon',
]);
const focus = {};
const horizon = {};
for (const [key, value] of Object.entries(parsed)) {
if (focusKeys.has(key)) focus[key] = value;
if (horizonKeys.has(key)) horizon[key] = value;
}
return { focus, horizon };
}
function add(a, b) {
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
}
function scale(v, amount) {
return [v[0] * amount, v[1] * amount, v[2] * amount];
}
function screenPlanePosition(basis, xPx, yPx, fartherM = 0) {
return add(
add(
add(basis.target, scale(basis.right, xPx * basis.metresPerPixel)),
scale(basis.up, yPx * basis.metresPerPixel),
),
scale(basis.away, fartherM),
);
}
function shotPath(scenario, frame) {
return path.join(SCREENSHOTS_DIR, `${scenario}-${frame}.png`);
}
async function readTileReadiness(page) {
return page.evaluate(() => {
const gev = window.__godsEyeView;
const controller = gev.mapStackController;
const activeStack = controller?.getActiveId?.() || null;
const tileset = controller?.googleTileset || gev.tileset || null;
const applicable = activeStack === 'photoreal' && tileset?.show !== false;
return {
tilesSettled: !applicable || tileset?.tilesLoaded === true,
tileSettleApplicable: applicable,
activeStack,
};
});
}
async function awaitTilesSettled(page, scenario) {
const result = await page.evaluate(async (timeoutMs) => {
const gev = window.__godsEyeView;
const controller = gev.mapStackController;
const activeStack = controller?.getActiveId?.() || null;
// The map-stack controller owns the authoritative Google tileset handle;
// the bootstrap field is retained only as a compatibility fallback.
const tileset = controller?.googleTileset || gev.tileset || null;
const applicable = activeStack === 'photoreal' && tileset?.show !== false;
if (!applicable) {
return {
tilesSettled: true,
tileSettleApplicable: false,
activeStack,
timedOut: false,
};
}
if (tileset.tilesLoaded === true) {
return {
tilesSettled: true,
tileSettleApplicable: true,
activeStack,
timedOut: false,
};
}
let allTilesLoadedObserved = false;
const removeListener = tileset.allTilesLoaded?.addEventListener?.(() => {
allTilesLoadedObserved = true;
});
const deadline = Date.now() + timeoutMs;
try {
while (Date.now() < deadline) {
gev.viewer.scene.render(gev.viewer.clock.currentTime);
if (tileset.tilesLoaded === true || allTilesLoadedObserved) {
return {
tilesSettled: true,
tileSettleApplicable: true,
activeStack,
timedOut: false,
};
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
return {
tilesSettled: false,
tileSettleApplicable: true,
activeStack,
timedOut: true,
};
} finally {
if (typeof removeListener === 'function') removeListener();
}
}, TILE_SETTLE_TIMEOUT_MS);
const status = result.tilesSettled ? 'settled' : `NOT settled after ${TILE_SETTLE_TIMEOUT_MS / 1000}s`;
console.log(` tiles (${scenario}): ${status}`);
return result;
}
async function capture(page, scenario, frame) {
if (!_firstCaptureScenarios.has(scenario)) {
_firstCaptureScenarios.add(scenario);
await awaitTilesSettled(page, scenario);
}
const tileReadiness = await readTileReadiness(page);
const out = shotPath(scenario, frame);
// Chromium's Page.captureScreenshot can wedge while a manually driven
// WebGL surface is paused. Reading the explicitly rendered canvas captures
// that same frame without handing frame ownership back to the browser.
const dataUrl = await page.evaluate(() => (
window.__godsEyeView.viewer.scene.canvas.toDataURL('image/png')
));
fs.writeFileSync(out, Buffer.from(dataUrl.slice(dataUrl.indexOf(',') + 1), 'base64'));
return { file: out, ...tileReadiness };
}
async function captureSequenceFrame(page, scenario, frameIndex, update) {
if (SMOKE) console.log(` frame ${frameIndex + 1}/${FRAME_COUNT}: update`);
await update();
if (SMOKE) console.log(` frame ${frameIndex + 1}/${FRAME_COUNT}: render`);
await advanceEvidenceFrame(page, FRAME_MS);
if (SMOKE) console.log(` frame ${frameIndex + 1}/${FRAME_COUNT}: screenshot`);
return capture(page, scenario, `frame-${String(frameIndex).padStart(2, '0')}`);
}
async function takeFrameClock(page) {
const result = await page.evaluate(() => {
const seam = window.__godsEyeView.dataManager.layers.get('flights').module.__focusEvidence;
return seam.takeFrameClock();
});
if (!result?.ok) throw new Error('Unable to take ownership of the evidence frame clock');
}
async function advanceEvidenceFrame(page, deltaMs = FRAME_MS) {
return page.evaluate((stepMs) => {
const gev = window.__godsEyeView;
const viewer = gev.viewer;
const seam = gev.dataManager.layers.get('flights').module.__focusEvidence;
const nowMs = seam.advanceFrameClock(stepMs);
const JulianDate = viewer.clock.currentTime.constructor;
JulianDate.addSeconds(viewer.clock.currentTime, stepMs / 1000, viewer.clock.currentTime);
viewer.dataSourceDisplay?.update(viewer.clock.currentTime);
viewer.scene.render(viewer.clock.currentTime);
// Cross a task boundary so Chrome can commit the explicitly rendered
// surface before capture. Cesium's drained default loop stays disabled.
return new Promise((resolve) => setTimeout(() => resolve(nowMs), 0));
}, deltaMs);
}
async function advanceEvidenceDuration(page, durationMs) {
// Cross one 80 ms consumer quantum to publish newly changed desired state,
// then jump to the requested endpoint. A zero-delta render is insufficient:
// the production focus passes correctly retain their cadence gate.
const totalMs = Math.max(0, durationMs);
const publishMs = Math.min(80, totalMs);
await advanceEvidenceFrame(page, publishMs);
if (totalMs > publishMs) await advanceEvidenceFrame(page, totalMs - publishMs);
}
async function releaseFrameClock(page) {
await page.evaluate(() => {
window.__godsEyeView.dataManager.layers
.get('flights').module.__focusEvidence.releaseFrameClock();
});
}
async function installSyntheticFetches(page) {
await page.evaluateOnNewDocument(() => {
const realFetch = window.fetch.bind(window);
const json = (body) => Promise.resolve(new Response(JSON.stringify(body), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}));
window.fetch = (input, init) => {
const url = typeof input === 'string' ? input : input?.url || '';
if (url.includes('/api/opensky-track')) return json({ path: [] });
if (url.includes('/api/adsblol/trace')) return json({ timestamp: Date.now() / 1000, trace: [] });
if (url.includes('/api/opensky')) return json({ time: Math.floor(Date.now() / 1000), states: [] });
if (url.includes('/api/adsbdb/')) return json({ found: false });
if (url.includes('/api/ais-live')) return json({ status: 'open', rows: [] });
return realFetch(input, init);
};
});
}
async function waitForApp(page) {
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.waitForFunction(
() => Boolean(window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager),
{ timeout: 60_000, polling: 200 },
);
}
async function setBasemap(page, stackId) {
const result = await page.evaluate(async (id) => (
window.__godsEyeView.styleManager.setMapStack(id)
), stackId);
if (!result?.ok) {
throw new Error(`Unable to activate --basemap ${stackId}: ${result?.error || 'unknown error'}`);
}
return result.activeStack;
}
async function enableLayer(page, layerId) {
await page.evaluate(async (id) => {
await window.__godsEyeView.dataManager.setEnabled(id, true);
}, layerId);
}
async function requireEvidenceSeams(page) {
const result = await page.evaluate(() => {
const manager = window.__godsEyeView.dataManager;
return {
flights: Boolean(manager.layers.get('flights')?.module?.__focusEvidence),
vessels: Boolean(manager.layers.get('ais-live-vessels')?.module?.__focusEvidence),
};
});
if (!result.flights || !result.vessels) {
throw new Error('Focus evidence seams are unavailable. Use the Vite dev server (not a production preview/build).');
}
}
async function setCamera(page, values) {
await page.evaluate(([lon, lat, height, heading, pitch]) => {
const viewer = window.__godsEyeView.viewer;
const ellipsoid = viewer.scene.globe.ellipsoid;
viewer.trackedEntity = undefined;
viewer.camera.cancelFlight?.();
viewer.scene.tweens?.removeAll?.();
viewer.camera.setView({
destination: ellipsoid.cartographicToCartesian({
longitude: lon * Math.PI / 180,
latitude: lat * Math.PI / 180,
height,
}),
orientation: { heading, pitch, roll: 0 },
});
}, values);
await advanceEvidenceDuration(page, 300);
}
async function injectAndTrackTarget(page, extraAircraft = [], target = TARGET) {
const result = await page.evaluate(({ targetRecord, extras }) => {
const layer = window.__godsEyeView.dataManager.layers.get('flights').module;
const injected = layer.__focusEvidence.setAircraft([targetRecord, ...extras]);
const tracked = layer.trackById(targetRecord.id);
return { injected, tracked };
}, { targetRecord: target, extras: extraAircraft });
if (!result.injected?.ok || !result.tracked) {
throw new Error(`Synthetic target setup failed: ${JSON.stringify(result)}`);
}
await page.waitForFunction(
() => Boolean(window.__godsEyeView.viewer.trackedEntity),
{ timeout: 10_000 },
);
await advanceEvidenceDuration(page, 400);
}
async function readTrackedBasis(page) {
return page.evaluate((targetId) => {
const gev = window.__godsEyeView;
const layer = gev.dataManager.layers.get('flights').module;
const target = layer.getAllPositions(100).find((entry) => entry.id === targetId)?.position;
const camera = gev.viewer.camera;
if (!target) throw new Error('Tracked target position unavailable');
const toTarget = {
x: target.x - camera.positionWC.x,
y: target.y - camera.positionWC.y,
z: target.z - camera.positionWC.z,
};
const distance = Math.hypot(toTarget.x, toTarget.y, toTarget.z);
const canvasHeight = gev.viewer.scene.canvas.clientHeight;
const metresPerPixel = (2 * distance * Math.tan(camera.frustum.fovy / 2)) / canvasHeight;
return {
target: [target.x, target.y, target.z],
right: [camera.rightWC.x, camera.rightWC.y, camera.rightWC.z],
up: [camera.upWC.x, camera.upWC.y, camera.upWC.z],
away: [toTarget.x / distance, toTarget.y / distance, toTarget.z / distance],
metresPerPixel,
targetDistanceM: distance,
};
}, TARGET_ID);
}
async function flightSnapshot(page) {
return page.evaluate(() => (
window.__godsEyeView.dataManager.layers.get('flights').module.__focusEvidence.snapshot()
));
}
async function setTuning(page, params) {
return page.evaluate((next) => (
window.__godsEyeView.dataManager.layers.get('flights').module.__focusEvidence.setTuning(next)
), params);
}
async function runControlledCrossing(page, effectiveParams) {
const scenario = 's1-controlled-crossing';
if (SMOKE) console.log(' setup: inject + track');
await injectAndTrackTarget(page, [{ ...TARGET, id: 'f0c002', callsign: 'CROSS2' }]);
if (SMOKE) console.log(' setup: read tracked basis');
const basis = await readTrackedBasis(page);
// Stay beyond the default 8% range-side hysteresis band so S1 exercises
// the farther-contact dim path instead of hovering in its neutral band.
const crossingDepthM = Math.max(600, basis.targetDistanceM * 0.12);
const start = screenPlanePosition(basis, -180, 0, crossingDepthM);
await page.evaluate((position) => {
const seam = window.__godsEyeView.dataManager.layers.get('flights').module.__focusEvidence;
seam.moveAircraft([{ id: 'f0c002', cartesian: position, trackDeg: 90 }]);
}, start);
// The placeholder begins at the target so tracking can establish its camera
// first. Restore fully off-target before frame 0; otherwise the sequence
// would start with inherited dimming rather than showing a true attack.
if (SMOKE) console.log(' setup: settle off-target contact');
await advanceEvidenceDuration(page, effectiveParams.focus.releaseMs + 150);
const frames = [];
for (let i = 0; i < FRAME_COUNT; i += 1) {
const xPx = -180 + (360 * i) / (FRAME_COUNT - 1);
const cartesian = screenPlanePosition(basis, xPx, 0, crossingDepthM);
const captured = await captureSequenceFrame(page, scenario, i, () => page.evaluate((position) => {
const seam = window.__godsEyeView.dataManager.layers.get('flights').module.__focusEvidence;
seam.moveAircraft([{ id: 'f0c002', cartesian: position, trackDeg: 90 }]);
}, cartesian));
const snapshot = await flightSnapshot(page);
frames.push({ index: i, xPx, ...captured, contacts: snapshot });
}
return { id: scenario, frameCount: frames.length, basis, crossingDepthM, frames };
}
async function vesselRowsAroundTarget(page) {
return page.evaluate(() => {
const gev = window.__godsEyeView;
const layer = gev.dataManager.layers.get('flights').module;
const target = layer.getAllPositions(100).find((entry) => entry.id === 'f0c001')?.position;
const Cartographic = gev.viewer.camera.positionCartographic.constructor;
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
const rows = [];
for (let i = 0; i < 12; i += 1) {
const angle = (i / 12) * Math.PI * 2;
const eastM = Math.cos(angle) * (15 + (i % 3) * 12);
const northM = Math.sin(angle) * (15 + (i % 3) * 12);
// Cesium.Transforms is not global; a small local lat/lon offset is
// sufficient at harbor scale and keeps the injected vessel at sea level.
const carto = Cartographic.fromCartesian(target, ellipsoid);
const lat = carto.latitude * 180 / Math.PI + northM / 111_320;
const lon = carto.longitude * 180 / Math.PI
+ eastM / (111_320 * Math.cos(carto.latitude));
rows.push({
mmsi: String(990000000 + i),
name: `HARBOR ${String(i + 1).padStart(2, '0')}`,
lat,
lon,
speed: 4 + (i % 5),
course: (i * 31) % 360,
type: i % 3 === 0 ? 'Cargo' : 'Tug',
});
}
return rows;
});
}
async function runHarborClutter(page, effectiveParams) {
const scenario = 's2-harbor-clutter';
const lowTarget = { ...TARGET, altitudeM: 50 };
// Harbor contacts straddle the tracked low-altitude subject's range. This
// scenario deliberately exercises the documented tunable all-overlap policy
// instead of being neutralized by the production `allow` range band.
const clutterFocus = { ...effectiveParams.focus, nearerBehavior: 'dim' };
await injectAndTrackTarget(page, [], lowTarget);
const rows = await vesselRowsAroundTarget(page);
await page.evaluate((vessels) => {
const seam = window.__godsEyeView.dataManager.layers
.get('ais-live-vessels').module.__focusEvidence;
seam.setVessels(vessels);
}, rows);
await setTuning(page, { focus: { ...clutterFocus, dimFloor: 1 } });
await advanceEvidenceDuration(page, 450);
const beforeCapture = await capture(page, scenario, 'before');
const before = await page.evaluate(() => (
window.__godsEyeView.dataManager.layers
.get('ais-live-vessels').module.__focusEvidence.snapshot()
));
await setTuning(page, { ...effectiveParams, focus: clutterFocus });
await advanceEvidenceDuration(page, Math.max(500, effectiveParams.focus.attackMs + 200));
const afterCapture = await capture(page, scenario, 'after');
const after = await page.evaluate(() => (
window.__godsEyeView.dataManager.layers
.get('ais-live-vessels').module.__focusEvidence.snapshot()
));
await setTuning(page, effectiveParams);
return {
id: scenario,
focusParams: clutterFocus,
frames: [
{ phase: 'before', ...beforeCapture, contacts: before },
{ phase: 'after', ...afterCapture, contacts: after },
],
};
}
async function runAirportTraffic(page, effectiveParams) {
const scenario = 's3-airport-traffic';
const placeholders = Array.from({ length: 40 }, (_, index) => ({
...TARGET,
id: `f3${String(index).padStart(4, '0')}`,
callsign: `APT${String(index).padStart(2, '0')}`,
klass: index % 7 === 0 ? 'widebody' : index % 5 === 0 ? 'fastjet' : 'airliner',
}));
await injectAndTrackTarget(page, placeholders);
const basis = await readTrackedBasis(page);
const trafficDepthM = Math.max(600, basis.targetDistanceM * 0.12);
const startingMoves = placeholders.map((record, index) => {
const angle = (index / placeholders.length) * Math.PI * 2;
return {
id: record.id,
cartesian: screenPlanePosition(
basis,
Math.cos(angle) * 220,
Math.sin(angle) * 220 * 0.55,
trafficDepthM + index * 3,
),
trackDeg: (angle * 180 / Math.PI + 180) % 360,
};
});
await page.evaluate((positions) => {
const seam = window.__godsEyeView.dataManager.layers.get('flights').module.__focusEvidence;
seam.moveAircraft(positions);
}, startingMoves);
await advanceEvidenceDuration(page, effectiveParams.focus.releaseMs + 150);
const frames = [];
for (let frame = 0; frame < FRAME_COUNT; frame += 1) {
const progress = frame / (FRAME_COUNT - 1);
const radiusPx = 220 - 205 * progress;
const moves = placeholders.map((record, index) => {
const angle = (index / placeholders.length) * Math.PI * 2 + progress * 0.35;
return {
id: record.id,
cartesian: screenPlanePosition(
basis,
Math.cos(angle) * radiusPx,
Math.sin(angle) * radiusPx * 0.55,
trafficDepthM + index * 3,
),
trackDeg: (angle * 180 / Math.PI + 180) % 360,
};
});
const captured = await captureSequenceFrame(page, scenario, frame, () => page.evaluate((positions) => {
const seam = window.__godsEyeView.dataManager.layers.get('flights').module.__focusEvidence;
seam.moveAircraft(positions);
}, moves));
frames.push({ index: frame, radiusPx, ...captured, contacts: await flightSnapshot(page) });
}
return { id: scenario, frameCount: frames.length, basis, frames };
}
async function runHorizonBand(page, effectiveParams) {
const scenario = 's4-horizon-band';
await page.evaluate(() => {
window.__godsEyeView.dataManager.layers.get('flights').module.stopTracking();
});
const contacts = [];
for (let row = 0; row < 6; row += 1) {
for (let column = 0; column < 10; column += 1) {
contacts.push({
id: `f4${String(row * 10 + column).padStart(4, '0')}`,
callsign: `LIMB${String(row * 10 + column).padStart(2, '0')}`,
longitude: -70.5 + column * 0.42,
latitude: 27.5 + row * 0.95,
altitudeM: 10_000 + (column % 3) * 800,
klass: column % 4 === 0 ? 'widebody' : 'airliner',
});
}
}
await page.evaluate((records) => {
window.__godsEyeView.dataManager.layers.get('flights')
.module.__focusEvidence.setAircraft(records);
}, contacts);
await setCamera(page, HORIZON_CAMERA);
await setTuning(page, {
horizon: { ...effectiveParams.horizon, scaleFloor: 1, alphaFloor: 1 },
});
await advanceEvidenceDuration(page, 450);
const beforeCapture = await capture(page, scenario, 'before');
const before = await flightSnapshot(page);
await setTuning(page, effectiveParams);
await advanceEvidenceDuration(page, 450);
const afterCapture = await capture(page, scenario, 'after');
const after = await flightSnapshot(page);
return {
id: scenario,
camera: HORIZON_CAMERA,
frames: [
{ phase: 'before', ...beforeCapture, contacts: before },
{ phase: 'after', ...afterCapture, contacts: after },
],
};
}
async function browserContext(page, browser) {
const gl = await page.evaluate(() => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
const debug = context?.getExtension('WEBGL_debug_renderer_info');
return {
userAgent: navigator.userAgent,
dpr: window.devicePixelRatio,
vendor: debug
? context.getParameter(debug.UNMASKED_VENDOR_WEBGL)
: context?.getParameter(context.VENDOR) || 'unavailable',
renderer: debug
? context.getParameter(debug.UNMASKED_RENDERER_WEBGL)
: context?.getParameter(context.RENDERER) || 'unavailable',
};
});
return {
browser: await browser.version(),
userAgent: gl.userAgent,
webglVendor: gl.vendor,
webglRenderer: gl.renderer,
viewport: { ...VIEWPORT, dpr: gl.dpr },
machine: {
hostname: os.hostname(),
platform: os.platform(),
arch: os.arch(),
},
caveat: HEADFUL
? 'Headful capture omits SwiftShader flags and is the real-GPU visual sign-off path.'
: 'SwiftShader/headless output is relative-only; rerun with --headful for real-GPU sign-off.',
};
}
async function main() {
const requestedParams = parseParams();
const response = await fetch(APP_URL).catch((error) => ({ ok: false, statusText: error.message }));
if (!response.ok) {
throw new Error(`Live dev server unavailable at ${APP_URL}: ${response.status || response.statusText}`);
}
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
fs.mkdirSync(path.dirname(JSON_PATH), { recursive: true });
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
protocolTimeout: 300000,
args: [
...(HEADFUL ? [] : ['--enable-unsafe-swiftshader', '--use-gl=swiftshader']),
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
`--window-size=${VIEWPORT.width},${VIEWPORT.height}`,
],
});
const page = await browser.newPage();
await page.setViewport(VIEWPORT);
const consoleMessages = [];
page.on('console', (message) => {
if (['error', 'warning', 'warn'].includes(message.type())) {
consoleMessages.push({ type: message.type(), text: message.text() });
}
});
page.on('pageerror', (error) => consoleMessages.push({ type: 'pageerror', text: error.message }));
const report = {
generatedAt: new Date().toISOString(),
url: APP_URL,
screenshotsDir: SCREENSHOTS_DIR,
requestedParams,
requestedBasemap: BASEMAP,
activeBasemap: null,
smoke: SMOKE,
effectiveParams: null,
context: null,
scenarios: [],
consoleMessages,
};
try {
await installSyntheticFetches(page);
await waitForApp(page);
report.activeBasemap = await setBasemap(page, BASEMAP);
await enableLayer(page, 'flights');
await enableLayer(page, 'ais-live-vessels');
await requireEvidenceSeams(page);
await takeFrameClock(page);
report.context = await browserContext(page, browser);
report.effectiveParams = await setTuning(page, requestedParams);
console.log(`Focus evidence → ${APP_URL}`);
console.log(` screenshots: ${SCREENSHOTS_DIR}`);
console.log(` renderer : ${report.context.webglRenderer}`);
console.log(` basemap : ${report.activeBasemap}`);
console.log(` params : ${JSON.stringify(report.effectiveParams)}`);
console.log(` mode : ${SMOKE ? 'smoke (S1, 6 frames)' : 'full (S1-S4)'}`);
console.log(` caveat : ${report.context.caveat}`);
const scenarios = [
['S1 controlled crossing', () => runControlledCrossing(page, report.effectiveParams)],
['S2 harbor clutter', () => runHarborClutter(page, report.effectiveParams)],
['S3 airport traffic', () => runAirportTraffic(page, report.effectiveParams)],
['S4 horizon band', () => runHorizonBand(page, report.effectiveParams)],
];
for (const [label, run] of (SMOKE ? scenarios.slice(0, 1) : scenarios)) {
console.log(` capture : ${label}`);
await page.bringToFront();
report.scenarios.push(await run());
}
} finally {
if (!page.isClosed()) await releaseFrameClock(page).catch(() => {});
await browser.close();
}
fs.writeFileSync(JSON_PATH, `${JSON.stringify(report, null, 2)}\n`);
console.log(` report : ${JSON_PATH}`);
if (consoleMessages.some((message) => message.type === 'error' || message.type === 'pageerror')) {
process.exitCode = 1;
}
}
main().catch((error) => {
console.error(error?.stack || error);
process.exit(1);
});
File diff suppressed because it is too large. Load diff
+726
View File
@@ -0,0 +1,726 @@
/**
* qa-height-datum.mjs — height/vertical-datum numeric proof harness.
*
* Scaffold reused verbatim from qa-cctv-v2.mjs: the puppeteer launcher
* (Chrome executable discovery, headless flags), `QA_BASE_URL` env,
* `record()`/tally pattern, and the Google-Maps-key-injection dev-server
* recipe documented in its header comment. See that file for the reasoning
* behind the launch flags and the SwiftShader caveats they share.
*
* Unlike qa-cctv-v2 (frustum GEOMETRY / raycast-count invariants), this
* harness asserts NUMBERS: ground heights, altitudes, and terrain-provider
* identity. Every assertion is GL-independent (no pixel/screenshot
* comparisons) — it reads plain numbers off live Cesium entities and the
* app's own public module APIs, exactly like qa-cctv-v2's SERIALIZE_GEOM_SRC
* pattern.
*
* Three assertion groups (brief order):
*
* 1. CCTV ground correctness — for 5 London + 5 Austin + 3 SF cameras,
* assert each record's placed mount ground reads finitely, then compare
* its separately exposed `groundPriorM` against the
* authoritative Re:Earth ellipsoidal value fetched through the app's
* OWN `/api/terrain/heights` proxy (not a second upstream call — the
* proxy IS the oracle here, matching Task 2/3's contract). Keeping the
* prior separate is essential because Google-3D can legitimately refine
* the rendered ground to the photogrammetric mesh while the immutable
* Re:Earth prior remains the fallback/datum reference. The prior check is
* `|groundPriorM - reearthEllipsoid| < 6m`. Google-3D after
* the one-shot snap: London/Austin active-camera ground must fall in
* the brief's bands (tileset-vs-DEM legitimately differs by building
* height, so bands not exact-match).
*
* 2. Aircraft — sample >=10 live aircraft via the flights layer's public
* API. `renderAltitudeM` isn't exposed as a named field on any public
* method (only `flights.js`/`militaryFlights.js` module-private
* `_flightData` carries `geoAltitudeM`/`renderAltitudeM` directly), so
* this harness reconstructs both sides purely from public methods:
* - render height: `getAllPositions()[i].altitudeM`, which is
* `Cartographic.fromCartesian(billboard.position).height` — i.e.
* exactly the height passed to `Cartesian3.fromDegrees(lon, lat,
* renderAltitudeM)` at the placement site (flights.js ~:2100).
* - aviation/baro height: `getNearby()`/`findByQuery()`'s
* `altitudeM`, which is `info.altitude` (the UNTOUCHED sticky
* barometric field — flights.js ~:2111) — this is the "geoAltM ??
* baroAltM" input side, not the render output.
* This is the brief's documented fallback ("if geo/baro fields aren't
* exposed on the public state, assert the weaker but still-meaningful
* under-terrain + finite/plausible checks") PLUS a same-process,
* Node-side independent recomputation of `baroM + geoidHeight(lat,lon)`
* via a direct import of `src/data/geoid.js` (a pure module, imported
* the same way qa-cctv-v2.mjs imports `computeFrustumGeometry` from
* cctv.js) — giving a real numeric cross-check without reaching into
* the layer's private closure state.
*
* Under-terrain detector fingerprint (tightened): a real datum-miss
* renders an aircraft at the ~0m ellipsoidal SENTINEL (the pre-existing
* "no data yet" default) while its actual terrain ground is nowhere
* near 0 — that combination is unambiguous. A non-zero under-terrain
* reading is NOT hard-failed: a legitimately climbing/descending
* aircraft can transiently render close to real ground near an airport
* (e.g. a climb-out a few dozen metres above a field that itself sits
* well above sea level), which is correct behavior, not a bug. Those
* ambiguous cases are recorded INCONCLUSIVE with the raw numbers
* rather than FAILed.
*
* 3. Regime C re-resolve — switch the map stack to keyless OSM in-page via
* `mapStackController.setStack('osm')`, assert
* `viewer.terrainProvider` is NOT an `EllipsoidTerrainProvider`
* (constructor-name check, matching qa-cctv-v2's "borrow statics off a
* live instance's constructor" pattern — no `window.Cesium` global
* exists), and that CCTV grounds re-resolve within one event cycle
* (`gev:map-stack-changed` → cctv.js's regime re-arm — see its Task 5
* handler) by serializing a camera's ground height before/after the
* switch settles and asserting it moved OFF any old fabricated-prior
* value.
*
* Upstream-dependent assertions (live Re:Earth `/api/terrain/heights`, live
* OpenSky aircraft) are marked INCONCLUSIVE — not FAIL — when the upstream
* is unreachable this run, mirroring qa-cctv-v2's tiles-timeout handling.
* GL-dependent tile settling (the Google-3D one-shot ground snap actually
* completing under headless SwiftShader) is likewise inconclusive-on-timeout.
*
* Run: QA_BASE_URL=http://localhost:4300 node scripts/qa-height-datum.mjs
*
* Exit 0 = no hard failures. Non-zero = at least one hard FAIL (or harness error).
*/
import puppeteer from 'puppeteer';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { ensureGeoidReady, geoidHeight } from '../src/data/geoid.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
const argv = process.argv.slice(2);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const getFlag = (name) => argv.includes(name);
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
const APP_URL = getOpt('--url', BASE_URL);
const HEADFUL = getFlag('--headful');
const SHOTS_DIR = path.join(REPO_ROOT, 'qa-shots', 'height-datum-qa');
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try {
if (fs.existsSync(candidate)) return candidate;
} catch {
/* ignore */
}
}
return null;
}
const results = [];
function record(name, ok, detail) {
results.push({ name, ok, detail });
const tag = ok === null ? '\x1b[33mINCONCLUSIVE\x1b[0m' : ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/**
* Waits for the scene's 3D tileset to report tilesLoaded===true. Copied from
* qa-cctv-v2.mjs (see its header comment for why this must be re-awaited
* fresh before each action that depends on a REAL ground sample, not
* reused from an earlier wait).
* @param {import('puppeteer').Page} page
* @param {number} [timeoutMs=15000]
* @returns {Promise<boolean>}
*/
function waitForTilesLoaded(page, timeoutMs = 15000) {
return page.waitForFunction(
() => {
const scene = window.__godsEyeView.viewer.scene;
const prims = scene.primitives;
for (let i = 0; i < prims.length; i++) {
const p = prims.get(i);
if (p && p.constructor && p.constructor.name === 'Cesium3DTileset') {
return p.tilesLoaded === true;
}
}
return true;
},
{ timeout: timeoutMs }
).then(() => true).catch(() => false);
}
/**
* Reads a camera's placed mount-point cartographic height off its
* `cctv-<id>-ray-tl` polyline entity (first position = the mount, per
* cctv.js `buildCoverageEntities`/`applyFrustumGeometry`), returns
* `{ mountAltM, groundM }` where `groundM = mountAltM - mountHeightM` — the
* SAME arithmetic cctv.js itself uses in reverse
* (`mountAlt = ground + mountHeightM` in `computeFrustumGeometry`).
* @param {import('puppeteer').Page} page
* @param {string} camId
* @returns {Promise<{mountAltM:number, mountHeightM:number, groundM:number}|null>}
*/
async function readCameraGround(page, camId) {
return page.evaluate((id) => {
const viewer = window.__godsEyeView.viewer;
const time = viewer.clock.currentTime;
const ent = viewer.entities.getById(`cctv-${id}-ray-tl`);
if (!ent || !ent.polyline) return null;
const positions = ent.polyline.positions.getValue(time);
if (!Array.isArray(positions) || positions.length < 1) return null;
const mount = positions[0];
// No window.Cesium global (qa-cctv-v2 precedent) — borrow Cartographic's
// static fromCartesian off a live Cartographic instance's constructor.
const carto = viewer.scene.globe.ellipsoid.cartesianToCartographic(mount);
const mountAltM = carto.height;
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
const cam = mod.getUIState().cameras.find((c) => c.id === id);
const mountHeightM = cam ? cam.mountHeightM : null;
return {
mountAltM,
mountHeightM,
groundM: Number.isFinite(mountHeightM) ? mountAltM - mountHeightM : null,
};
}, camId);
}
/**
* Fetches the authoritative Re:Earth ellipsoidal ground height for a
* (lat, lon) pair through the APP'S OWN `/api/terrain/heights` proxy (the
* same oracle terrainHeights.js's `resolveEllipsoidalGround` uses) — this is
* the ground-truth reference for assertion group 1, fetched directly (not
* through in-page code) so a proxy hiccup can be told apart from an app bug.
* @param {number} lat
* @param {number} lon
* @returns {Promise<number|null>} ellipsoid height in metres, or null on failure.
*/
async function fetchReearthEllipsoid(lat, lon) {
const url = `${APP_URL}/api/terrain/heights?points=${encodeURIComponent(`${lon.toFixed(5)},${lat.toFixed(5)}`)}`;
try {
const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
if (!res.ok) return null;
const body = await res.json();
const v = Number(body?.results?.[0]?.ellipsoid);
return Number.isFinite(v) ? v : null;
} catch {
return null;
}
}
async function main() {
console.log(`\nHeight-Datum Fix Numeric Proof`);
console.log(` App URL : ${APP_URL}`);
console.log(` Mode : ${HEADFUL ? 'headful' : 'headless'}\n`);
try {
const res = await fetch(APP_URL, { method: 'GET' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
console.error(`\x1b[31mDev server not reachable at ${APP_URL} (${e.message}).\x1b[0m`);
process.exit(2);
}
fs.mkdirSync(SHOTS_DIR, { recursive: true });
// Node-side geoid grid, used only for the aircraft baro+N cross-check
// (independent of the app's own bundled copy — same package, separate
// process, so this is a real recomputation, not reading the app's cache).
await ensureGeoidReady();
const chromeExecutable = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
'--use-angle=swiftshader',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1280,800',
],
});
const consoleErrors = [];
let exitCode = 0;
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
page.on('console', (msg) => {
if (msg.type() === 'error') {
const t = msg.text();
if (!/Failed to load resource|net::ERR|status of 4\d\d|status of 5\d\d/.test(t)) {
consoleErrors.push(t);
}
}
});
console.log('Loading app...');
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() => window.__godsEyeView && window.__godsEyeView.viewer && window.__godsEyeView.dataManager,
{ timeout: 60000 }
);
await sleep(4000);
// =========================================================================
// Group 1: CCTV ground correctness
// =========================================================================
console.log('Enabling CCTV layer (full city-packs catalog)...');
await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
const entry = dm.layers.get('cctv');
if (!entry.enabled) await dm.toggle('cctv');
});
const camCount = await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('cctv').module.getUIState().count);
console.log(`CCTV catalog loaded: ${camCount} cameras. Waiting for the geometry-load queue to drain...`);
// The full-catalog drain is NOT a precondition for group 1's assertions
// below: every record renders correctly from `record.groundPrior` the
// instant the Re:Earth prior batch lands (Task 5 contract #1/#4 — "the
// prior IS the resolution" on globe stacks, and the fallback path in
// google-3d before a real sample resolves), which is exactly what makes
// the per-camera ground reads meaningful even mid-drain. The queue drain
// itself additionally attempts ONE REAL scene.sampleHeight per camera in
// google-3d regime (Task 5 contract #3/#5 — the ≤1×N invariant
// qa-cctv-v2 also locks), and with the full city-packs catalog
// size (800 cameras: 250 Austin + 300 Caltrans + 250 TfL — measured
// directly probing this harness's own dev server) that can take many
// minutes under headless SwiftShader (empirically ~2-6s/sample once
// real tile contention kicks in past the first few hundred). Waiting for
// FULL completion here would make routine harness runs impractically
// slow for no assertion gain, so this bounds the wait to a fixed,
// reasonable ceiling and reports the outcome as environmental/scale
// dependent (inconclusive, not a hard fail) rather than failing the
// whole run — mirroring qa-cctv-v2's own tiles-timeout handling.
const DRAIN_WAIT_CEILING_MS = 120000;
const drained = await page.waitForFunction(
() => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
const ui = mod.getUIState();
return ui.loading && ui.loading.active === false;
},
{ timeout: DRAIN_WAIT_CEILING_MS }
).then(() => true).catch(() => false);
record(`CCTV geometry-load queue drains within ${Math.round(DRAIN_WAIT_CEILING_MS / 1000)}s (N=${camCount})`,
drained ? true : null,
drained
? 'loading.active === false'
: `did not fully drain within the ceiling at N=${camCount} — environmental/scale-dependent under headless SwiftShader ` +
'(each un-drained camera still renders correctly from its Re:Earth prior; group 1 assertions below read that prior directly, so this does not block them)');
// Pull the full camera list, then bucket by city so we can pick 5
// London + 5 Austin + 3 SF regardless of catalog ordering. TfL tags
// `city: 'London'` and Austin's own pack tags `city: 'Austin'` exactly
// (vite.config.js); Caltrans tags `city` with an arbitrary upstream
// `nearbyPlace` string (NOT a stable "San Francisco" literal), so SF-area
// Caltrans cameras are identified by proximity to the SF anchor instead
// (same anchor vite.config.js's CALTRANS_ANCHORS uses for prioritization).
const allCameras = await page.evaluate(() => window.__godsEyeView.dataManager.layers.get('cctv').module.getUIState().cameras);
console.log(`Camera catalog: ${allCameras.length} total.`);
const SF_ANCHOR = { lat: 37.7793, lon: -122.4193 };
const haversineKm = (lat1, lon1, lat2, lon2) => {
const R = 6371;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};
const londonCams = allCameras.filter((c) => c.city === 'London').slice(0, 5);
const austinCams = allCameras.filter((c) => c.city === 'Austin').slice(0, 5);
const sfCams = allCameras
.filter((c) => c.provider === 'Caltrans' && haversineKm(c.lat, c.lon, SF_ANCHOR.lat, SF_ANCHOR.lon) < 60)
.slice(0, 3);
record('found >=5 London (TfL) test cameras', londonCams.length >= 5, `found ${londonCams.length}`);
record('found >=5 Austin test cameras', austinCams.length >= 5, `found ${austinCams.length}`);
record('found >=3 SF-area (Caltrans) test cameras', sfCams.length >= 3 ? true : null,
sfCams.length >= 3
? `found ${sfCams.length}`
: `found ${sfCams.length} — live Caltrans catalog unavailable/partial this run (upstream INCONCLUSIVE, not an app datum failure)`);
const cityBuckets = [
{ label: 'London', cams: londonCams },
{ label: 'Austin', cams: austinCams },
{ label: 'SF', cams: sfCams },
];
const groundRows = []; // for the report/console table
for (const { label, cams } of cityBuckets) {
for (const cam of cams) {
const state = await page.evaluate((id) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
return mod.getUIState().cameras.find((camera) => camera.id === id) || null;
}, cam.id);
const geom = await readCameraGround(page, cam.id);
if (!geom || !Number.isFinite(geom.groundM)) {
record(`${label} ${cam.id}: record ground reads finitely`, false, 'ray-tl entity missing or non-finite height');
continue;
}
record(`${label} ${cam.id}: rendered ground reads finitely`, true,
`renderedGround=${geom.groundM.toFixed(1)}m`);
const priorM = state?.groundPriorM;
if (!Number.isFinite(priorM)) {
record(`${label} ${cam.id}: Re:Earth ground prior resolved`, null,
`renderedGround=${geom.groundM.toFixed(1)}m; prior unavailable this run`);
groundRows.push({ label, id: cam.id, groundM: geom.groundM, priorM: null, reearthM: null });
continue;
}
const reearth = await fetchReearthEllipsoid(cam.lat, cam.lon);
if (reearth === null) {
record(`${label} ${cam.id}: |ground prior - Re:Earth ellipsoid| < 6m`, null,
`Re:Earth proxy unreachable this run — prior=${priorM.toFixed(1)}m rendered=${geom.groundM.toFixed(1)}m (upstream INCONCLUSIVE, not a harness/app failure)`);
groundRows.push({ label, id: cam.id, groundM: geom.groundM, priorM, reearthM: null });
continue;
}
const delta = Math.abs(priorM - reearth);
record(`${label} ${cam.id}: |ground prior - Re:Earth ellipsoid| < 6m`, delta < 6,
`prior=${priorM.toFixed(1)}m rendered=${geom.groundM.toFixed(1)}m reearth=${reearth.toFixed(1)}m Δ=${delta.toFixed(1)}m`);
groundRows.push({ label, id: cam.id, groundM: geom.groundM, priorM, reearthM: reearth });
}
}
console.log('\n Ground-height table (city, camera, rendered ground, stored prior, Re:Earth ellipsoid):');
for (const row of groundRows) {
console.log(` ${row.label.padEnd(8)} ${row.id.padEnd(24)} rendered=${row.groundM.toFixed(1)}m` +
(row.priorM !== null ? ` prior=${row.priorM.toFixed(1)}m` : ' prior=N/A') +
(row.reearthM !== null ? ` reearth=${row.reearthM.toFixed(1)}m` : ' reearth=N/A'));
}
console.log('');
// -----------------------------------------------------------------------
// Google-3D band check: activate the London and Austin cameras nearest
// the viewer (mirrors qa-cctv-v2's focusNearest activation pattern),
// wait for the one-shot ground snap to complete (tilesLoaded gate), then
// assert the ACTIVE camera's ground falls in the brief's band. Bands
// (not exact) because the Google tileset surface legitimately differs
// from the Re:Earth DEM by building height once the snap refines it.
// -----------------------------------------------------------------------
console.log('Checking Google-3D active-camera ground BANDS (post-snap)...');
async function checkActiveCameraBand(cityLabel, targetCamId, band) {
await page.evaluate((id) => {
window.__godsEyeView.dataManager.layers.get('cctv').module.selectCamera(id, { focus: true, durationSec: 0.1 });
}, targetCamId);
await sleep(600);
const tilesReady = await waitForTilesLoaded(page, 30000);
// Give the one-shot completion pass a beat to land even after tiles
// report ready (update() ticks on its own interval — see cctv.js).
await sleep(1500);
const geom = await readCameraGround(page, targetCamId);
if (!tilesReady) {
record(`${cityLabel} active camera (${targetCamId}) google-3d ground ∈ [${band[0]},${band[1]}]m`, null,
`tiles never reported loaded within 30s under headless GL — environmental, inconclusive (recordGround=${geom?.groundM?.toFixed?.(1) ?? 'n/a'}m)`);
return;
}
if (!geom || !Number.isFinite(geom.groundM)) {
record(`${cityLabel} active camera (${targetCamId}) google-3d ground ∈ [${band[0]},${band[1]}]m`, false, 'ground unreadable');
return;
}
const inBand = geom.groundM >= band[0] && geom.groundM <= band[1];
record(`${cityLabel} active camera (${targetCamId}) google-3d ground ∈ [${band[0]},${band[1]}]m`, inBand,
`recordGround=${geom.groundM.toFixed(1)}m`);
}
if (londonCams.length) {
await checkActiveCameraBand('London', londonCams[0].id, [45, 75]);
} else {
record('London active camera google-3d ground ∈ [45,75]m', null, 'no London camera available to test');
}
if (austinCams.length) {
await checkActiveCameraBand('Austin', austinCams[0].id, [110, 135]);
} else {
record('Austin active camera google-3d ground ∈ [110,135]m', null, 'no Austin camera available to test');
}
// =========================================================================
// Group 2: Aircraft render altitude
// =========================================================================
console.log('Enabling flights layer, waiting for a live aircraft sample...');
await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
const entry = dm.layers.get('flights');
if (!entry.enabled) await dm.toggle('flights');
});
// OpenSky polls on its own interval; give it real time to land a batch
// (the layer polls every ~30s per docs/CURRENT-STATE.md).
const gotAircraft = await page.waitForFunction(
() => {
const mod = window.__godsEyeView.dataManager.layers.get('flights').module;
return mod.getAllPositions(1).length > 0;
},
{ timeout: 45000 }
).then(() => true).catch(() => false);
if (!gotAircraft) {
record('>=10 live aircraft sampled from OpenSky', null,
'no aircraft appeared within 45s — OpenSky upstream INCONCLUSIVE this run (not an app failure)');
} else {
// Let a full second poll cycle land (updateInterval: 30000 — see
// flights.js) before sampling. A just-appeared ON-GROUND aircraft can
// legitimately render at the sticky `alt` default (0m ellipsoidal) for
// ONE tick while `_warmGroundedAircraftSurfaceCache`'s batch resolve is
// still in flight (fire-and-forget; picked up by cachedEllipsoidalGround
// on a LATER poll — see flights.js's own comment on that function). That
// window is a documented, time-bounded product behavior, not a
// datum-fix regression — sampling immediately after the FIRST aircraft
// appears would catch exactly that transient and misreport it as an
// under-terrain violation. Waiting past a second poll interval lets the
// cache warm before the under-terrain detector runs.
console.log('Waiting through a second poll cycle so on-ground aircraft ground-cache warm-up completes...');
await sleep(32000);
// getAllPositions gives render-height (carto.height off the actual
// billboard Cartesian3 — i.e. renderAltitudeM as placed by
// Cartesian3.fromDegrees(lon, lat, renderAltitudeM)); getNearby gives
// the untouched aviation/baro field (info.altitude) per-icao24. Cross
// the two public APIs by icao24/id to reconstruct both sides without
// reaching into the layer's private _flightData closure.
const allPositions = await page.evaluate(() => {
const mod = window.__godsEyeView.dataManager.layers.get('flights').module;
return mod.getAllPositions(60);
});
// getNearby needs an ECEF center + range; use the live camera position
// (a real Cartesian3 instance — no window.Cesium global available, so
// this borrows a live instance rather than constructing one, matching
// qa-cctv-v2's pattern) with an effectively unbounded range so it
// returns everything currently shown regardless of viewer position.
const nearbyList = await page.evaluate(() => {
const mod = window.__godsEyeView.dataManager.layers.get('flights').module;
const center = window.__godsEyeView.viewer.camera.position;
return mod.getNearby(center, Number.MAX_VALUE, 200).map((a) => ({ icao24: a.icao24, altitudeM: a.altitudeM }));
});
const baroById = new Map(nearbyList.map((a) => [a.icao24, a.altitudeM]));
const sampleSize = Math.min(allPositions.length, 25);
const sample = allPositions.slice(0, sampleSize);
record(`>=10 live aircraft sampled from OpenSky (public getAllPositions/getNearby)`,
sample.length >= 10 ? true : (sample.length > 0 ? null : false),
`sampled ${sample.length} (getAllPositions total=${allPositions.length})`);
let plausibleCount = 0;
let checkedCount = 0;
const underTerrainDetails = [];
const baroPlusNDeltas = [];
for (const ac of sample) {
const renderAltM = ac.altitudeM; // carto.height off the live billboard position
const baroM = baroById.get(ac.id);
// Finite + plausible sanity: render altitude must be a real number in
// a physically sane band (-500m .. 20000m covers everything from
// Death-Valley-adjacent ground traffic to high-altitude cruise).
const finitePlausible = Number.isFinite(renderAltM) && renderAltM > -500 && renderAltM < 20000;
if (finitePlausible) plausibleCount += 1;
// Cross-check vs baro+geoidHeight when we have the baro side (weaker
// per-aircraft equality check: geoAltitudeM isn't exposed as a named
// field on any public flights-layer method — only the module-private
// `_flightData` carries geoAltitudeM/renderAltitudeM directly — so
// this harness can only recompute the FALLBACK branch's expected
// value (baro + geoid N) from the public API and log the delta,
// rather than assert it individually, for aircraft where the true
// render source was actually geo_altitude, which can legitimately
// differ from baro+N by tens of metres).
if (Number.isFinite(baroM) && Number.isFinite(renderAltM)) {
const n = geoidHeight(ac.latitude, ac.longitude);
const expectedBaroPlusN = baroM + n;
const deltaVsBaroPlusN = Math.abs(renderAltM - expectedBaroPlusN);
// Not asserted individually (geo_altitude legitimately diverges from
// baro+N) — logged into the report table so the cross-check is
// actually visible, not silently discarded.
baroPlusNDeltas.push({ id: ac.id, renderAltM, expectedBaroPlusN, deltaVsBaroPlusN });
}
checkedCount += 1;
}
record(`aircraft render altitudes are finite + physically plausible (-500..20000m)`,
checkedCount > 0 ? plausibleCount === checkedCount : null,
`${plausibleCount}/${checkedCount} plausible`);
if (baroPlusNDeltas.length) {
console.log('\n baro+geoidN cross-check table (id, renderAlt, expected baro+N, |Δ|):');
for (const d of baroPlusNDeltas) {
console.log(` ${String(d.id).padEnd(10)} renderAlt=${d.renderAltM.toFixed(1)}m` +
` expected=${d.expectedBaroPlusN.toFixed(1)}m Δ=${d.deltaVsBaroPlusN.toFixed(1)}m`);
}
console.log('');
}
// Under-terrain detector, tightened to its unambiguous fingerprint.
// Ground truth is fetched fresh through the app's own
// /api/terrain/heights proxy (same oracle as group 1), so this stays
// GL-independent and upstream-verifiable rather than reaching into
// private closures.
//
// The REAL bug (fixed by this task's Bug 1 change: flights.js's
// _warmGroundedAircraftSurfaceCache now keys its warm-cache resolve off
// the aircraft's RAW POLL lat/lon — the same coords
// pickRenderAltitudeM's surfaceM read uses — instead of the
// continuously dead-reckoned billboard position) has an unambiguous
// numeric fingerprint: an aircraft rendering at the ~0m ellipsoidal
// SENTINEL (the pre-existing "no geo/baro data yet" default) while its
// terrain ground is nowhere near 0m. HARD-FAIL only on that exact
// combination.
//
// Any OTHER under-terrain reading (non-zero renderAltitudeM below
// ground - 50m) is ambiguous — it could legitimately be a climbing/
// descending aircraft briefly near real ground elevation close to an
// airport (e.g. a departure a few dozen metres up over a field that
// itself sits well above sea level) — so those are recorded
// INCONCLUSIVE with the raw values, not FAILed.
const SENTINEL_EPS_M = 1; // Math.abs(renderAlt) < 1 is the exact 0m default
const SENTINEL_GROUND_FLOOR_M = 100; // terrain must be unambiguously non-zero
let underTerrainChecked = 0;
let sentinelViolations = 0;
let ambiguousUnderTerrain = 0;
const ambiguousDetails = [];
for (const gc of sample) {
const ground = await fetchReearthEllipsoid(gc.latitude, gc.longitude);
if (ground === null) continue; // upstream unreachable for this point — skip, don't fail
underTerrainChecked += 1;
if (gc.altitudeM >= ground - 50) continue; // not under terrain at all
const isSentinelMiss = Math.abs(gc.altitudeM) < SENTINEL_EPS_M && Math.abs(ground) > SENTINEL_GROUND_FLOOR_M;
if (isSentinelMiss) {
sentinelViolations += 1;
underTerrainDetails.push(`${gc.id}: renderAlt=${gc.altitudeM.toFixed(1)}m ground=${ground.toFixed(1)}m (0m SENTINEL fingerprint)`);
} else {
ambiguousUnderTerrain += 1;
ambiguousDetails.push(`${gc.id}: renderAlt=${gc.altitudeM.toFixed(1)}m ground=${ground.toFixed(1)}m`);
}
}
record('no aircraft renders at the 0m sentinel while terrain ground is far from 0m (datum-miss fingerprint)',
underTerrainChecked > 0 ? sentinelViolations === 0 : null,
underTerrainChecked > 0
? `${sentinelViolations} violation(s) out of ${underTerrainChecked} checked${underTerrainDetails.length ? ': ' + underTerrainDetails.slice(0, 3).join(' | ') : ''}`
: 'Re:Earth proxy unreachable for every sampled point this run — INCONCLUSIVE');
if (ambiguousUnderTerrain > 0) {
record('other under-terrain readings (non-zero, ambiguous — may be legit climb/descent near an airport)',
null,
`${ambiguousUnderTerrain} case(s): ${ambiguousDetails.slice(0, 5).join(' | ')}`);
}
}
// =========================================================================
// Group 3: Regime C re-resolve
// =========================================================================
console.log('Switching map stack to keyless OSM (regime C)...');
// Grab a London camera's ground BEFORE the switch (still whatever regime
// we were in — Google-3D, given the app's default).
const regimeCTargetId = londonCams[0]?.id || austinCams[0]?.id || allCameras[0]?.id;
const groundBeforeSwitch = regimeCTargetId ? await readCameraGround(page, regimeCTargetId) : null;
await page.evaluate(async () => {
await window.__godsEyeView.mapStackController.setStack('osm');
});
// Real (non-flat) terrain loads ASYNCHRONOUSLY on a globe stack, and by two
// different code paths depending on whether a Cesium Ion token is present:
// - NO token → keyless Re:Earth `CesiumTerrainProvider` via a DIRECT
// assignment (synchronous — ready immediately).
// - token → Cesium World Terrain via `scene.setTerrain(...)`, whose
// provider resolves in the background: `viewer.terrainProvider`
// is transiently `null` until the World Terrain layer.json
// loads (can exceed 1.5 s under SwiftShader), THEN becomes a
// `CesiumTerrainProvider`.
// Either way the END STATE is a real `CesiumTerrainProvider`, never the flat
// `EllipsoidTerrainProvider` regime C used to be stuck on. Poll for that end
// state instead of a fixed sleep (a fixed sleep races the async World Terrain
// load — the old failure mode). Report whatever it settled on.
let terrainCtor = null;
try {
await page.waitForFunction(() => {
const p = window.__godsEyeView?.viewer?.terrainProvider;
const name = p && p.constructor && p.constructor.name;
return !!name && name !== 'EllipsoidTerrainProvider';
}, { timeout: 12000, polling: 250 });
} catch { /* timed out — fall through and report the settled ctor below */ }
terrainCtor = await page.evaluate(() => {
const provider = window.__godsEyeView.viewer.terrainProvider;
return provider?.constructor?.name || null;
});
record('globe-stack terrain is a real CesiumTerrainProvider, not the flat EllipsoidTerrainProvider (regime B/C)',
terrainCtor !== null && terrainCtor !== 'EllipsoidTerrainProvider',
`constructor=${terrainCtor}`);
if (regimeCTargetId) {
const groundAfterSwitch = await readCameraGround(page, regimeCTargetId);
if (!groundBeforeSwitch || !groundAfterSwitch ||
!Number.isFinite(groundBeforeSwitch.groundM) || !Number.isFinite(groundAfterSwitch.groundM)) {
record(`CCTV grounds re-resolved for regime C (${regimeCTargetId})`, false, 'ground unreadable before or after switch');
} else {
// The prior-everywhere invariant (Task 5) means BOTH readings should
// already be close to the Re:Earth ellipsoid value in every regime —
// the real regression this guards is the OLD fabricated-prior bug
// (London ~15m, Austin ~150m fixed catalog numbers regardless of
// regime). Assert the post-switch ground is NOT stuck at either
// known-bad fabricated constant, and is within a generous tolerance
// of the pre-switch value (both should already reflect the same
// Re:Earth prior — a big jump would indicate a stale/blank regime
// record instead of a re-arm).
const isLondonId = londonCams.some((c) => c.id === regimeCTargetId);
const staleValue = isLondonId ? 15 : 150;
const notStuckAtFabricated = Math.abs(groundAfterSwitch.groundM - staleValue) > 5;
const stableAcrossSwitch = Math.abs(groundAfterSwitch.groundM - groundBeforeSwitch.groundM) < 20;
record(`CCTV ground NOT stuck at the old fabricated prior (${staleValue}m) after regime C switch (${regimeCTargetId})`,
notStuckAtFabricated,
`before=${groundBeforeSwitch.groundM.toFixed(1)}m after=${groundAfterSwitch.groundM.toFixed(1)}m`);
record(`CCTV ground re-resolved within one event cycle, consistent with the prior-everywhere invariant (${regimeCTargetId})`,
stableAcrossSwitch,
`Δ=${Math.abs(groundAfterSwitch.groundM - groundBeforeSwitch.groundM).toFixed(1)}m`);
}
} else {
record('CCTV grounds re-resolved for regime C', null, 'no test camera available');
}
// -----------------------------------------------------------------------
// Screenshot for human visual review (not a pass/fail gate).
// -----------------------------------------------------------------------
await sleep(500);
const shot = path.join(SHOTS_DIR, 'height-datum-regimeC.png');
await page.screenshot({ path: shot });
console.log(` screenshot (visual review only, not a gate) → ${path.relative(REPO_ROOT, shot)}`);
record('no console errors during height-datum exercise', consoleErrors.length === 0,
consoleErrors.length ? consoleErrors.slice(0, 3).join(' | ') : 'clean');
for (const r of results) {
if (r.ok === false) exitCode = 1;
}
} finally {
await browser.close();
}
console.log('\n' + '─'.repeat(60));
const pass = results.filter((r) => r.ok === true).length;
const fail = results.filter((r) => r.ok === false).length;
const inconclusive = results.filter((r) => r.ok === null).length;
console.log(` RESULT: ${pass} passed, ${fail} failed, ${inconclusive} inconclusive`);
console.log('─'.repeat(60) + '\n');
process.exit(exitCode);
}
main().catch((e) => {
console.error('\x1b[31mHarness error:\x1b[0m', e);
process.exit(3);
});
File diff suppressed because it is too large. Load diff
+422
View File
@@ -0,0 +1,422 @@
#!/usr/bin/env node
/**
* qa-label-readability.mjs — before/after evidence capture for ambient label
* legibility over bright ground.
*
* Run:
* node scripts/qa-label-readability.mjs --url http://localhost:4244 --tag after
*
* Every scene pins its own camera pose AND substitutes a deterministic
* synthetic contact field for the live pollers, so a `before` run and an
* `after` run produce pixel-comparable frames: same contacts, same callsigns,
* same placements, same imagery. The only difference between the two tags is
* the code under test.
*
* Scenes cover the cases the readability spec calls out:
* bright-* — sunlit aerial imagery, one shot per visual style
* dark-* — dark water/terrain, where a plate must NOT turn into a box
* sats-* — space-tier contacts over the lit Earth disc
* horizon-* — cockpit pose at the horizon, contacts against SKY, where the
* plate must feather back toward the bare-text look
* band-* — the horizon crossing the middle of the label field, so the
* feather can be judged for smoothness rather than popping
*
* Each shot also reports a luminance histogram of the label band, so the
* "did the plate actually darken anything" question has a number behind it and
* not only an eyeball.
*/
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:4244');
const TAG = getOpt('--tag', 'after');
/** Comma-separated scene-id prefixes. Empty runs the whole set. */
const SCENE_FILTER = getOpt('--scenes', '')
.split(',')
.map((token) => token.trim())
.filter(Boolean);
const SHOT_DIR = path.resolve(getOpt('--out', 'qa-shots/labels'));
const SETTLE_MAX_MS = 40_000;
const SETTLE_MIN_MS = 4_000;
/**
* Scenes are (camera pose × visual style). `field` describes the synthetic
* contact cloud dropped around the camera target.
*/
/** Sunlit airport apron — the brightest ground the product routinely flies over. */
const APRON = { lon: -97.6664, lat: 30.1975, height: 1100, heading: 0.35, pitch: -0.85 };
/**
* Cockpit pose: cruise altitude, level, looking out at the horizon. From 10 km
* the ellipsoid silhouette sits ~3.2° below the local horizontal, so a level
* camera puts the horizon line just under frame centre and everything the
* `horizon-*` field lands on is open sky.
*/
const COCKPIT = { lon: -98.35, lat: 30.05, height: 10_000, heading: 1.42, pitch: 0 };
/** Contacts biased into the upper frame: every one of them backs onto sky. */
const SKY_FIELD = {
kind: 'air', count: 30, near: 40_000, far: 120_000, spanY: 0.9, biasY: 0.3,
};
const SCENES = [
{
id: 'bright-normal',
style: 'normal',
note: 'sunlit apron concrete — the failure case from the field report',
camera: APRON,
field: { kind: 'air', count: 30, near: 700, far: 2600 },
},
{
id: 'bright-retro',
style: 'retro',
note: 'CRT amber over the same sunlit ground',
camera: APRON,
field: { kind: 'air', count: 30, near: 700, far: 2600 },
},
{
id: 'bright-surveillance',
style: 'surveillance',
note: 'NVG green over the same sunlit ground',
camera: APRON,
field: { kind: 'air', count: 30, near: 700, far: 2600 },
},
{
id: 'bright-thermal',
style: 'thermal',
note: 'FLIR over the same sunlit ground',
camera: APRON,
field: { kind: 'air', count: 30, near: 700, far: 2600 },
},
{
id: 'dark-normal',
style: 'normal',
note: 'dark water — a plate here must stay a whisper, not a box',
camera: { lon: 139.05, lat: 34.45, height: 9000, heading: 0.2, pitch: -0.62 },
field: { kind: 'air', count: 30, near: 6000, far: 26_000 },
},
{
id: 'sats-normal',
style: 'normal',
note: 'space tier over the lit Earth disc',
camera: { lon: -60, lat: 20, height: 21_000_000, heading: 0, pitch: -Math.PI / 2 },
field: { kind: 'sat', count: 30, near: 14_000_000, far: 20_000_000 },
},
{
id: 'horizon-normal',
style: 'normal',
note: 'cockpit at the horizon — the sky backdrop the plate must feather off',
camera: COCKPIT,
field: SKY_FIELD,
},
{
id: 'horizon-retro',
style: 'retro',
note: 'CRT amber against the same sky',
camera: COCKPIT,
field: SKY_FIELD,
},
{
id: 'horizon-surveillance',
style: 'surveillance',
note: 'NVG green against the same sky',
camera: COCKPIT,
field: SKY_FIELD,
},
{
id: 'horizon-thermal',
style: 'thermal',
note: 'FLIR against the same sky',
camera: COCKPIT,
field: SKY_FIELD,
},
{
id: 'band-normal',
style: 'normal',
note: 'horizon crossing the label field — feather smoothness, both sides in one frame',
camera: COCKPIT,
// Shallower depths keep the low contacts above ground rather than inside it,
// so the frame carries genuine terrain-backed AND sky-backed labels.
field: {
kind: 'air', count: 30, near: 15_000, far: 60_000, spanY: 1.0, biasY: -0.1,
},
// The band scene's contacts sit lower in the frame than the default crop.
crop: { x: 0.16, y: 0.28, w: 0.52, h: 0.54 },
},
];
const CHROME_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Pin puppeteer's Chrome-for-Testing: system Chrome auto-updates underneath
// the harnesses and its software-GL behaviour shifts across majors.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
].filter(Boolean);
function findChrome() {
for (const candidate of CHROME_CANDIDATES) {
try { if (fs.existsSync(candidate)) return candidate; } catch { /* fall through */ }
}
return null;
}
/** Decode a base64 PNG inside the page and return luminance statistics. */
async function measureShot(page, base64) {
return page.evaluate(async (b64) => {
const image = new Image();
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
image.src = `data:image/png;base64,${b64}`;
});
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
// Sample the central band only: the vignette and the fixed chrome rails
// would otherwise dominate a whole-frame histogram.
const x0 = Math.round(image.width * 0.2);
const x1 = Math.round(image.width * 0.8);
const y0 = Math.round(image.height * 0.15);
const y1 = Math.round(image.height * 0.8);
const data = ctx.getImageData(x0, y0, x1 - x0, y1 - y0).data;
const values = [];
for (let i = 0; i < data.length; i += 4) {
values.push(data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114);
}
values.sort((a, b) => a - b);
const at = (q) => Math.round(values[Math.min(values.length - 1, Math.floor(values.length * q))]);
return { p05: at(0.05), p25: at(0.25), p50: at(0.5), p95: at(0.95), samples: values.length };
}, base64);
}
/** Wait until consecutive frames stop changing, so tiles are fully resolved. */
async function settle(page) {
const started = Date.now();
let previous = null;
while (Date.now() - started < SETTLE_MAX_MS) {
await new Promise((resolve) => setTimeout(resolve, 1500));
const state = await page.evaluate(() => {
const { viewer, tileset } = window.__godsEyeView;
return {
tilesLoaded: tileset?.tilesLoaded !== false,
pending: viewer.scene.globe?._surface?._tileLoadQueueHigh?.length ?? 0,
};
});
const settled = state.tilesLoaded && state.pending === 0;
if (settled && previous && Date.now() - started > SETTLE_MIN_MS) return true;
previous = settled;
}
return false;
}
async function main() {
console.log('\nLabel readability evidence');
console.log(` App URL : ${APP_URL}`);
console.log(` Tag : ${TAG}`);
console.log(` Output : ${SHOT_DIR}\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 = findChrome();
const browser = await puppeteer.launch({
headless: 'new',
...(executablePath ? { executablePath } : {}),
args: [
'--no-sandbox', '--disable-setuid-sandbox',
'--use-gl=angle', '--use-angle=swiftshader',
'--disable-dev-shm-usage', '--disable-web-security',
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
'--window-size=1280,800',
],
});
const results = [];
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
page.on('pageerror', (error) => console.warn(` ! pageerror: ${error.message}`));
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.styleManager,
{ timeout: 60_000, polling: 100 },
);
// flyToAustin arrives ~500 ms after init; let it start and land before any
// scene sets its own pose, or the arrival overwrites the first camera.
await new Promise((resolve) => setTimeout(resolve, 2500));
const selected = SCENE_FILTER.length
? SCENES.filter((scene) => SCENE_FILTER.some((prefix) => scene.id.startsWith(prefix)))
: SCENES;
if (selected.length === 0) throw new Error(`--scenes matched nothing: ${SCENE_FILTER}`);
for (const scene of selected) {
process.stdout.write(` ${scene.id} … `);
await page.evaluate((spec) => {
const { viewer, dataManager, styleManager } = window.__godsEyeView;
viewer.camera.cancelFlight();
const Cartesian3 = viewer.camera.position.constructor;
viewer.camera.setView({
destination: Cartesian3.fromDegrees(
spec.camera.lon, spec.camera.lat, spec.camera.height,
),
orientation: {
heading: spec.camera.heading,
pitch: spec.camera.pitch,
roll: 0,
},
});
// Contacts are placed by SCREEN position rather than by geography: a
// pitched camera looks well ahead of its own ground point, so a cloud
// centred on the camera coordinate mostly lands behind the frustum and
// the frame comes back with two labels instead of a field. Building
// straight off the camera basis guarantees every contact lands inside
// the keyhole, at a known depth, in both the before and after runs.
const camera = viewer.camera;
const frustum = camera.frustum;
const halfHeight = Math.tan((frustum.fovy ?? frustum.fov ?? 1.0) / 2);
const halfWidth = halfHeight * (frustum.aspectRatio || 1.6);
const basis = {
px: camera.position.x, py: camera.position.y, pz: camera.position.z,
dx: camera.direction.x, dy: camera.direction.y, dz: camera.direction.z,
ux: camera.up.x, uy: camera.up.y, uz: camera.up.z,
rx: camera.right.x, ry: camera.right.y, rz: camera.right.z,
};
// A 6x5 jittered lattice over the central 76% of the frame. Deterministic
// by construction — no RNG anywhere in the harness.
const pointAt = (ndcX, ndcY, distance) => {
const sx = ndcX * halfWidth * distance;
const sy = ndcY * halfHeight * distance;
return new Cartesian3(
basis.px + basis.dx * distance + basis.rx * sx + basis.ux * sy,
basis.py + basis.dy * distance + basis.ry * sx + basis.uy * sy,
basis.pz + basis.dz * distance + basis.rz * sx + basis.uz * sy,
);
};
const build = (prefix) => {
const rows = [];
const cols = 6;
const isSat = spec.field.kind === 'sat';
for (let i = 0; i < spec.field.count; i++) {
const col = i % cols;
const row = Math.floor(i / cols);
const jitter = ((i * 37) % 11) / 11 - 0.5;
// spanY/biasY let a scene aim the lattice at sky, at ground, or
// straight across the horizon line without moving the camera.
const spanX = spec.field.spanX ?? 1.52;
const spanY = spec.field.spanY ?? 1.34;
const biasY = spec.field.biasY ?? 0;
const ndcX = ((col + 0.5) / cols - 0.5) * spanX + jitter * 0.06;
const ndcY = ((row + 0.5) / Math.ceil(spec.field.count / cols) - 0.5)
* spanY + biasY + jitter * 0.05;
const depth = spec.field.near
+ (spec.field.far - spec.field.near) * (((i * 53) % 17) / 17);
const flightLevel = 20 + ((i * 29) % 380);
rows.push({
sourceId: `${prefix}-${String(i).padStart(3, '0')}`,
id: isSat ? `SAT-${String(1000 + i)}` : `JA${String(20000 + i * 7)}`,
metric: isSat ? 'LEO' : `FL${String(flightLevel).padStart(3, '0')}`,
position: pointAt(ndcX, ndcY, depth),
type: isSat ? 'SAT' : 'AIR',
tier: isSat ? 'space' : (i % 6 === 0 ? 'military' : 'civil'),
});
}
return rows;
};
const assignment = spec.field.kind === 'sat'
? { satellites: build('s'), flights: [], military: [] }
: { flights: build('f'), military: [], satellites: [] };
window.__LABEL_EVIDENCE = assignment;
for (const layerId of Object.keys(assignment)) {
const entry = dataManager.layers.get(layerId);
if (entry?.module) {
entry.module.getDetectableObjects = () => window.__LABEL_EVIDENCE[layerId];
}
}
styleManager.setStyle(spec.style, { applyPreset: false });
// Set detection AFTER the style: military presets auto-arm Panoptic at
// 0% density, which would otherwise leave the frame without callouts.
styleManager.setDetection({
enabled: true,
densityPct: 100,
allocationStrategy: 'elastic',
});
}, scene);
const converged = await settle(page);
const base64 = await page.screenshot({ encoding: 'base64' });
fs.writeFileSync(
path.join(SHOT_DIR, `${TAG}-${scene.id}.png`),
Buffer.from(base64, 'base64'),
);
// A 2x blow-up of the label field. Ambient callouts are 10px mono, and
// the taste call this evidence exists for cannot be made at 1:1.
const crop = await page.evaluate(async ({ b64, box }) => {
const image = new Image();
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
image.src = `data:image/png;base64,${b64}`;
});
const sx = Math.round(image.width * box.x);
const sy = Math.round(image.height * box.y);
const sw = Math.round(image.width * box.w);
const sh = Math.round(image.height * box.h);
const canvas = document.createElement('canvas');
canvas.width = sw * 2;
canvas.height = sh * 2;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
ctx.drawImage(image, sx, sy, sw, sh, 0, 0, sw * 2, sh * 2);
return canvas.toDataURL('image/png').split(',')[1];
}, { b64: base64, box: scene.crop ?? { x: 0.16, y: 0.14, w: 0.52, h: 0.5 } });
fs.writeFileSync(
path.join(SHOT_DIR, `${TAG}-${scene.id}-crop.png`),
Buffer.from(crop, 'base64'),
);
const stats = await measureShot(page, base64);
const diagnostics = await page.evaluate(
() => window.__godsEyeView.styleManager.getDetectionState?.() || {},
);
results.push({ scene: scene.id, converged, ...stats });
console.log(
`p05=${stats.p05} p25=${stats.p25} p50=${stats.p50} p95=${stats.p95}`
+ `${converged ? '' : ' (tiles did not fully settle)'}`
+ ` [${diagnostics.detectionMode || '?'}]`,
);
}
} finally {
await browser.close();
}
const summary = path.join(SHOT_DIR, `${TAG}-summary.json`);
fs.writeFileSync(summary, `${JSON.stringify(results, null, 2)}\n`);
console.log(`\n ${results.length} shots → ${SHOT_DIR}`);
console.log(` summary → ${summary}\n`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
+600
View File
@@ -0,0 +1,600 @@
#!/usr/bin/env node
/**
* Deterministic QA for the detection overlay's bounded two-lane label pipeline.
*
* Run:
* node scripts/qa-labels.mjs --url http://localhost:4173
*
* The harness replaces only the three detection read APIs in the loaded page.
* It does not enable their network pollers or mutate repository data. The field
* contains 12,000 stable observations and is sampled while the global camera
* makes a small orbit.
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
const argv = process.argv.slice(2);
const getFlag = (name) => argv.includes(name);
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 HEADFUL = getFlag('--headful');
const SHOT_DIR = path.resolve('qa-shots/labels');
const STORAGE_KEY = 'gev:detection-allocation:v1';
const OBSERVATION_COUNT = 12000;
const SYNTHETIC_SAMPLE_MS = 10000;
const NORMAL_SAMPLE_MS = 6000;
const SAMPLE_TIMEOUT_MS = 120000;
const MIN_SYNTHETIC_SOLVES = 20;
const MIN_SYNTHETIC_SPAN_MS = SYNTHETIC_SAMPLE_MS;
const MIN_NORMAL_FRAMES = 20;
const SETTLED_SOLVE_COUNT = 3;
const GLOBAL_CAMERA_MIN_HEIGHT_M = 20_000_000;
const GLOBAL_CAMERA_MAX_HEIGHT_M = 30_000_000;
const FIELD_COUNTS = Object.freeze({
flights: 7200,
military: 1000,
satellites: 3800,
});
const NORMAL_COUNTS = Object.freeze({
flights: 4300,
military: 70,
satellites: 830,
});
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try {
if (fs.existsSync(candidate)) return candidate;
} catch {
// Let Puppeteer use its bundled browser.
}
}
return null;
}
const results = [];
function record(name, ok, detail) {
results.push({ name, ok, detail });
const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
function percentile(values, pct) {
const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
if (sorted.length === 0) return Number.NaN;
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * pct) - 1)];
}
function parseJson(value, fallback) {
try {
return JSON.parse(value);
} catch {
return fallback;
}
}
function analyzeMembership(samples) {
let replacements = 0;
let exposure = 0;
const transitions = new Map();
for (let index = 0; index < samples.length; index++) {
const sample = samples[index];
const current = new Set(sample.labeledKeys);
if (index > 0) {
const previous = new Set(samples[index - 1].labeledKeys);
const removed = [...previous].filter((key) => !current.has(key)).length;
const added = [...current].filter((key) => !previous.has(key)).length;
replacements += Math.max(removed, added);
const elapsedSec = Math.max(0.001, (sample.t - samples[index - 1].t) / 1000);
exposure += Math.max(1, (previous.size + current.size) / 2) * elapsedSec;
}
for (const key of current) {
if (!transitions.has(key)) transitions.set(key, []);
transitions.get(key).push(sample.t);
}
}
let showHideShow = 0;
for (const times of transitions.values()) {
for (let index = 1; index < times.length; index++) {
const previousIndex = samples.findIndex((sample) => sample.t === times[index - 1]);
const nextIndex = samples.findIndex((sample) => sample.t === times[index]);
if (nextIndex - previousIndex > 1 && times[index] - times[index - 1] <= 2000) {
showHideShow++;
break;
}
}
}
const churnPctPerSec = exposure > 0
? replacements / exposure * 100
: 0;
return {
churnPctPerSec,
showHideShow,
replacements,
exposure,
transitions: Math.max(0, samples.length - 1),
minimumPopulation: samples.length
? Math.min(...samples.map((sample) => sample.labeledKeys.length))
: 0,
maximumPopulation: samples.length
? Math.max(...samples.map((sample) => sample.labeledKeys.length))
: 0,
};
}
async function waitForConclusiveSamples(page, {
minimumElapsedMs,
minimumSolves = 0,
minimumSolveSpanMs = 0,
minimumNormalFrames = 0,
}) {
const deadline = Date.now() + SAMPLE_TIMEOUT_MS;
while (Date.now() < deadline) {
const state = await page.evaluate(() => {
const qa = window.__LABEL_QA;
const solves = qa?.solve || [];
const matchingFrames = (qa?.frames || []).filter(
(sample) => sample.observationCount === 5200,
);
return {
elapsedMs: performance.now() - (qa?.startedAt || performance.now()),
solveCount: solves.length,
solveSpanMs: solves.length > 1 ? solves.at(-1).t - solves[0].t : 0,
normalFrameCount: matchingFrames.length,
};
});
if (state.elapsedMs >= minimumElapsedMs
&& state.solveCount >= minimumSolves
&& state.solveSpanMs >= minimumSolveSpanMs
&& state.normalFrameCount >= minimumNormalFrames) return state;
await new Promise((resolve) => setTimeout(resolve, 250));
}
return null;
}
async function waitForSettledSyntheticField(page) {
await page.waitForFunction(
({ observationCount, settledSolveCount, minHeight, maxHeight }) => {
const solves = window.__LABEL_QA?.solve || [];
if (solves.length < settledSolveCount) return false;
return solves.slice(-settledSolveCount).every((sample) => (
sample.observationCount === observationCount
&& sample.labeledKeys.length > 0
&& sample.selectedCount === sample.labeledKeys.length
&& sample.cameraHeight >= minHeight
&& sample.cameraHeight <= maxHeight
));
},
{ timeout: SAMPLE_TIMEOUT_MS, polling: 100 },
{
observationCount: OBSERVATION_COUNT,
settledSolveCount: SETTLED_SOLVE_COUNT,
minHeight: GLOBAL_CAMERA_MIN_HEIGHT_M,
maxHeight: GLOBAL_CAMERA_MAX_HEIGHT_M,
},
);
}
async function main() {
console.log('\nDetection Label QA');
console.log(` App URL : ${APP_URL}`);
console.log(` Observations : ${OBSERVATION_COUNT}`);
console.log(` Sample window: >=${SYNTHETIC_SAMPLE_MS / 1000}s synthetic + >=${NORMAL_SAMPLE_MS / 1000}s normal\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 = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(executablePath ? { executablePath } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
'--use-angle=swiftshader',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1280,800',
],
});
const consoleErrors = [];
const failedResponses = [];
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();
const sourceUrl = message.location()?.url || '';
if (!/Failed to load resource.*404/i.test(text)) {
consoleErrors.push(sourceUrl ? `${text} [${sourceUrl}]` : text);
}
});
page.on('pageerror', (error) => consoleErrors.push(`pageerror: ${error.message}`));
page.on('response', (response) => {
if (response.status() >= 500) {
failedResponses.push(`HTTP ${response.status()} ${response.url()}`);
}
});
await page.evaluateOnNewDocument((storageKey, appOrigin) => {
localStorage.setItem(storageKey, 'WEIGHTED');
const realFetch = window.fetch.bind(window);
window.fetch = (input, init) => {
const requestUrl = typeof input === 'string' || input instanceof URL
? String(input)
: input?.url;
const url = new URL(requestUrl, window.location.href);
if (url.origin === appOrigin && url.pathname === '/api/openai/hud-summary') {
return Promise.resolve(new Response(JSON.stringify({ summary: 'QA globe ready' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}));
}
return realFetch(input, init);
};
}, STORAGE_KEY, APP_ORIGIN);
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.styleManager,
{ timeout: 60000, polling: 100 },
);
// flyToAustin schedules its 600 m arrival 500 ms after initialization.
// Let that callback start, then cancel it before establishing the global
// measurement camera so startup motion cannot invalidate the sample.
await new Promise((resolve) => setTimeout(resolve, 600));
await page.evaluate(() => window.__godsEyeView.viewer.camera.cancelFlight());
const stateChecks = await page.evaluate((storageKey) => {
const manager = window.__godsEyeView.styleManager;
const restored = manager.getDetectionState();
manager._syncDetectionUiFromEngine();
const storedAfterPassiveSync = localStorage.getItem(storageKey);
const off = manager.setDetection({ enabled: false, densityPct: 25 });
const restoredFromOff = manager.setDetection({ enabled: true });
return { restored, storedAfterPassiveSync, off, restoredFromOff };
}, STORAGE_KEY);
record(
'allocation preference survives passive UI sync',
stateChecks.storedAfterPassiveSync === 'WEIGHTED',
`share/engine=${stateChecks.restored.allocationStrategy}, stored=${stateChecks.storedAfterPassiveSync}`,
);
record(
'density update while OFF restores the matching profile',
stateChecks.off.detectionMode === 'OFF'
&& stateChecks.off.densityPct === 25
&& stateChecks.restoredFromOff.detectionMode === 'SPARSE'
&& stateChecks.restoredFromOff.densityPct === 25,
`OFF=${stateChecks.off.densityPct}%, restore=${stateChecks.restoredFromOff.detectionMode}/${stateChecks.restoredFromOff.densityPct}%`,
);
const injected = await page.evaluate(({ fieldCounts, normalCounts }) => {
const { viewer, dataManager, styleManager } = window.__godsEyeView;
viewer.camera.cancelFlight();
const Cartesian3 = viewer.camera.position.constructor;
const field = { flights: [], military: [], satellites: [] };
const layerIds = Object.keys(field);
const goldenAngle = Math.PI * (3 - Math.sqrt(5));
for (const layerId of layerIds) {
const count = fieldCounts[layerId];
for (let localIndex = 0; localIndex < count; localIndex++) {
const radius = 57 * Math.sqrt((localIndex + 0.5) / count);
const angle = localIndex * goldenAngle + layerIds.indexOf(layerId) * 0.7;
const lat = Math.max(-65, Math.min(75, 30 + radius * Math.sin(angle)));
const lon = -97 + radius * Math.cos(angle) / Math.max(0.4, Math.cos(lat * Math.PI / 180));
const sourceId = `${layerId.slice(0, 1)}-${String(localIndex).padStart(4, '0')}`;
field[layerId].push({
sourceId,
id: sourceId.toUpperCase(),
metric: layerId === 'satellites' ? 'LEO' : `${250 + (localIndex % 450)}KT`,
position: Cartesian3.fromDegrees(lon, lat, layerId === 'satellites' ? 550000 : 9000),
type: layerId === 'satellites' ? 'SAT' : 'AIR',
tier: layerId === 'military' ? 'military' : (layerId === 'satellites' ? 'space' : 'civil'),
});
}
}
window.__LABEL_QA_FIELD = field;
window.__LABEL_QA_NORMAL_COUNTS = normalCounts;
for (const layerId of layerIds) {
const entry = dataManager.layers.get(layerId);
entry.module.getDetectableObjects = () => window.__LABEL_QA_FIELD[layerId];
}
viewer.camera.setView({
destination: Cartesian3.fromDegrees(-97, 30, 25000000),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
styleManager.setDetection({
enabled: true,
densityPct: 100,
allocationStrategy: 'elastic',
});
window.__LABEL_QA = {
solve: [],
frames: [],
lastRevision: -1,
lastFrameNumber: -1,
startedAt: performance.now(),
};
let lastOrbitAt = performance.now();
window.__LABEL_QA_ORBIT = setInterval(() => {
const now = performance.now();
viewer.camera.rotateRight((now - lastOrbitAt) * 0.0000012);
lastOrbitAt = now;
viewer.scene.requestRender();
}, 100);
const sample = () => {
const diagnostics = styleManager.getDetectionDiagnostics();
const frameNumber = viewer.scene.frameState?.frameNumber ?? -1;
if (diagnostics && frameNumber !== window.__LABEL_QA.lastFrameNumber) {
window.__LABEL_QA.lastFrameNumber = frameNumber;
window.__LABEL_QA.frames.push({
didSolve: diagnostics.didSolve,
placementBuildCount: diagnostics.placementBuildCount,
selectedCount: diagnostics.selectedCount,
fadingCount: diagnostics.fadingCount,
cohortCount: diagnostics.cohortCount,
observationCount: diagnostics.observationCount,
frameTotalMs: diagnostics.frameTotalMs,
paintMs: diagnostics.paintMs,
throttleSkipCount: diagnostics.throttleSkipCount,
});
}
const dataset = document.querySelector('#world-overlay-canvas')?.dataset;
const revision = Number(dataset?.solveRevision || 0);
if (dataset && revision > 0 && revision !== window.__LABEL_QA.lastRevision) {
window.__LABEL_QA.lastRevision = revision;
window.__LABEL_QA.solve.push({
t: performance.now(),
revision,
solveMs: Number(dataset.solveMs),
frameTotalMs: Number(dataset.frameTotalMs),
paintMs: Number(dataset.paintMs),
throttleSkipCount: Number(dataset.throttleSkipCount),
placementBuildCount: Number(dataset.placementBuildCount),
observationCount: Number(dataset.observationCount),
selectedCount: Number(dataset.selectedCount),
fadingCount: Number(dataset.fadingCount),
cameraHeight: viewer.camera.positionCartographic.height,
demandByLayer: JSON.parse(dataset.demandByLayer || '{}'),
cohortByLayer: JSON.parse(dataset.cohortByLayer || '{}'),
entitlementByLayer: JSON.parse(dataset.entitlementByLayer || '{}'),
labeledKeys: JSON.parse(dataset.labeledKeys || '[]'),
});
}
requestAnimationFrame(sample);
};
requestAnimationFrame(sample);
return Object.fromEntries(layerIds.map((layerId) => [layerId, field[layerId].length]));
}, { fieldCounts: FIELD_COUNTS, normalCounts: NORMAL_COUNTS });
record(
'deterministic field contains exactly 12,000 observations',
Object.values(injected).reduce((sum, count) => sum + count, 0) === OBSERVATION_COUNT,
JSON.stringify(injected),
);
// Warm-up before steady-state p95: the first solves after field
// injection run on cold-tier code paths and skew p95 on a fresh server.
// (perf wave 2 added a 2.5 s sleep for this; main's settle+conclusive
// -samples gates supersede it — same fix, deterministic instead of timed.)
await waitForSettledSyntheticField(page);
await page.evaluate(() => {
const viewer = window.__godsEyeView.viewer;
const revision = Number(document.querySelector('#world-overlay-canvas')?.dataset?.solveRevision || 0);
window.__LABEL_QA.solve = [];
window.__LABEL_QA.frames = [];
window.__LABEL_QA.lastRevision = revision;
window.__LABEL_QA.lastFrameNumber = viewer.scene.frameState?.frameNumber ?? -1;
window.__LABEL_QA.startedAt = performance.now();
});
const syntheticSampling = await waitForConclusiveSamples(page, {
minimumElapsedMs: SYNTHETIC_SAMPLE_MS,
minimumSolves: MIN_SYNTHETIC_SOLVES,
minimumSolveSpanMs: MIN_SYNTHETIC_SPAN_MS,
});
await page.screenshot({
path: path.join(SHOT_DIR, 'deterministic-12000.png'),
fullPage: false,
});
const syntheticSamples = await page.evaluate(() => {
const completed = window.__LABEL_QA;
const counts = window.__LABEL_QA_NORMAL_COUNTS;
for (const layerId of Object.keys(window.__LABEL_QA_FIELD)) {
window.__LABEL_QA_FIELD[layerId] = window.__LABEL_QA_FIELD[layerId].slice(0, counts[layerId]);
}
window.__LABEL_QA = {
solve: [],
frames: [],
lastRevision: -1,
lastFrameNumber: -1,
startedAt: performance.now(),
};
return completed;
});
await page.waitForFunction(
() => window.__godsEyeView.styleManager.getDetectionDiagnostics()?.observationCount === 5200,
{ timeout: SAMPLE_TIMEOUT_MS, polling: 100 },
);
await page.evaluate(() => {
const viewer = window.__godsEyeView.viewer;
const revision = Number(document.querySelector('#world-overlay-canvas')?.dataset?.solveRevision || 0);
window.__LABEL_QA.solve = [];
window.__LABEL_QA.frames = [];
window.__LABEL_QA.lastRevision = revision;
window.__LABEL_QA.lastFrameNumber = viewer.scene.frameState?.frameNumber ?? -1;
window.__LABEL_QA.startedAt = performance.now();
});
const normalSampling = await waitForConclusiveSamples(page, {
minimumElapsedMs: NORMAL_SAMPLE_MS,
minimumNormalFrames: MIN_NORMAL_FRAMES,
});
const normalSamples = await page.evaluate(() => {
clearInterval(window.__LABEL_QA_ORBIT);
return window.__LABEL_QA;
});
await page.screenshot({
path: path.join(SHOT_DIR, 'normal-global-5200.png'),
fullPage: false,
});
const solveSamples = syntheticSamples.solve;
const steadySolveSamples = solveSamples.filter((sample) => (
sample.observationCount === OBSERVATION_COUNT
&& sample.labeledKeys.length > 0
&& sample.selectedCount === sample.labeledKeys.length
&& sample.cameraHeight >= GLOBAL_CAMERA_MIN_HEIGHT_M
&& sample.cameraHeight <= GLOBAL_CAMERA_MAX_HEIGHT_M
));
const normalFieldFrames = normalSamples.frames.filter(
(sample) => sample.observationCount === 5200,
);
const normalFrames = normalFieldFrames.filter((sample) => !sample.didSolve);
const solveP95 = percentile(solveSamples.map((sample) => sample.solveMs), 0.95);
const renderP95 = percentile(normalFrames.map((sample) => sample.frameTotalMs), 0.95);
// SwiftShader can render the complete Cesium scene below 8 FPS, making
// every sampled overlay frame eligible for the 125 ms solve tick. Paint is
// timed separately from solve, so it remains the deterministic frame-lane
// measurement even when there are no non-solve frames in this backend.
const paintP95 = percentile(
(normalFrames.length ? normalFrames : normalFieldFrames).map((sample) => sample.paintMs),
0.95,
);
const throttles = normalSamples.frames.map((sample) => sample.throttleSkipCount).filter(Number.isFinite);
const throttleDelta = throttles.length ? Math.max(...throttles) - Math.min(...throttles) : 0;
const normalPlacementOverflow = normalFieldFrames.filter(
(sample) => sample.placementBuildCount > sample.selectedCount + sample.fadingCount
+ (sample.didSolve ? sample.cohortCount : 0),
);
const solvePlacementOverflow = solveSamples.filter((sample) => {
const cohort = Object.values(sample.cohortByLayer).reduce((sum, count) => sum + count, 0);
return sample.placementBuildCount > cohort + sample.selectedCount + sample.fadingCount;
});
const cohortOverflow = solveSamples.filter((sample) => Object.entries(sample.cohortByLayer)
.some(([layerId, count]) => {
const quota = sample.entitlementByLayer[layerId] || 0;
return count > Math.min(256, Math.max(64, 4 * quota));
}));
const membership = analyzeMembership(solveSamples);
record(
'sampling is conclusive across the pathological and normal fields',
Boolean(syntheticSampling && normalSampling)
&& solveSamples.length >= MIN_SYNTHETIC_SOLVES
&& syntheticSampling.solveSpanMs >= MIN_SYNTHETIC_SPAN_MS
&& normalFieldFrames.length >= MIN_NORMAL_FRAMES,
`synthetic solves=${solveSamples.length}, span=${syntheticSampling?.solveSpanMs?.toFixed(0) || 0}ms; normal frames=${normalFieldFrames.length}`,
);
record(
'synthetic churn samples remain non-empty at the 12,000-contact global view',
steadySolveSamples.length === solveSamples.length,
`steady=${steadySolveSamples.length}/${solveSamples.length}; population=${membership.minimumPopulation}-${membership.maximumPopulation}`,
);
record(
'solve cohort remains bounded per layer',
cohortOverflow.length === 0,
`${solveSamples.length} solves checked`,
);
record(
'placement construction is bounded in both lanes',
normalPlacementOverflow.length === 0 && solvePlacementOverflow.length === 0,
`normal overflow=${normalPlacementOverflow.length}, solve overflow=${solvePlacementOverflow.length}`,
);
record(
'solve p95 is below approximately 4 ms',
solveP95 < 4.5,
`${solveP95.toFixed(2)} ms`,
);
record(
'normal-field overlay paint p95 is at or below 10 ms',
paintP95 <= 10,
`paint=${paintP95.toFixed(2)} ms${Number.isFinite(renderP95) ? `, non-solve total=${renderP95.toFixed(2)} ms` : ', every sampled frame included a solve'}`,
);
record(
'normal scene does not trigger adaptive throttling',
throttleDelta === 0,
`skip delta=${throttleDelta}`,
);
record(
'label churn remains below 8% per second',
membership.churnPctPerSec < 8,
`${membership.churnPctPerSec.toFixed(2)}%/s; replacements=${membership.replacements}, exposure=${membership.exposure.toFixed(1)} label-s, transitions=${membership.transitions}, population=${membership.minimumPopulation}-${membership.maximumPopulation}`,
);
record(
'no show-hide-show cycle occurs within two seconds',
membership.showHideShow === 0,
`cycles=${membership.showHideShow}`,
);
record(
'console remains clean',
consoleErrors.length === 0,
consoleErrors.slice(0, 3).join(' | ') || '0 errors',
);
record(
'no HTTP 5xx responses occur',
failedResponses.length === 0,
failedResponses.slice(0, 3).join(' | ') || '0 responses',
);
console.log(`\n Screenshot: ${path.join(SHOT_DIR, 'deterministic-12000.png')}`);
console.log(` Screenshot: ${path.join(SHOT_DIR, 'normal-global-5200.png')}`);
console.log(` Synthetic solves: ${solveSamples.length}; normal frames: ${normalFieldFrames.length} (${normalFrames.length} non-solve)`);
} finally {
await browser.close();
}
const failures = results.filter((result) => !result.ok);
console.log(`\n${results.length - failures.length}/${results.length} checks passed.`);
if (failures.length) process.exit(1);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
+653
View File
@@ -0,0 +1,653 @@
#!/usr/bin/env node
/** Focused browser proof for the responsive, accessible Map Source tray. */
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import puppeteer from 'puppeteer';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const shotsDir = path.join(repoRoot, 'qa-shots', 'map-source-tray');
const appUrl = process.env.QA_BASE_URL || 'http://localhost:4173';
const headful = process.argv.includes('--headful');
// The no-ion-token contract is a real shipped state that a normal keyed
// `dev-fresh.sh` run can never reach, so it went untested on every machine that
// has a token. `--keyless` (or QA_MAP_SOURCE_TRAY_KEYLESS=1) clears the
// controller's token in-page before the key-required assertions, so one server
// can prove both branches. It forces the keyless EXPECTATIONS as well: a seam
// that fails to take effect is a failure, not a quiet fall-through to the keyed
// branch.
const forceKeyless = process.argv.includes('--keyless')
|| process.env.QA_MAP_SOURCE_TRAY_KEYLESS === '1';
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH
|| (() => { try { return puppeteer.executablePath(); } catch { return null; } })();
if (!executablePath || !fs.existsSync(executablePath)) {
throw new Error('Puppeteer Chrome for Testing is unavailable');
}
fs.mkdirSync(shotsDir, { recursive: true });
const browser = await puppeteer.launch({
headless: headful ? false : 'new',
executablePath,
args: ['--use-angle=metal', '--enable-gpu', '--no-sandbox'],
});
const page = await browser.newPage();
const failures = [];
const consoleErrors = [];
page.on('console', (message) => {
if (message.type() === 'error' && !/Failed to load resource.*404/i.test(message.text())) {
const source = message.location()?.url;
consoleErrors.push(source ? `${message.text()} [${source}]` : message.text());
}
});
page.on('pageerror', (error) => consoleErrors.push(error.message));
const check = (name, passed, detail = '') => {
console.log(` [${passed ? 'PASS' : 'FAIL'}] ${name}${detail ? ` — ${detail}` : ''}`);
if (!passed) failures.push(name);
};
const trayMetrics = () => page.evaluate(() => {
const panel = document.getElementById('control-panel');
const popover = document.getElementById('control-panel-popover');
const row = document.getElementById('map-stack-chips');
const popoverRect = popover.getBoundingClientRect();
const chips = [...row.children].map((chip) => {
const rect = chip.getBoundingClientRect();
return {
id: chip.dataset.stackId,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
pressed: chip.getAttribute('aria-pressed'),
ariaDisabled: chip.getAttribute('aria-disabled'),
ariaLabel: chip.getAttribute('aria-label'),
};
});
return {
viewport: { width: innerWidth, height: innerHeight },
expanded: document.getElementById('control-panel-toggle').getAttribute('aria-expanded'),
pinned: panel.classList.contains('dock-pinned'),
popover: {
left: popoverRect.left,
right: popoverRect.right,
top: popoverRect.top,
bottom: popoverRect.bottom,
width: popoverRect.width,
},
columns: getComputedStyle(row).gridTemplateColumns,
rows: new Set(chips.map((chip) => chip.top)).size,
chips,
};
});
try {
await page.setViewport({ width: 1000, height: 900, deviceScaleFactor: 1 });
await page.setRequestInterception(true);
page.on('request', (request) => {
const url = new URL(request.url());
if (url.origin === new URL(appUrl).origin && url.pathname === '/api/openai/hud-summary') {
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ summary: 'Map Source tray QA' }),
});
return;
}
request.continue();
});
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 60_000 });
await page.waitForFunction(
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 60_000 },
);
const presentation = await page.evaluate(() => ({
ids: [...document.querySelectorAll('.map-stack-chip')].map((chip) => chip.dataset.stackId),
retiredPanel: Boolean(document.getElementById('stack-panel')),
toggleTag: document.getElementById('control-panel-toggle')?.tagName,
controls: document.getElementById('control-panel-toggle')?.getAttribute('aria-controls'),
}));
check(
'exact four-source presentation; the retired left Map Stack panel is gone',
JSON.stringify(presentation.ids) === JSON.stringify([
'photoreal', 'bing-aerial', 'bing-labels', 'osm',
]) && !presentation.retiredPanel,
JSON.stringify(presentation),
);
check(
'compact wing is a semantic disclosure',
presentation.toggleTag === 'BUTTON' && presentation.controls === 'control-panel-popover',
JSON.stringify(presentation),
);
await page.focus('#control-panel-toggle');
await page.keyboard.press('Enter');
await new Promise((resolve) => setTimeout(resolve, 300));
const keyboardOpen = await page.evaluate(() => ({
expanded: document.getElementById('control-panel-toggle').getAttribute('aria-expanded'),
activeStack: document.activeElement?.dataset?.stackId || null,
}));
check(
'Enter opens the tray and hands focus to a Map Source tile',
keyboardOpen.expanded === 'true' && keyboardOpen.activeStack === 'photoreal',
JSON.stringify(keyboardOpen),
);
await page.keyboard.press('Escape');
await new Promise((resolve) => setTimeout(resolve, 40));
const keyboardClose = await page.evaluate(() => ({
expanded: document.getElementById('control-panel-toggle').getAttribute('aria-expanded'),
activeId: document.activeElement?.id || null,
}));
check(
'Escape closes the tray and restores disclosure focus',
keyboardClose.expanded === 'false' && keyboardClose.activeId === 'control-panel-toggle',
JSON.stringify(keyboardClose),
);
await page.keyboard.press('Space');
await new Promise((resolve) => setTimeout(resolve, 300));
const spaceOpen = await page.evaluate(() => ({
expanded: document.getElementById('control-panel-toggle').getAttribute('aria-expanded'),
activeStack: document.activeElement?.dataset?.stackId || null,
}));
check(
'Space opens the tray through the same keyboard path',
spaceOpen.expanded === 'true' && spaceOpen.activeStack === 'photoreal',
JSON.stringify(spaceOpen),
);
await page.keyboard.press('Escape');
await page.keyboard.down('Enter');
await new Promise((resolve) => setTimeout(resolve, 320));
await page.keyboard.up('Enter');
await page.keyboard.press('Escape');
await page.keyboard.press('Enter');
await new Promise((resolve) => setTimeout(resolve, 300));
const longHoldRecovery = await page.evaluate(() => ({
expanded: document.getElementById('control-panel-toggle').getAttribute('aria-expanded'),
activeStack: document.activeElement?.dataset?.stackId || null,
}));
check(
'long Enter hold cannot strand the disclosure keyboard path',
longHoldRecovery.expanded === 'true' && longHoldRecovery.activeStack === 'photoreal',
JSON.stringify(longHoldRecovery),
);
if (forceKeyless) {
await page.evaluate(() => {
const styleManager = window.__godsEyeView.styleManager;
window.__qaIonTokenBackup = styleManager.mapStackController.cesiumToken;
styleManager.mapStackController.cesiumToken = '';
styleManager._initMapStackControl();
});
}
await page.focus('[data-stack-id="bing-aerial"]');
const ionAvailable = await page.$eval(
'[data-stack-id="bing-aerial"]',
(chip) => chip.getAttribute('aria-disabled') !== 'true',
);
await page.click('[data-stack-id="bing-aerial"]');
if (ionAvailable) {
// Cesium creates the imagery provider asynchronously. Wait for controller
// truth instead of assuming a keyed switch can settle in one animation.
await page.waitForFunction(
() => window.__godsEyeView.styleManager.mapStackController.getActiveId() === 'bing-aerial'
|| Boolean(window.__godsEyeView.styleManager.mapStackController.getState()?.lastError),
{ timeout: 20_000 },
).catch(() => {});
}
const ionSource = await page.evaluate(() => {
const chip = document.querySelector('[data-stack-id="bing-aerial"]');
return {
focused: document.activeElement === chip,
ariaDisabled: chip.getAttribute('aria-disabled'),
ariaLabel: chip.getAttribute('aria-label'),
active: [...document.querySelectorAll('.map-stack-chip')]
.filter((candidate) => candidate.getAttribute('aria-pressed') === 'true')
.map((candidate) => candidate.dataset.stackId),
};
});
if (forceKeyless || ionSource.ariaDisabled === 'true') {
check(
'key-required sources stay focusable, explained, and inert when no ion token is configured',
ionSource.ariaDisabled === 'true'
&& ionSource.focused
&& /token required/i.test(ionSource.ariaLabel)
&& JSON.stringify(ionSource.active) === JSON.stringify(['photoreal']),
JSON.stringify(ionSource),
);
} else {
check(
'key-required sources switch normally when the ion token is configured',
ionSource.focused
&& ionSource.ariaDisabled === 'false'
&& JSON.stringify(ionSource.active) === JSON.stringify(['bing-aerial']),
JSON.stringify(ionSource),
);
}
if (forceKeyless) {
// Hand the real token back so every later assertion runs against the same
// configuration in both invocations.
await page.evaluate(() => {
const styleManager = window.__godsEyeView.styleManager;
styleManager.mapStackController.cesiumToken = window.__qaIonTokenBackup || '';
delete window.__qaIonTokenBackup;
styleManager._initMapStackControl();
});
}
const switching = await page.evaluate(async () => {
const styleManager = window.__godsEyeView.styleManager;
const controller = styleManager.mapStackController;
const originalSetStack = controller.setStack.bind(controller);
const before = controller.getActiveId();
let release;
const gate = new Promise((resolve) => { release = resolve; });
controller.setStack = async (stackId) => {
await gate;
return originalSetStack(stackId);
};
const switchPromise = styleManager._setMapStack('osm', { syncShare: false });
const during = {
status: document.getElementById('map-stack-status').textContent,
active: [...document.querySelectorAll('.map-stack-chip')]
.filter((chip) => chip.getAttribute('aria-pressed') === 'true')
.map((chip) => chip.dataset.stackId),
};
release();
await switchPromise;
const after = {
status: document.getElementById('map-stack-status').textContent,
active: [...document.querySelectorAll('.map-stack-chip')]
.filter((chip) => chip.getAttribute('aria-pressed') === 'true')
.map((chip) => chip.dataset.stackId),
};
controller.setStack = originalSetStack;
await styleManager._setMapStack(before, { syncShare: false });
return { before, during, after };
});
check(
'switching feedback is truthful and active state moves only after commit',
switching.during.status === '...'
&& JSON.stringify(switching.during.active) === JSON.stringify([switching.before])
&& JSON.stringify(switching.after.active) === JSON.stringify(['osm']),
JSON.stringify(switching),
);
const acquiringLifecycle = await page.evaluate(async () => {
const styleManager = window.__godsEyeView.styleManager;
const status = document.getElementById('global-loading-status');
const snapshot = () => ({
hidden: status.hidden,
state: status.dataset.state || null,
label: document.getElementById('global-loading-label').textContent.trim(),
detail: document.getElementById('global-loading-detail').textContent.trim(),
});
styleManager._handleShareTrackingRestoreStatus({
classification: 'pending',
layerId: 'flights',
targetId: 'qa-flight',
label: 'flight',
});
await new Promise((resolve) => setTimeout(resolve, 40));
const pending = snapshot();
styleManager._handleShareTrackingRestoreStatus({
classification: 'followed',
layerId: 'flights',
targetId: 'qa-flight',
label: 'flight',
});
const followed = snapshot();
styleManager._handleShareTrackingRestoreStatus({
classification: 'pending',
layerId: 'military',
targetId: 'qa-military',
label: 'military flight',
});
styleManager._handleShareTrackingRestoreStatus({
classification: 'source-unavailable',
layerId: 'flights',
targetId: 'qa-flight',
label: 'flight',
});
await new Promise((resolve) => setTimeout(resolve, 40));
const staleTerminal = snapshot();
styleManager._handleShareTrackingRestoreStatus({
classification: 'cancelled',
layerId: 'military',
targetId: 'qa-military',
label: 'military flight',
});
const cancelled = snapshot();
return { pending, followed, staleTerminal, cancelled };
});
check(
'ACQUIRING DOM notice persists, ignores stale terminals, and clears on ownership completion',
acquiringLifecycle.pending.hidden === false
&& acquiringLifecycle.pending.state === 'acquiring'
&& acquiringLifecycle.pending.label === 'ACQUIRING'
&& acquiringLifecycle.pending.detail === 'SHARED FLIGHT'
&& acquiringLifecycle.followed.hidden === true
&& acquiringLifecycle.staleTerminal.hidden === false
&& acquiringLifecycle.staleTerminal.state === 'acquiring'
&& acquiringLifecycle.staleTerminal.detail === 'SHARED MILITARY FLIGHT'
&& acquiringLifecycle.cancelled.hidden === true,
JSON.stringify(acquiringLifecycle),
);
const acquiringFailureArbitration = await page.evaluate(async () => {
const styleManager = window.__godsEyeView.styleManager;
const dataManager = styleManager._dataManager;
const status = document.getElementById('global-loading-status');
const originalGetAll = dataManager.getAll;
const snapshot = () => ({
hidden: status.hidden,
state: status.dataset.state || null,
label: document.getElementById('global-loading-label').textContent.trim(),
detail: document.getElementById('global-loading-detail').textContent.trim(),
});
const waitForQueuedNotice = async (label, timeoutMs = 1000) => {
const deadline = performance.now() + timeoutMs;
while (styleManager._globalStatusNotice?.label !== label
&& performance.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 16));
}
return styleManager._globalStatusNotice?.label === label;
};
const baseNow = performance.now();
try {
styleManager._loadingFeedbackState = {
phase: 'idle', visible: false, startedAt: 0, showAt: 0, hideAt: 0,
activeIds: [], batchOutcome: null, terminal: null, operation: null,
};
styleManager._handleShareTrackingRestoreStatus({
classification: 'pending',
layerId: 'flights',
targetId: 'qa-failure-flight',
label: 'flight',
});
dataManager.getAll = () => [{
id: 'qa-unrelated-layer',
name: 'QA unrelated layer',
lifecycleState: 'enabling',
enabled: false,
stats: {},
}];
styleManager._updateGlobalLoadingFeedback(baseNow);
styleManager._updateGlobalLoadingFeedback(baseNow + 200);
dataManager.getAll = () => [];
styleManager._loadingFeedbackEvent = {
type: 'visibility-failed',
layerId: 'qa-unrelated-layer',
error: new Error('QA offline'),
};
styleManager._updateGlobalLoadingFeedback(baseNow + 300);
const failureStart = snapshot();
styleManager._handleShareTrackingRestoreStatus({
classification: 'source-unavailable',
layerId: 'flights',
targetId: 'qa-failure-flight',
label: 'flight',
});
const queuedNoticeReady = await waitForQueuedNotice(
'Shared flight could not be restored — feed unavailable',
);
const shareFailureQueued = snapshot();
styleManager._updateGlobalLoadingFeedback(baseNow + 5299);
const failureEnd = snapshot();
styleManager._updateGlobalLoadingFeedback(baseNow + 5300);
const shareFailureStart = snapshot();
styleManager._updateGlobalLoadingFeedback(baseNow + 10299);
const shareFailureEnd = snapshot();
styleManager._updateGlobalLoadingFeedback(baseNow + 10300);
const settled = snapshot();
return {
failureStart,
queuedNoticeReady,
shareFailureQueued,
failureEnd,
shareFailureStart,
shareFailureEnd,
settled,
};
} finally {
dataManager.getAll = originalGetAll;
styleManager._handleShareTrackingRestoreStatus({
classification: 'cancelled',
layerId: 'flights',
targetId: 'qa-failure-flight',
label: 'flight',
});
}
});
check(
'manager failure then queued share failure each receives its full visible dwell',
acquiringFailureArbitration.failureStart.hidden === false
&& acquiringFailureArbitration.failureStart.state === 'error'
&& acquiringFailureArbitration.failureStart.label === 'LOAD FAILED'
&& acquiringFailureArbitration.queuedNoticeReady === true
&& acquiringFailureArbitration.shareFailureQueued.state === 'error'
&& acquiringFailureArbitration.shareFailureQueued.label === 'LOAD FAILED'
&& acquiringFailureArbitration.failureEnd.state === 'error'
&& acquiringFailureArbitration.failureEnd.label === 'LOAD FAILED'
&& acquiringFailureArbitration.shareFailureStart.state === 'error'
&& acquiringFailureArbitration.shareFailureStart.label === 'Shared flight could not be restored — feed unavailable'
&& acquiringFailureArbitration.shareFailureEnd.label === 'Shared flight could not be restored — feed unavailable'
&& acquiringFailureArbitration.settled.hidden === true,
JSON.stringify(acquiringFailureArbitration),
);
// Unpinned mouse-away dismissal AFTER a tile click. Chromium focuses a
// <button> on mouse press, so a close-guard reading plain
// `document.activeElement` left the tray permanently open once Map Source
// moved into it — switch a basemap and the popover never went away again
// (field report). The pin samples the exact mechanism: focus IS parked
// inside the panel and is NOT `:focus-visible`, and the tray closes anyway.
const setControlPanelPinned = (wanted) => page.evaluate((want) => {
const panel = document.getElementById('control-panel');
if (panel.classList.contains('dock-pinned') !== want) {
document.querySelector('.dock-pin-btn[data-pin-target="control-panel"]').click();
}
return panel.classList.contains('dock-pinned');
}, wanted);
const readTrayState = () => page.evaluate(() => {
const panel = document.getElementById('control-panel');
const active = document.activeElement;
let focusVisible = null;
try { focusVisible = active?.matches?.(':focus-visible') ?? null; } catch { focusVisible = null; }
return {
collapsed: panel.classList.contains('collapsed'),
expanded: document.getElementById('control-panel-toggle').getAttribute('aria-expanded'),
focusInside: panel.contains(active),
focusVisible,
};
});
const clickTileThenLeave = async (stackId) => {
await page.evaluate(() => window.__godsEyeView.styleManager
.setPanelCollapsed('control-panel', false, { explicit: true }));
await new Promise((resolve) => setTimeout(resolve, 240));
await page.click(`[data-stack-id="${stackId}"]`);
await new Promise((resolve) => setTimeout(resolve, 120));
const afterClick = await readTrayState();
await page.mouse.move(20, 20); // leave the dock entirely → pointerleave
// Past the 420 ms unpinned close delay with room for a slow frame.
await new Promise((resolve) => setTimeout(resolve, 1000));
return { afterClick, afterLeave: await readTrayState() };
};
// The OTHER half of the same rule, asserted positively: a KEYBOARD user who
// tabbed to a tile and pressed Enter must keep the tray, because closing it
// out from under them would strand the caret in a hidden surface. Same
// mouse-away that dismisses after a click, opposite outcome — so a fix that
// simply deleted the focus guard would fail here.
await setControlPanelPinned(false);
await page.evaluate(() => window.__godsEyeView.styleManager
.setPanelCollapsed('control-panel', true, { explicit: true }));
await new Promise((resolve) => setTimeout(resolve, 200));
await page.focus('#control-panel-toggle');
await page.keyboard.press('Enter'); // opens and hands focus to the active tile
await new Promise((resolve) => setTimeout(resolve, 400));
await page.keyboard.press('Tab'); // tab ONTO a tile, keyboard modality
await page.keyboard.press('Enter'); // activate it from the keyboard
await new Promise((resolve) => setTimeout(resolve, 200));
const keyboardAfterActivate = await page.evaluate(() => ({
focusedStack: document.activeElement?.dataset?.stackId || null,
isChip: !!document.activeElement?.classList?.contains('map-stack-chip'),
}));
// Enter the tray with the pointer and leave again, so a real pointerleave
// schedules the close this pin expects to be declined.
const chipPoint = await page.$eval('#map-stack-chips .map-stack-chip', (chip) => {
const rect = chip.getBoundingClientRect();
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
});
await page.mouse.move(chipPoint.x, chipPoint.y);
await new Promise((resolve) => setTimeout(resolve, 120));
await page.mouse.move(20, 20);
await new Promise((resolve) => setTimeout(resolve, 1000));
const keyboardHold = await readTrayState();
check(
'keyboard activation of a tile HOLDS the tray open through the same mouse-away',
keyboardAfterActivate.isChip === true
&& keyboardHold.collapsed === false
&& keyboardHold.expanded === 'true'
&& keyboardHold.focusInside === true
&& keyboardHold.focusVisible === true,
JSON.stringify({ keyboardAfterActivate, keyboardHold }),
);
await setControlPanelPinned(false);
const dismissAfterTileClick = await clickTileThenLeave('osm');
const pinnedForHold = await setControlPanelPinned(true);
const pinnedHold = await clickTileThenLeave('photoreal');
await setControlPanelPinned(false);
await page.evaluate(() => window.__godsEyeView.styleManager
._setMapStack('photoreal', { syncShare: false }));
// Hand the tray back OPEN and unpinned — the responsive block below starts by
// clicking the pin control, which is only hittable while the tray is showing.
await page.evaluate(() => window.__godsEyeView.styleManager
.setPanelCollapsed('control-panel', false, { explicit: true }));
await new Promise((resolve) => setTimeout(resolve, 240));
check(
'a tile click does not exempt the unpinned tray from mouse-away auto-dismiss',
dismissAfterTileClick.afterClick.collapsed === false
&& dismissAfterTileClick.afterClick.focusInside === true
&& dismissAfterTileClick.afterClick.focusVisible === false
&& dismissAfterTileClick.afterLeave.collapsed === true
&& dismissAfterTileClick.afterLeave.expanded === 'false'
&& pinnedForHold === true
&& pinnedHold.afterLeave.collapsed === false,
JSON.stringify({ dismissAfterTileClick, pinnedForHold, pinnedHold }),
);
await page.click('.dock-pin-btn[data-pin-target="control-panel"]');
const desktop = await trayMetrics();
check(
'desktop tray is one row and fully inside the viewport',
desktop.rows === 1
&& desktop.popover.left >= 0
&& desktop.popover.right <= desktop.viewport.width,
JSON.stringify(desktop),
);
await page.setViewport({ width: 620, height: 900, deviceScaleFactor: 1 });
await new Promise((resolve) => setTimeout(resolve, 180));
const at620 = await trayMetrics();
check(
'620 px tray uses two rows without clipping',
at620.expanded === 'true'
&& at620.pinned
&& at620.rows === 2
&& at620.popover.left >= 0
&& at620.popover.right <= at620.viewport.width,
JSON.stringify(at620),
);
await page.setViewport({ width: 480, height: 900, deviceScaleFactor: 1 });
await new Promise((resolve) => setTimeout(resolve, 180));
const at480 = await trayMetrics();
const allRectsInside = at480.chips.every((chip) => (
chip.left >= 0 && chip.right <= at480.viewport.width
&& chip.top >= 0 && chip.bottom <= at480.viewport.height
));
check(
'480 px live resize keeps the open tray and every tile in bounds',
at480.expanded === 'true'
&& at480.pinned
&& at480.rows === 2
&& at480.popover.left >= 0
&& at480.popover.right <= at480.viewport.width
&& allRectsInside,
JSON.stringify(at480),
);
await page.screenshot({ path: path.join(shotsDir, '480-open.png') });
await page.click('.dock-pin-btn[data-pin-target="control-panel"]');
await page.click('#control-panel-toggle');
const cdp = await page.createCDPSession();
await cdp.send('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 1 });
const toggleRect = await page.$eval('#control-panel-toggle', (toggle) => {
const rect = toggle.getBoundingClientRect();
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
});
await page.touchscreen.tap(toggleRect.x, toggleRect.y);
await new Promise((resolve) => setTimeout(resolve, 100));
const touchOpen = await page.$eval('#control-panel-toggle', (toggle) => toggle.getAttribute('aria-expanded'));
check('coarse-pointer tap opens the compact wing', touchOpen === 'true', `aria-expanded=${touchOpen}`);
await cdp.send('Emulation.setTouchEmulationEnabled', { enabled: false });
// Retired and unknown stack ids take the SAME path. `bing-road` was the fifth
// source behind the retired left Map Stack panel; deleting it from
// `MAP_STACKS` (no build carrying it ever shipped publicly, so no link is
// owed anything) means an old `map=bing-road` link is now simply an
// unrecognized id, and `setStack()`'s `getStack(id) || getStack('photoreal')`
// fallback lands it on Google 3D with that tile lit — never on a hidden fifth
// source whose status reads ROAD while no tile is pressed.
await page.setViewport({ width: 1000, height: 900, deviceScaleFactor: 1 });
for (const legacyId of ['bing-road', 'garbage']) {
await page.goto(`${appUrl}#v=2&lat=30.27&lon=-97.74&map=${legacyId}`, {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 60_000 });
await page.waitForFunction(
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 60_000 },
);
await page.waitForFunction(
() => window.__godsEyeView.styleManager.mapStackController.getState()?.status !== 'switching',
{ timeout: 20_000 },
).catch(() => {});
const restored = await page.evaluate(() => ({
activeId: window.__godsEyeView.styleManager.mapStackController.getActiveId(),
lastError: window.__godsEyeView.styleManager.mapStackController.getState()?.lastError || null,
status: document.getElementById('map-stack-status').textContent.trim(),
pressed: [...document.querySelectorAll('.map-stack-chip')]
.filter((chip) => chip.getAttribute('aria-pressed') === 'true')
.map((chip) => chip.dataset.stackId),
}));
check(
`a map=${legacyId} link restores to photoreal with the photoreal tile lit`,
restored.activeId === 'photoreal'
&& restored.lastError === null
&& JSON.stringify(restored.pressed) === JSON.stringify(['photoreal']),
JSON.stringify(restored),
);
await page.screenshot({ path: path.join(shotsDir, `legacy-${legacyId}.png`) });
}
check('no new page or console errors', consoleErrors.length === 0, consoleErrors.join(' | '));
} finally {
await browser.close();
}
if (failures.length) {
console.error(`\nMap Source tray QA failed: ${failures.join(', ')}`);
process.exitCode = 1;
} else {
console.log('\nMap Source tray QA passed.');
}
+943
View File
@@ -0,0 +1,943 @@
#!/usr/bin/env node
/**
* Phase 0 world-overlay baseline harness.
*
* The harness intentionally measures the current implementation without
* importing or changing application modules. Each scene gets a fresh page,
* enables its layers through DataLayerManager.toggle(), waits for layer data,
* then samples five seconds of scripted camera motion and five seconds at rest.
*
* Usage:
* node scripts/qa-overlay-baseline.mjs
* node scripts/qa-overlay-baseline.mjs --scene datacenters
* node scripts/qa-overlay-baseline.mjs --scene cctv-street,detection-50
* node scripts/qa-overlay-baseline.mjs --json overlay-baseline.json
* node scripts/qa-overlay-baseline.mjs --screenshots-dir overlay-shots
* node scripts/qa-overlay-baseline.mjs --hardware-gpu --headful
* node scripts/qa-overlay-baseline.mjs --dist-dir gev-dist
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import puppeteer from 'puppeteer';
const CHROME_EXECUTABLE = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const DEFAULT_URL = 'http://localhost:4176';
const VIEWPORT = Object.freeze({ width: 1440, height: 900 });
const SAMPLE_MS = 5_000;
const BETWEEN_PHASE_SETTLE_MS = 1_000;
const DEFAULT_SETTLE_MS = 3_000;
const LAYER_WAIT_MS = 60_000;
const KNOWN_OVERLAY_CANVASES = new Set([
'world-overlay-canvas',
'tracked-readout',
'firms-labels',
'vessel-labels',
'cctv-cards',
]);
const SCENES = Object.freeze([
{ id: 'datacenters', layers: ['local-datacenters'], camera: [-98, 38, 6_000_000, 0, -Math.PI / 2] },
{ id: 'dams', layers: ['local-dams'], camera: [-98, 38, 6_000_000, 0, -Math.PI / 2] },
{ id: 'datacenters+dams', layers: ['local-datacenters', 'local-dams'], camera: [-98, 38, 6_000_000, 0, -Math.PI / 2] },
{ id: 'submarine-cables', layers: ['telegeography-submarine-cables'], camera: [-20, 12, 11_000_000, 0, -Math.PI / 2] },
{ id: 'cctv-street', layers: ['cctv'], cctvHeightM: 1_500 },
{ id: 'cctv-city', layers: ['cctv'], cctvHeightM: 6_000 },
{ id: 'cctv-high', layers: ['cctv'], cctvHeightM: 12_000 },
{ id: 'firms', layers: ['local-firms'], camera: [-110, 45, 5_000_000, 0, -Math.PI / 2] },
{ id: 'vessels', layers: ['ais-live-vessels'], camera: [4.05, 51.93, 18_000, 0.3, -1.25] },
{ id: 'detection-25', layers: ['flights', 'satellites'], detectionDensity: 25, camera: [-98, 38, 2_500_000, 0, -Math.PI / 2] },
{ id: 'detection-50', layers: ['flights', 'satellites'], detectionDensity: 50, camera: [-98, 38, 2_500_000, 0, -Math.PI / 2] },
{ id: 'detection-100', layers: ['flights', 'satellites'], detectionDensity: 100, camera: [-98, 38, 2_500_000, 0, -Math.PI / 2] },
{ id: 'tracked-civil-aircraft', layers: ['flights'], trackedFlight: true, camera: [-98, 38, 2_500_000, 0, -Math.PI / 2] },
{ id: 'missions-selected', layers: ['rocket-launches'], missionSelected: true, camera: [0, 15, 18_000_000, 0, -Math.PI / 2] },
{ id: 'cockpit-mode', layers: ['flights'], trackedFlight: true, cockpit: true, camera: [-98, 38, 2_500_000, 0, -Math.PI / 2] },
]);
const SCENE_ALIASES = Object.freeze({
'datacenters-dams': 'datacenters+dams',
cables: 'submarine-cables',
'tracked-civil': 'tracked-civil-aircraft',
missions: 'missions-selected',
cockpit: 'cockpit-mode',
});
const argv = process.argv.slice(2);
function getOpt(name, fallback = null) {
const index = argv.indexOf(name);
return index >= 0 && argv[index + 1] ? argv[index + 1] : fallback;
}
function hasFlag(name) {
return argv.includes(name);
}
function selectedScenes() {
const raw = getOpt('--scene');
if (!raw) return [...SCENES];
const requested = raw.split(',').map((value) => value.trim()).filter(Boolean);
const selected = [];
for (const requestedId of requested) {
const id = SCENE_ALIASES[requestedId] || requestedId;
const scene = SCENES.find((candidate) => candidate.id === id);
if (!scene) {
throw new Error(`Unknown scene '${requestedId}'. Available: ${SCENES.map((candidate) => candidate.id).join(', ')}`);
}
selected.push(scene);
}
return selected;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function contentType(filePath) {
const extension = path.extname(filePath).toLowerCase();
return ({
'.css': 'text/css',
'.gif': 'image/gif',
'.html': 'text/html',
'.ico': 'image/x-icon',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
'.js': 'text/javascript',
'.json': 'application/json',
'.ktx2': 'image/ktx2',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.wasm': 'application/wasm',
'.webp': 'image/webp',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
})[extension] || 'application/octet-stream';
}
async function installStaticDist(page, distDir) {
if (!distDir) return;
const root = path.resolve(distDir);
await page.setRequestInterception(true);
page.on('request', async (request) => {
const url = new URL(request.url());
try {
if (url.origin === new URL(APP_URL).origin) {
if (url.pathname.startsWith('/api/')) {
await request.respond({
status: 503,
contentType: 'application/json',
body: JSON.stringify({ error: 'offline_dist_capture', message: 'Dev-server proxies unavailable' }),
});
return;
}
const relativePath = decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname).replace(/^\/+/, '');
const filePath = path.resolve(root, relativePath);
if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) {
await request.respond({ status: 403, body: 'Forbidden' });
return;
}
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
await request.respond({ status: 404, body: 'Not found' });
return;
}
await request.respond({
status: 200,
contentType: contentType(filePath),
body: fs.readFileSync(filePath),
});
return;
}
if (url.hostname === 'tile.googleapis.com' && url.pathname.includes('/3dtiles/')) {
await request.respond({
status: 403,
contentType: 'application/json',
body: JSON.stringify({ error: { message: 'Offline dist capture: Google tiles unavailable' } }),
});
return;
}
await request.continue();
} catch {
try { await request.abort(); } catch { /* request already resolved */ }
}
});
}
async function installDeterministicDevEndpoints(page) {
if (DIST_DIR) return;
await page.setRequestInterception(true);
page.on('request', async (request) => {
const url = new URL(request.url());
if (url.origin !== new URL(APP_URL).origin) {
await request.continue();
return;
}
if (url.pathname === '/api/openai/hud-summary') {
await request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ summary: 'QA globe ready' }),
});
return;
}
if (url.pathname === '/api/ais-live') {
await request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ status: 'connected', rows: [], lastMessageAt: null }),
});
return;
}
await request.continue();
});
}
function round(value, digits = 3) {
if (!Number.isFinite(value)) return null;
const factor = 10 ** digits;
return Math.round(value * factor) / factor;
}
function percentile(values, fraction) {
const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
if (!sorted.length) return null;
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * fraction) - 1));
return sorted[index];
}
function summarizeFrameIntervals(intervals) {
const usable = intervals.filter((value) => Number.isFinite(value) && value > 0);
if (!usable.length) {
return { samples: 0, meanMs: null, p50Ms: null, p95Ms: null, p99Ms: null, maxMs: null };
}
const mean = usable.reduce((sum, value) => sum + value, 0) / usable.length;
return {
samples: usable.length,
meanMs: round(mean),
p50Ms: round(percentile(usable, 0.50)),
p95Ms: round(percentile(usable, 0.95)),
p99Ms: round(percentile(usable, 0.99)),
maxMs: round(Math.max(...usable)),
};
}
function numericDelta(after, before, key) {
return (after?.totals?.[key] || 0) - (before?.totals?.[key] || 0);
}
function inventoryDelta(before, after) {
return {
entities: numericDelta(after, before, 'entities'),
labelGraphics: numericDelta(after, before, 'labelGraphics'),
nonemptyLabelText: numericDelta(after, before, 'nonemptyLabelText'),
emptyLabelText: numericDelta(after, before, 'emptyLabelText'),
shownLabelGraphics: numericDelta(after, before, 'shownLabelGraphics'),
primitiveLabels: numericDelta(after, before, 'primitiveLabels'),
primitiveBillboards: numericDelta(after, before, 'primitiveBillboards'),
primitivePoints: numericDelta(after, before, 'primitivePoints'),
};
}
function printContext(context) {
console.log('\nWorld Overlay Phase 0 Baseline');
console.log(` URL : ${context.url}`);
console.log(` Machine : ${context.machine.hostname} · ${context.machine.platform} ${context.machine.release} · ${context.machine.arch}`);
console.log(` CPU / RAM : ${context.machine.cpuModel} · ${context.machine.logicalCpuCount} logical · ${context.machine.totalMemoryGiB} GiB`);
console.log(` Browser : ${context.browser.version}`);
console.log(` User agent : ${context.browser.userAgent}`);
console.log(` Viewport : ${context.viewport.width}×${context.viewport.height} CSS px · DPR ${context.viewport.dpr}`);
console.log(` WebGL : ${context.browser.webglVendor} · ${context.browser.webglRenderer}`);
console.log(` Capture : ${context.captureMode}`);
console.log(context.gpuMode === 'hardware'
? ' Timing note: hardware-GPU run; retain the renderer string with every comparison.\n'
: ' Timing note: SwiftShader absolute FPS is non-representative; compare motion/rest and before/after only.\n');
}
function printScene(result) {
const tag = result.status === 'ok' ? 'OK' : result.status === 'skipped' ? 'SKIP' : 'ERROR';
console.log(`[${tag}] ${result.scene}`);
if (result.reason) console.log(` reason : ${result.reason}`);
for (const activation of result.layerActivations || []) {
const delta = activation.delta;
console.log(` toggle ${activation.layerId.padEnd(31)} entities ${String(delta.entities).padStart(6)} · LabelGraphics ${String(delta.labelGraphics).padStart(6)} · primitive labels ${String(delta.primitiveLabels).padStart(6)}`);
}
if (result.entityInventory) {
const totals = result.entityInventory.totals;
console.log(` scene total : entities ${totals.entities} · LabelGraphics ${totals.labelGraphics} (${totals.shownLabelGraphics} show=true) · primitive labels ${totals.primitiveLabels}`);
}
for (const phaseName of ['motion', 'rest']) {
const phase = result.samples?.[phaseName];
if (!phase) continue;
const frame = phase.frameTime;
console.log(` ${phaseName.padEnd(6)} frame: n=${frame.samples} mean=${frame.meanMs} ms p50=${frame.p50Ms} ms p95=${frame.p95Ms} ms p99=${frame.p99Ms} ms`);
for (const canvas of phase.canvases || []) {
console.log(` ${canvas.id.padEnd(18)} frames≈${String(canvas.clearRectCalls).padStart(4)} fillText=${String(canvas.fillTextCalls).padStart(5)} drawImage=${String(canvas.drawImageCalls).padStart(5)} 2D-sync=${String(canvas.totalSyncMs).padStart(8)} ms`);
}
}
const detection = result.samples?.rest?.detectionDiagnostics;
if (detection) {
console.log(` detection : observations=${detection.observationCount ?? 'n/a'} candidates=${detection.candidateCount ?? 'n/a'} visible=${detection.visibleCount ?? 'n/a'} selected=${detection.selectedCount ?? 'n/a'} solve=${round(detection.solveMs)} ms paint=${round(detection.paintMs)} ms`);
}
const exposed = result.samples?.rest?.exposedCounts;
if (exposed && Object.values(exposed).some((value) => value != null)) {
console.log(` exposed : CCTV cards=${exposed.cctvOwnedEntries ?? 'n/a'}/${exposed.cctvEntryLimit ?? 'n/a'} · FIRMS objects=${exposed.firmsSourceObjects ?? 'n/a'} · vessel objects=${exposed.vesselSourceObjects ?? 'n/a'}`);
}
console.log('');
}
async function installPageInstrumentation(page) {
await page.evaluateOnNewDocument(() => {
const trackedMethods = [
'arc', 'arcTo', 'beginPath', 'clearRect', 'closePath', 'drawImage', 'fill',
'fillRect', 'fillText', 'lineTo', 'measureText', 'moveTo', 'quadraticCurveTo',
'rect', 'resetTransform', 'restore', 'rotate', 'roundRect', 'save', 'scale',
'setTransform', 'stroke', 'strokeRect', 'strokeText', 'translate',
];
const metrics = new Map();
const metricFor = (canvas) => {
const id = canvas?.id || '(anonymous-canvas)';
let metric = metrics.get(id);
if (!metric) {
metric = { id, calls: 0, totalSyncMs: 0, maxSyncMs: 0, methods: {} };
metrics.set(id, metric);
}
return metric;
};
const proto = globalThis.CanvasRenderingContext2D?.prototype;
if (proto) {
for (const method of trackedMethods) {
const original = proto[method];
if (typeof original !== 'function' || original.__gevBaselineWrapped) continue;
const wrapped = function(...args) {
const startedAt = performance.now();
try {
return Reflect.apply(original, this, args);
} finally {
const elapsed = performance.now() - startedAt;
const metric = metricFor(this.canvas);
metric.calls++;
metric.totalSyncMs += elapsed;
metric.maxSyncMs = Math.max(metric.maxSyncMs, elapsed);
const methodMetric = metric.methods[method] || (metric.methods[method] = { calls: 0, totalSyncMs: 0 });
methodMetric.calls++;
methodMetric.totalSyncMs += elapsed;
}
};
Object.defineProperty(wrapped, '__gevBaselineWrapped', { value: true });
try { proto[method] = wrapped; } catch { /* A non-writable method stays uninstrumented. */ }
}
}
globalThis.__overlayBaselineInstrumentation = {
reset() { metrics.clear(); },
snapshot() {
return Array.from(metrics.values()).map((metric) => ({
id: metric.id,
calls: metric.calls,
totalSyncMs: metric.totalSyncMs,
maxSyncMs: metric.maxSyncMs,
methods: JSON.parse(JSON.stringify(metric.methods)),
}));
},
};
});
}
async function waitForApp(page) {
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.waitForFunction(
() => Boolean(window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager),
{ timeout: 60_000 },
);
await page.evaluate(() => {
const viewer = window.__godsEyeView.viewer;
try { viewer.camera.cancelFlight(); } catch { /* no flight */ }
try { viewer.scene.tweens?.removeAll?.(); } catch { /* no tween collection */ }
});
await sleep(1_250);
}
async function setCamera(page, camera) {
if (!camera) return;
await page.evaluate(([lon, lat, height, heading, pitch]) => {
const viewer = window.__godsEyeView.viewer;
const ellipsoid = viewer.scene.globe.ellipsoid;
viewer.camera.cancelFlight?.();
viewer.scene.tweens?.removeAll?.();
viewer.camera.setView({
destination: ellipsoid.cartographicToCartesian({
longitude: lon * Math.PI / 180,
latitude: lat * Math.PI / 180,
height,
}),
orientation: { heading, pitch, roll: 0 },
});
viewer.scene.requestRender?.();
}, camera);
}
async function readLayerState(page, layerId) {
return page.evaluate((id) => {
const manager = window.__godsEyeView.dataManager;
const entry = manager.layers.get(id);
if (!entry) return null;
let stats = {};
try { stats = entry.initialized ? entry.module.getStats?.() || {} : {}; } catch (error) { stats = { error: error.message }; }
return { enabled: entry.enabled, initialized: entry.initialized, stats };
}, layerId);
}
async function waitForLayer(page, layerId) {
const dataBearing = new Set([
'local-datacenters', 'local-dams', 'telegeography-submarine-cables', 'cctv',
'local-firms', 'ais-live-vessels', 'flights', 'satellites', 'rocket-launches',
]);
if (!dataBearing.has(layerId)) return readLayerState(page, layerId);
try {
await page.waitForFunction((id) => {
const entry = window.__godsEyeView?.dataManager?.layers?.get(id);
if (!entry?.enabled || !entry.initialized) return false;
let stats;
try { stats = entry.module.getStats?.() || {}; } catch { return false; }
if (stats.loading === true) return false;
if (stats.error) return true;
return Number(stats.count || 0) > 0;
}, { timeout: LAYER_WAIT_MS, polling: 250 }, layerId);
} catch {
// The caller records the final state and decides whether live-data absence
// makes the specialized scene skippable.
}
return readLayerState(page, layerId);
}
async function readEntityInventory(page) {
return page.evaluate(() => {
const viewer = window.__godsEyeView.viewer;
const time = viewer.clock.currentTime;
const collections = [];
const totals = {
entities: 0,
labelGraphics: 0,
shownLabelGraphics: 0,
nonemptyLabelText: 0,
emptyLabelText: 0,
primitiveLabels: 0,
primitiveBillboards: 0,
primitivePoints: 0,
scenePrimitives: 0,
};
const propertyValue = (property, fallback) => {
if (property == null) return fallback;
try {
return typeof property.getValue === 'function' ? property.getValue(time) : property;
} catch {
return fallback;
}
};
const labelGroup = (entityId) => {
const id = String(entityId || '');
if (id.startsWith('rocket-launch:')) return 'mission-anchor';
if (id.startsWith('rocket-satellite:')) return 'mission-live-or-estimated';
if (id.startsWith('rocket-orbit-label:')) return 'mission-orbit';
if (id.startsWith('rocket-reentry-label:')) return 'mission-reentry';
if (id.startsWith('cctv-')) return 'cctv';
return id.includes(':') ? id.split(':', 1)[0] : '(other)';
};
const addCollection = (scope, name, entityCollection, collectionShown = true) => {
const entities = Array.from(entityCollection?.values || []);
let labelGraphics = 0;
let shownLabelGraphics = 0;
let nonemptyLabelText = 0;
let emptyLabelText = 0;
const labelGroups = {};
for (const entity of entities) {
if (!entity?.label) continue;
labelGraphics++;
const text = String(propertyValue(entity.label.text, '') || '');
if (text) nonemptyLabelText++;
else emptyLabelText++;
const entityShown = entity.show !== false && collectionShown;
const labelShown = Boolean(propertyValue(entity.label.show, true));
if (entityShown && labelShown) shownLabelGraphics++;
const group = labelGroup(entity.id);
labelGroups[group] = (labelGroups[group] || 0) + 1;
}
const row = {
scope,
name,
entities: entities.length,
labelGraphics,
shownLabelGraphics,
nonemptyLabelText,
emptyLabelText,
labelGroups,
};
collections.push(row);
totals.entities += row.entities;
totals.labelGraphics += row.labelGraphics;
totals.shownLabelGraphics += row.shownLabelGraphics;
totals.nonemptyLabelText += row.nonemptyLabelText;
totals.emptyLabelText += row.emptyLabelText;
};
addCollection('viewer', 'viewer.entities', viewer.entities, true);
for (let index = 0; index < viewer.dataSources.length; index++) {
const dataSource = viewer.dataSources.get(index);
addCollection('dataSource', dataSource.name || `dataSource:${index}`, dataSource.entities, dataSource.show !== false);
}
const primitiveTypes = {};
const seen = new Set();
const visitPrimitiveCollection = (collection, depth = 0) => {
if (!collection || depth > 5 || seen.has(collection)) return;
seen.add(collection);
const length = Number(collection.length || 0);
for (let index = 0; index < length; index++) {
let primitive;
try { primitive = collection.get(index); } catch { primitive = null; }
if (!primitive || seen.has(primitive)) continue;
const type = primitive.constructor?.name || '(anonymous)';
primitiveTypes[type] = (primitiveTypes[type] || 0) + 1;
totals.scenePrimitives++;
if (Array.isArray(primitive._labels)) totals.primitiveLabels += primitive._labels.length;
if (Array.isArray(primitive._billboards)) totals.primitiveBillboards += primitive._billboards.length;
if (Array.isArray(primitive._pointPrimitives)) totals.primitivePoints += primitive._pointPrimitives.length;
if (typeof primitive.length === 'number' && typeof primitive.get === 'function') {
visitPrimitiveCollection(primitive, depth + 1);
}
}
};
visitPrimitiveCollection(viewer.scene.primitives);
return { totals, collections, primitiveTypes };
});
}
async function activateLayers(page, layerIds) {
const activations = [];
for (const layerId of layerIds) {
const before = await readEntityInventory(page);
const toggleResult = await page.evaluate(async (id) => {
const manager = window.__godsEyeView.dataManager;
if (!manager.layers.has(id)) return { error: `unregistered layer: ${id}` };
if (!manager.isEnabled(id)) await manager.toggle(id);
return { enabled: manager.isEnabled(id) };
}, layerId);
const state = await waitForLayer(page, layerId);
const after = await readEntityInventory(page);
activations.push({
layerId,
toggleResult,
state,
delta: inventoryDelta(before, after),
collectionsAdded: after.collections.filter((row) => !before.collections.some((prior) => prior.scope === row.scope && prior.name === row.name)),
});
}
return activations;
}
async function prepareCctv(page, heightM) {
const camera = await page.evaluate((height) => {
const layer = window.__godsEyeView.dataManager.layers.get('cctv')?.module;
const state = layer?.getUIState?.();
const record = state?.cameras?.find((candidate) => Number.isFinite(candidate.lon) && Number.isFinite(candidate.lat));
if (!record) return null;
return [record.lon, record.lat, height, 0, -1.35];
}, heightM);
if (!camera) return 'CCTV catalog did not expose a geolocated camera';
await setCamera(page, camera);
await sleep(7_000);
return null;
}
async function prepareFirms(page) {
const result = await page.evaluate(() => {
const layer = window.__godsEyeView.dataManager.layers.get('local-firms')?.module;
const stats = layer?.getStats?.() || {};
const fire = layer?.getStrongestFire?.();
return { stats, fire };
});
if (!result.fire) return `FIRMS unavailable (${result.stats.error || 'no detections'})`;
await setCamera(page, [result.fire.longitude, result.fire.latitude, 60_000, 0, -1.45]);
await sleep(4_000);
return null;
}
async function prepareVessels(page) {
const state = await readLayerState(page, 'ais-live-vessels');
if (!(state?.stats?.count > 0)) return `AIS unavailable (${state?.stats?.error || 'no live vessels'})`;
await sleep(2_500);
return null;
}
async function prepareDetection(page, densityPct) {
const sourceStates = await Promise.all(['flights', 'satellites'].map((id) => readLayerState(page, id)));
if (!sourceStates.some((state) => (state?.stats?.count || 0) > 0)) {
return 'Detection sources exposed no observations';
}
const control = await page.evaluate((density) => window.__godsEyeView.styleManager.setDetection({
enabled: true,
densityPct: density,
}), densityPct);
if (!control?.ok) return `Detection control failed (${control?.error || 'unknown error'})`;
await sleep(2_000);
return null;
}
async function prepareTrackedFlight(page, cockpit) {
if (cockpit) {
const contacts = await page.evaluate(() => window.__godsEyeView.styleManager.setContextMode(
'contacts',
{ origin: 'user' },
));
if (!contacts?.ok) {
return `Contacts activation failed (${contacts?.error || 'unknown error'})`;
}
}
const tracked = await page.evaluate(() => {
const entry = window.__godsEyeView.dataManager.layers.get('flights');
const layer = entry?.module;
const candidates = layer?.getAllPositions?.(500) || [];
const airborne = candidates.find((candidate) => Number(candidate.altitudeM) > 1_000) || candidates[0];
if (!airborne) return { ok: false, reason: 'no rendered civil aircraft' };
const ok = layer.trackById?.(airborne.id);
return { ok: Boolean(ok), id: airborne.id, label: airborne.label, reason: ok ? null : 'trackById rejected candidate' };
});
if (!tracked.ok) return `Civil tracking unavailable (${tracked.reason})`;
try {
await page.waitForFunction(() => Boolean(window.__godsEyeView.viewer.trackedEntity?.position), { timeout: 10_000 });
} catch {
return 'Civil tracking did not create a tracked entity';
}
await sleep(2_000);
if (!cockpit) return null;
const entryAvailable = await page.evaluate(() => {
const button = document.getElementById('cockpit-entry');
if (!button || button.hidden || button.disabled) return false;
button.click();
return true;
});
if (!entryAvailable) return 'Cockpit entry control was unavailable for the tracked aircraft';
try {
await page.waitForFunction(() => document.body.classList.contains('cockpit-mode'), { timeout: 5_000 });
} catch {
return 'Cockpit entry control did not enter cockpit mode';
}
await sleep(2_000);
return null;
}
async function prepareMission(page) {
const state = await readLayerState(page, 'rocket-launches');
if (!(state?.stats?.count > 0)) return `Mission feed unavailable (${state?.stats?.error || 'no missions'})`;
const selected = await page.evaluate(() => {
const button = document.querySelector('[data-mission-roster-index="0"]');
if (!button) return false;
button.click();
return true;
});
if (!selected) return 'Mission roster did not expose a selectable mission';
try {
await page.waitForFunction(() => String(window.__godsEyeView.viewer.selectedEntity?.id || '').startsWith('rocket-launch:'), { timeout: 10_000 });
} catch {
return 'Mission selection did not reach the Cesium selected entity';
}
await sleep(2_000);
return null;
}
async function prepareScene(page, scene) {
if (scene.camera) await setCamera(page, scene.camera);
const layerActivations = await activateLayers(page, scene.layers);
let skipReason = null;
if (scene.cctvHeightM) skipReason = await prepareCctv(page, scene.cctvHeightM);
if (!skipReason && scene.id === 'firms') skipReason = await prepareFirms(page);
if (!skipReason && scene.id === 'vessels') skipReason = await prepareVessels(page);
if (!skipReason && scene.detectionDensity) skipReason = await prepareDetection(page, scene.detectionDensity);
if (!skipReason && scene.trackedFlight) skipReason = await prepareTrackedFlight(page, scene.cockpit);
if (!skipReason && scene.missionSelected) skipReason = await prepareMission(page);
await sleep(DEFAULT_SETTLE_MS);
return { layerActivations, skipReason };
}
async function readCanvasSummary(page) {
return page.evaluate(() => Array.from(document.querySelectorAll('canvas')).map((canvas) => {
const style = getComputedStyle(canvas);
return {
id: canvas.id || '(anonymous-canvas)',
width: canvas.width,
height: canvas.height,
cssWidth: canvas.clientWidth,
cssHeight: canvas.clientHeight,
display: style.display,
opacity: style.opacity,
zIndex: style.zIndex,
};
}));
}
async function samplePhase(page, moving) {
const raw = await page.evaluate(async ({ durationMs, movingCamera }) => {
const viewer = window.__godsEyeView.viewer;
const instrumentation = window.__overlayBaselineInstrumentation;
instrumentation?.reset?.();
const intervals = [];
const startedAt = performance.now();
let lastFrameAt = null;
await new Promise((resolve) => {
const step = (now) => {
if (lastFrameAt != null) intervals.push(now - lastFrameAt);
lastFrameAt = now;
const elapsed = now - startedAt;
if (movingCamera) {
viewer.camera.rotateRight(0.00035);
viewer.camera.rotateUp(0.00008 * Math.sin(elapsed / 600));
}
viewer.scene.requestRender?.();
if (elapsed >= durationMs) resolve();
else requestAnimationFrame(step);
};
requestAnimationFrame(step);
});
const canvasMetrics = instrumentation?.snapshot?.() || [];
const detectionDiagnostics = window.__godsEyeView.styleManager?.getDetectionDiagnostics?.() || null;
const manager = window.__godsEyeView.dataManager;
const cctvEntry = manager.layers.get('cctv');
const cctvState = cctvEntry?.initialized ? cctvEntry.module.getUIState?.() : null;
const firmsEntry = manager.layers.get('local-firms');
const vesselsEntry = manager.layers.get('ais-live-vessels');
return {
intervals,
canvasMetrics,
detectionDiagnostics,
exposedCounts: {
cctvOwnedEntries: cctvState?.ambientCards?.count ?? null,
cctvEntryLimit: cctvState?.ambientCards?.limit ?? null,
firmsSourceObjects: firmsEntry?.initialized ? firmsEntry.module.getStats?.()?.count ?? null : null,
vesselSourceObjects: vesselsEntry?.initialized ? vesselsEntry.module.getStats?.()?.count ?? null : null,
},
elapsedMs: performance.now() - startedAt,
};
}, { durationMs: SAMPLE_MS, movingCamera: moving });
const canvases = raw.canvasMetrics
.filter((metric) => KNOWN_OVERLAY_CANVASES.has(metric.id))
.map((metric) => ({
id: metric.id,
calls: metric.calls,
totalSyncMs: round(metric.totalSyncMs),
maxSyncMs: round(metric.maxSyncMs),
clearRectCalls: metric.methods.clearRect?.calls || 0,
fillTextCalls: metric.methods.fillText?.calls || 0,
drawImageCalls: metric.methods.drawImage?.calls || 0,
measureTextCalls: metric.methods.measureText?.calls || 0,
strokeCalls: metric.methods.stroke?.calls || 0,
fillCalls: metric.methods.fill?.calls || 0,
methods: Object.fromEntries(Object.entries(metric.methods).map(([name, value]) => [name, {
calls: value.calls,
totalSyncMs: round(value.totalSyncMs),
}])),
}))
.sort((a, b) => a.id.localeCompare(b.id));
return {
requestedDurationMs: SAMPLE_MS,
actualDurationMs: round(raw.elapsedMs),
frameTime: summarizeFrameIntervals(raw.intervals),
canvases,
detectionDiagnostics: raw.detectionDiagnostics,
exposedCounts: raw.exposedCounts,
};
}
async function captureShot(page, shotsDir, sceneId, suffix) {
if (!shotsDir) return null;
fs.mkdirSync(shotsDir, { recursive: true });
const safeScene = sceneId.replace(/[^a-z0-9-]+/gi, '-');
const outPath = path.join(shotsDir, `${safeScene}-${suffix}.png`);
await page.screenshot({ path: outPath });
return outPath;
}
async function runScene(browser, scene, shotsDir) {
const page = await browser.newPage();
await page.setViewport({ ...VIEWPORT, deviceScaleFactor: DPR });
await installStaticDist(page, DIST_DIR);
await installDeterministicDevEndpoints(page);
const consoleMessages = [];
page.on('console', (message) => {
if (['error', 'warning', 'warn'].includes(message.type())) {
consoleMessages.push({ type: message.type(), text: message.text() });
}
});
page.on('pageerror', (error) => consoleMessages.push({ type: 'pageerror', text: error.message }));
await installPageInstrumentation(page);
const result = {
scene: scene.id,
status: 'ok',
reason: null,
layerActivations: [],
entityInventory: null,
canvasInventory: [],
samples: null,
screenshots: [],
consoleMessages,
};
try {
await waitForApp(page);
const prepared = await prepareScene(page, scene);
result.layerActivations = prepared.layerActivations;
result.entityInventory = await readEntityInventory(page);
result.canvasInventory = await readCanvasSummary(page);
if (prepared.skipReason) {
result.status = 'skipped';
result.reason = prepared.skipReason;
return result;
}
const beforeShot = await captureShot(page, shotsDir, scene.id, 'before-motion');
if (beforeShot) result.screenshots.push(beforeShot);
const motion = await samplePhase(page, true);
const afterMotionShot = await captureShot(page, shotsDir, scene.id, 'after-motion');
if (afterMotionShot) result.screenshots.push(afterMotionShot);
await sleep(BETWEEN_PHASE_SETTLE_MS);
const rest = await samplePhase(page, false);
result.samples = { motion, rest };
result.entityInventoryAfter = await readEntityInventory(page);
return result;
} catch (error) {
result.status = 'error';
result.reason = error?.stack || error?.message || String(error);
return result;
} finally {
await page.close();
}
}
const APP_URL = getOpt('--url', DEFAULT_URL);
const DPR = Number(getOpt('--dpr', '1'));
const JSON_PATH = getOpt('--json');
const SCREENSHOTS_DIR = getOpt('--screenshots-dir');
const DIST_DIR = getOpt('--dist-dir');
const HEADFUL = hasFlag('--headful');
const HARDWARE_GPU = hasFlag('--hardware-gpu');
function softwareRenderer(renderer) {
return /swiftshader|software|llvmpipe/i.test(String(renderer || ''));
}
function unavailableRenderer(renderer) {
return !String(renderer || '').trim() || String(renderer).toLowerCase() === 'unavailable';
}
async function main() {
if (!fs.existsSync(CHROME_EXECUTABLE)) {
throw new Error(`Required Chrome executable not found: ${CHROME_EXECUTABLE}`);
}
if (!Number.isFinite(DPR) || DPR <= 0) throw new Error(`Invalid --dpr value: ${DPR}`);
const scenes = selectedScenes();
if (DIST_DIR && (!fs.existsSync(DIST_DIR) || !fs.statSync(DIST_DIR).isDirectory())) {
throw new Error(`--dist-dir is not a directory: ${DIST_DIR}`);
}
if (!DIST_DIR) {
const response = await fetch(APP_URL).catch((error) => ({ ok: false, statusText: error.message }));
if (!response.ok) throw new Error(`Dev server unavailable at ${APP_URL}: ${response.status || response.statusText}`);
}
const browser = await puppeteer.launch({
executablePath: CHROME_EXECUTABLE,
headless: HEADFUL ? false : 'new',
args: [
...(HARDWARE_GPU ? [] : ['--enable-unsafe-swiftshader', '--use-gl=swiftshader']),
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
`--window-size=${VIEWPORT.width},${VIEWPORT.height}`,
],
});
const run = { generatedAt: new Date().toISOString(), context: null, scenes: [] };
try {
const contextPage = await browser.newPage();
await contextPage.setViewport({ ...VIEWPORT, deviceScaleFactor: DPR });
await installStaticDist(contextPage, DIST_DIR);
await contextPage.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60_000 });
const browserContext = await contextPage.evaluate(() => {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
const debug = gl?.getExtension('WEBGL_debug_renderer_info');
return {
userAgent: navigator.userAgent,
dpr: window.devicePixelRatio,
webglVendor: debug ? gl.getParameter(debug.UNMASKED_VENDOR_WEBGL) : gl?.getParameter(gl.VENDOR) || 'unavailable',
webglRenderer: debug ? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL) : gl?.getParameter(gl.RENDERER) || 'unavailable',
};
});
await contextPage.close();
const cpus = os.cpus();
if (HARDWARE_GPU && (
unavailableRenderer(browserContext.webglRenderer)
|| softwareRenderer(browserContext.webglRenderer)
)) {
throw new Error(`Hardware GPU requested but Chrome reported ${browserContext.webglRenderer}`);
}
run.context = {
url: APP_URL,
machine: {
hostname: os.hostname(),
platform: os.platform(),
release: os.release(),
arch: os.arch(),
cpuModel: cpus[0]?.model || 'unknown',
logicalCpuCount: cpus.length,
totalMemoryGiB: round(os.totalmem() / (1024 ** 3), 1),
},
browser: {
version: await browser.version(),
userAgent: browserContext.userAgent,
webglVendor: browserContext.webglVendor,
webglRenderer: browserContext.webglRenderer,
},
viewport: { ...VIEWPORT, dpr: browserContext.dpr },
gpuMode: HARDWARE_GPU ? 'hardware' : 'swiftshader',
chromeExecutable: CHROME_EXECUTABLE,
chromeArgs: HARDWARE_GPU
? ['--no-sandbox']
: ['--enable-unsafe-swiftshader', '--use-gl=swiftshader', '--no-sandbox'],
sampleMs: SAMPLE_MS,
captureMode: DIST_DIR ? `static-dist:${path.resolve(DIST_DIR)}` : 'dev-server',
};
printContext(run.context);
for (const scene of scenes) {
const result = await runScene(browser, scene, SCREENSHOTS_DIR);
run.scenes.push(result);
printScene(result);
}
} finally {
await browser.close();
}
run.summary = {
ok: run.scenes.filter((scene) => scene.status === 'ok').length,
skipped: run.scenes.filter((scene) => scene.status === 'skipped').length,
errors: run.scenes.filter((scene) => scene.status === 'error').length,
};
console.log(`Summary: ${run.summary.ok} measured · ${run.summary.skipped} skipped · ${run.summary.errors} errors`);
if (JSON_PATH) {
fs.mkdirSync(path.dirname(path.resolve(JSON_PATH)), { recursive: true });
fs.writeFileSync(JSON_PATH, `${JSON.stringify(run, null, 2)}\n`);
console.log(`JSON: ${path.resolve(JSON_PATH)}`);
}
if (run.summary.errors > 0) process.exitCode = 1;
if (HARDWARE_GPU && run.summary.ok === 0) {
console.error('Hardware GPU run produced no measured scenes.');
process.exitCode = 1;
}
}
main().catch((error) => {
console.error(error?.stack || error);
process.exit(1);
});
+413
View File
@@ -0,0 +1,413 @@
#!/usr/bin/env node
/**
* qa-perf — render-governor regression gate (perf wave 2).
*
* Asserts the governor's observable contract with RELATIVE frame-count
* assertions (SwiftShader-safe; no wall-clock GPU numbers):
*
* 1. Idle + zero layers + parked camera → the scene stops rendering
* (near-zero postRender fires over a settle-then-observe window).
* 2. A discrete mutation while idle (style slider write routed through
* governorRequestRender) → at least one render, then settles again.
* 3. Camera movement while idle → renders happen (Cesium-native path).
* 4. Flights enabled → continuous mode (postRender cadence ≈ rAF cadence,
* and ≥5× the idle count over the same window).
* 5. Flights disabled again → back to idle (near-zero fires).
* 6. Governor diagnostics agree with the mode at every step.
*
* ── WHAT THE IDLE WINDOWS MEASURE, AND WHY THEY WAIT ────────────────────────
*
* The idle checks (1 and 5) measure the STEADY-STATE floor: what a parked scene
* costs once it has settled. They do not measure how long settling takes, so the
* counted window has to begin AFTER settling — and "after" is a stronger
* condition than "nothing is happening right now".
*
* That distinction became load-bearing when detection stopped holding the render
* loop open (2026-08-22) and became a default. With a paint lane active, the
* world-overlay host honours its occluder observers with frames: chrome that
* changes SIZE resizes an occluder, `markLayoutDirty` fires, and that becomes a
* `requestRender`. While detection forced continuous mode this was invisible.
* Event-driven, it shows up as renders.
*
* The specific contaminant, pinned with stack traces on this tree, is triggered
* by THIS HARNESS'S OWN SETUP. Disabling every layer publishes a `visibility`
* change; the HUD subscribes to that and marks its semantic summary dirty
* (`src/hud.js`), and the summary refreshes on a 15-SECOND interval
* (`HUD_SUMMARY_INTERVAL_MS`). So up to 15 s after the teardown a tick fetches a
* new summary and TYPES it in with a typewriter animation; the growing text
* reflows `.hud-corner.hud-top-left`, an occluder, and the host turns that
* reflow into a handful of frames. Two instrumented runs (150 s and 180 s
* watches) agree to the second: the teardown lands at 15 s, the burst at 31 s —
* seven frames in that one second — which is 16 s after the teardown and 4 s
* INTO the window the old settle had already started counting. Neither run then
* saw a single further render, across 146 s and 176 s respectively. It is a
* one-shot: the tick that fetches also clears the summary's dirty flag and
* commits its context signature, so every later tick returns early.
*
* Waiting for "an empty second" cannot survive that, because the scene really IS
* empty for the ~15 s between the teardown and the tick that notices it. So the
* settle below requires a quiet RUN LONGER THAN ONE FULL SUMMARY TICK, and that
* run resets on any activity. Quiet then means something stronger and checkable:
* a complete refresh period has elapsed in which the app looked at its new state
* and found nothing to do. The length is derived from the app's own cadence
* rather than tuned against a flake rate — if that interval changes, this moves
* with it.
*
* The threshold inside the window (≤4 fires / 5 s) is untouched. The teeth live
* in the WAIT, not the threshold: a scene rendering every frame never produces an
* empty second at all, so the bounded wait expires and FAILS. Quiet that never
* arrives IS the failure, which is why the timeout is an assertion rather than a
* fall-through — and it is what keeps this honest if the churn ever stops being a
* one-shot. It would: when `/api/openai/hud-summary` is unreachable the summary
* stays dirty and re-types every 15 s, so that machine gets a loud repeatable
* failure here rather than a coin flip.
*
* The underlying fix belongs to the host, not to this harness: an active lane
* with nothing to place is not paint work and should not be honoured with a
* frame. That is worldOverlay surgery — see the post-launch ledger entry
* "world-overlay honours occluder churn as paint work" in
* the performance contract in `docs/CURRENT-STATE.md`.
*
* Usage: node scripts/qa-perf.mjs [--url http://localhost:4173]
* Requires a running dev server. Headless; flags disable occlusion
* throttling so rAF cadence is trustworthy (hidden-pane gotcha).
*/
import puppeteer from 'puppeteer';
const argv = process.argv;
const url = argv.includes('--url') ? argv[argv.indexOf('--url') + 1] : 'http://localhost:4173';
const results = [];
function check(name, pass, detail) {
results.push({ name, pass });
const tag = pass ? 'PASS' : 'FAIL';
console.log(` [${tag}] ${name}${detail ? ` — ${JSON.stringify(detail)}` : ''}`);
}
const browser = await puppeteer.launch({
headless: 'new',
protocolTimeout: 300_000,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--window-size=1440,900',
// Never let background/occlusion throttling freeze rAF or timers — the
// measurements below depend on an honest frame clock.
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-background-timer-throttling',
],
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 860 });
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 90_000 });
// Boot flyTo + tile warm + all deferred init.
await new Promise((r) => setTimeout(r, 15_000));
// Park deterministically and disable every layer.
await page.evaluate(async () => {
const gev = window.__godsEyeView;
const v = gev.viewer;
v.camera.cancelFlight();
const ell = v.scene.globe.ellipsoid;
v.camera.setView({
destination: ell.cartographicToCartesian({
longitude: -97.74 * Math.PI / 180, latitude: 30.27 * Math.PI / 180, height: 60_000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
for (const [id, entry] of gev.dataManager.layers) {
if (entry.enabled) { try { await gev.dataManager.setEnabled(id, false, { origin: 'user' }); } catch { /* gate reports via counts */ } }
}
});
// Let tiles finish + fades settle + the settling frames drain.
await new Promise((r) => setTimeout(r, 12_000));
/** Count scene postRender fires and rAF ticks over windowMs. */
const countFrames = (windowMs) => page.evaluate((ms) => new Promise((resolve) => {
const scene = window.__godsEyeView.viewer.scene;
let renders = 0; let rafs = 0;
const remove = scene.postRender.addEventListener(() => { renders += 1; });
const t0 = performance.now();
const tick = () => {
rafs += 1;
if (performance.now() - t0 < ms) requestAnimationFrame(tick);
else { remove(); resolve({ renders, rafs }); }
};
requestAnimationFrame(tick);
}), windowMs);
const diag = () => page.evaluate(() => window.__godsEyeView.getRenderGovernorDiagnostics?.()
|| window.__gevRenderGovernor?.getDiagnostics?.() || null);
/**
* The HUD's semantic summary refreshes on this cadence (`src/hud.js`,
* `HUD_SUMMARY_INTERVAL_MS`). A layer-visibility change marks it dirty, and
* the tick that picks that up types the new text in — reflowing an occluder
* and, with a paint lane active, costing frames. Mirrored rather than
* imported because this harness runs against the built app, not the module.
*/
const HUD_SUMMARY_INTERVAL_MS = 15_000;
/** One full refresh period, plus a second of margin, in 1 s windows. */
const QUIET_RUN_WINDOWS = Math.ceil(HUD_SUMMARY_INTERVAL_MS / 1_000) + 1;
/**
* Wait until the scene is genuinely settled, and report whether it ever was.
* This is the START of the idle windows below — see the header.
*
* Requires a CONSECUTIVE RUN of empty one-second windows at least one full
* summary refresh period long, and resets that run on any activity. One empty
* window — or three — proves nothing here: the scene is genuinely idle for the
* whole gap between a teardown and the tick that notices it, so a short
* confirmation happily reports "quiet" moments before the typewriter fires.
* A run longer than the period cannot straddle that gap: it either contains
* the burst, and restarts, or it postdates it.
*
* `quiet: false` is a FAILURE, not a fall-through: a scene rendering every
* frame never produces an empty window at all, so this is what catches a real
* hot loop — and equally a contaminant that has stopped being a one-shot. The
* bound stays comfortably longer than settling takes and comfortably shorter
* than forever: worst case is one interrupted run, the burst itself, then a
* clean run (~55 s), against a 120 s ceiling.
*/
const settleUntilQuiet = async (maxMs = 120_000, requiredConsecutive = QUIET_RUN_WINDOWS) => {
const deadline = Date.now() + maxMs;
let windows = 0;
let consecutive = 0;
let busiest = 0;
let restarts = 0;
while (Date.now() < deadline) {
windows += 1;
const { renders } = await countFrames(1_000);
busiest = Math.max(busiest, renders);
if (renders === 0) consecutive += 1;
else { if (consecutive > 0) restarts += 1; consecutive = 0; }
if (consecutive >= requiredConsecutive) {
return { quiet: true, windows, busiest, restarts, ranFor: consecutive, needRun: requiredConsecutive };
}
}
// A failure here reads as "never held N empty seconds in a row, best run was
// M, restarted R times, busiest window was B" — enough to tell a hot loop
// from a contaminant that has stopped being a one-shot.
return { quiet: false, windows, busiest, restarts, ranFor: consecutive, needRun: requiredConsecutive };
};
// ── 1. idle: near-zero renders ────────────────────────────────────────
const idleSettle = await settleUntilQuiet();
const idle = await countFrames(5_000);
const d1 = await diag();
// Quiet that never arrives is the hot-loop failure — assert it, do not skip it.
check('the parked scene holds a settled quiet run before the idle window is counted', idleSettle.quiet, idleSettle);
check('governor reports idle mode with zero layers', d1?.mode === 'idle', d1);
check('idle parked scene stops rendering (≤4 fires / 5s)', idle.renders <= 4, idle);
// ── 1b. detection at its FIRST-RUN DEFAULT must not hold the parked scene ─
//
// Detection became ON by default on 2026-08-22. It used to take an
// unconditional continuous-render hold whenever it was on — nobody felt that
// while it defaulted OFF, but as a DEFAULT it would pin every idle first-run
// tab at 60 fps forever, defeating this governor outright. The hold is gone
// (`src/data/detectionRenderDemand.js`): detection repaints on change and asks
// for single frames only while a bounded animation is running.
//
// This is the gate for that. The scene above is parked with zero layers, so
// detection-on and detection-off must yield the SAME near-zero render count.
const detectionDefault = await page.evaluate(
() => window.__godsEyeView.styleManager.getDetectionState?.() || null,
);
check(
'precondition: detection is ON at its first-run default (Dense @ 75)',
detectionDefault?.detectionMode === 'DENSE' && detectionDefault?.densityPct === 75,
detectionDefault,
);
check(
'detection ON takes NO continuous-render hold',
d1?.mode === 'idle' && !d1.holds.includes('detection'),
d1,
);
// The control: the same window with detection explicitly OFF.
await page.evaluate(() => { window.__godsEyeView.styleManager._setDetectionMode('OFF'); });
await new Promise((r) => setTimeout(r, 1_500)); // let any fade chain terminate
const idleDetectOff = await countFrames(5_000);
check('idle baseline with detection OFF (≤4 fires / 5s)', idleDetectOff.renders <= 4, idleDetectOff);
// Back to the default. A regression here is the entire point of this gate: the
// old hold produced a full 60 fps window instead of near-zero.
await page.evaluate(() => { window.__godsEyeView.styleManager._setDetectionMode('DENSE'); });
await new Promise((r) => setTimeout(r, 1_500));
const idleDetectOn = await countFrames(5_000);
const dDetect = await diag();
check('idle parked scene with detection ON (≤4 fires / 5s)', idleDetectOn.renders <= 4, idleDetectOn);
check(
'detection ON costs no more idle frames than detection OFF',
idleDetectOn.renders <= idleDetectOff.renders + 2,
{ on: idleDetectOn.renders, off: idleDetectOff.renders },
);
check(
'governor still idle with detection ON',
dDetect?.mode === 'idle' && !dDetect.holds.includes('detection'),
dDetect,
);
// …and the overlay must still be LIVE, not merely quiet: with detection on, a
// camera nudge has to repaint promptly, or "idle" would only mean "stale".
const detectMove = await Promise.all([
countFrames(2_500),
page.evaluate(() => new Promise((resolve) => {
const v = window.__godsEyeView.viewer;
let steps = 0;
const id = setInterval(() => {
v.camera.moveForward(50);
steps += 1;
if (steps >= 20) { clearInterval(id); resolve(true); }
}, 60);
})),
]).then(([frames]) => frames);
check(
'detection ON still repaints promptly on camera motion (≥10 / 2.5s)',
detectMove.renders >= 10,
detectMove,
);
// A render count alone proves the SCENE rendered, not that the detection
// painter ran — a painter disabled outright would score a perfect idle and
// sail through every check above. Count the painter's own frames across the
// same kind of motion, so the teeth reach the thing this change touched.
const detectPainted = await page.evaluate(() => new Promise((resolve) => {
const gev = window.__godsEyeView;
const before = gev.styleManager.getDetectionDiagnostics?.()?.frameCount ?? null;
let paints = 0;
// The diagnostics object is rebuilt on every detection paint, so a fresh
// identity is one painted frame. Sampling it per scene frame is enough to
// tell "painting" from "silent" without reaching into module internals.
let last = gev.styleManager.getDetectionDiagnostics?.();
const remove = gev.viewer.scene.postRender.addEventListener(() => {
const now = gev.styleManager.getDetectionDiagnostics?.();
if (now && now !== last) { paints += 1; last = now; }
});
let steps = 0;
const id = setInterval(() => {
gev.viewer.camera.moveForward(50);
steps += 1;
if (steps >= 20) {
clearInterval(id);
setTimeout(() => {
remove();
resolve({ paints, before, after: gev.styleManager.getDetectionDiagnostics?.()?.frameCount ?? null });
}, 400);
}
}, 60);
}));
check(
'detection is still PAINTING, not merely quiet (≥5 painted frames on motion)',
detectPainted.paints >= 5,
detectPainted,
);
await new Promise((r) => setTimeout(r, 3_000)); // settle back to parked
// ── 2. a REAL slider mutation renders (full UI → uniform → request path) ─
const afterMutation = await Promise.all([
countFrames(2_500),
page.evaluate(() => new Promise((resolve) => setTimeout(() => {
// Drive the actual sharpen slider: input event → handler →
// _applySharpenIntensity → governorRequestRender. Proves the wiring,
// not just the facade.
const slider = document.getElementById('sharpen-intensity-slider');
if (!slider) { resolve({ ok: false }); return; }
slider.value = String(Math.min(100, Number(slider.value) + 7));
slider.dispatchEvent(new Event('input', { bubbles: true }));
resolve({ ok: true });
}, 500))),
]).then(([frames]) => frames);
check('real slider mutation while idle renders ≥1 and ≤10 frames', afterMutation.renders >= 1 && afterMutation.renders <= 10, afterMutation);
// ── 2b. animated style cycle: style-anim holds, then releases ─────────
await page.evaluate(() => { window.__godsEyeView.styleManager.setStyle('retro'); });
await new Promise((r) => setTimeout(r, 900)); // crossfade + first ticks
const dAnim = await diag();
check('animated style takes the style-anim hold (continuous)', dAnim?.mode === 'continuous' && dAnim.holds.includes('style-anim'), dAnim);
const animFrames = await countFrames(2_000);
check('animated style keeps frames flowing (≥50% rAF)', animFrames.renders >= animFrames.rafs * 0.5, animFrames);
// Returning to normal keeps detection ON by design (it is the default, and
// detection persists across style switches). Since 2026-08-22 detection holds
// nothing, so this asserts the strictly harder thing: the scene returns to
// idle with detection still ON — where before it could only go idle by also
// turning detection off.
await page.evaluate(() => { window.__godsEyeView.styleManager.setStyle('normal'); });
await new Promise((r) => setTimeout(r, 1_500)); // fade out + loop self-stop
const dAnimOff = await diag();
const detectionStillOn = await page.evaluate(
() => window.__godsEyeView.styleManager.getDetectionState?.()?.detectionMode || null,
);
check(
'style-anim hold releases and the scene goes idle with detection still ON',
dAnimOff?.mode === 'idle'
&& !dAnimOff.holds.includes('style-anim')
&& !dAnimOff.holds.includes('detection')
&& detectionStillOn !== 'OFF',
{ diag: dAnimOff, detection: detectionStillOn },
);
// ── 2c. satellites holder enters and leaves diagnostics ───────────────
await page.evaluate(async () => {
await window.__godsEyeView.dataManager.setEnabled('satellites', true, { origin: 'user' });
});
const dSat = await diag();
check('satellites enable registers its holder', dSat?.holds.includes('satellites'), dSat);
await page.evaluate(async () => {
await window.__godsEyeView.dataManager.setEnabled('satellites', false, { origin: 'user' });
});
await new Promise((r) => setTimeout(r, 2_000));
const dSatOff = await diag();
check('satellites disable releases its holder', !dSatOff?.holds.includes('satellites'), dSatOff);
// ── 3. camera movement renders (Cesium-native path) ───────────────────
const duringMove = await Promise.all([
countFrames(2_500),
page.evaluate(() => new Promise((resolve) => {
const v = window.__godsEyeView.viewer;
let steps = 0;
const id = setInterval(() => {
v.camera.moveForward(50);
steps += 1;
if (steps >= 20) { clearInterval(id); resolve(true); }
}, 60);
})),
]).then(([frames]) => frames);
check('camera movement while idle produces renders (≥10 / 2.5s)', duringMove.renders >= 10, duringMove);
await new Promise((r) => setTimeout(r, 3_000)); // settle
// ── 4. flights enabled → continuous ───────────────────────────────────
await page.evaluate(async () => {
await window.__godsEyeView.dataManager.setEnabled('flights', true, { origin: 'user' });
});
await new Promise((r) => setTimeout(r, 5_000));
const active = await countFrames(5_000);
const d4 = await diag();
check('governor reports continuous mode with flights on', d4?.mode === 'continuous', d4);
check('flights-on cadence ≈ rAF cadence (≥70%)', active.renders >= active.rafs * 0.7, active);
check('flights-on renders ≥5× idle renders', active.renders >= Math.max(1, idle.renders) * 5, { active: active.renders, idle: idle.renders });
// ── 5. flights disabled → idle again ──────────────────────────────────
await page.evaluate(async () => {
await window.__godsEyeView.dataManager.setEnabled('flights', false, { origin: 'user' });
});
// Deselect flows, fades, and the chrome churn the overlay host re-evaluates
// its occluders against all have to drain first — and this teardown, like the
// one in the setup above, marks the HUD summary dirty, so the same deferred
// typewriter is still ahead of us. Same rule: hold a quiet run longer than one
// refresh period before counting anything.
const teardownSettle = await settleUntilQuiet();
const idleAgain = await countFrames(5_000);
const d5 = await diag();
check('the scene holds a settled quiet run again before the second idle window', teardownSettle.quiet, teardownSettle);
check('governor returns to idle after disable', d5?.mode === 'idle', d5);
check('scene stops rendering again (≤4 fires / 5s)', idleAgain.renders <= 4, idleAgain);
} finally {
await browser.close();
}
const passed = results.filter((r) => r.pass).length;
console.log(`\nqa-perf: ${passed}/${results.length} passed`);
console.log(`RESULT: ${passed} passed, ${results.length - passed} failed, 0 skipped`);
process.exit(passed === results.length ? 0 : 1);
+4625
View File
File diff suppressed because it is too large. Load diff
+578
View File
@@ -0,0 +1,578 @@
#!/usr/bin/env node
/**
* qa-sprites-b5.mjs — throwaway QA harness for skylight Tasks 8+9
* (type-aware billboard silhouettes + per-class 2D/3D scale; Batch 5).
*
* Injects synthetic STRAIGHT-FLYING aircraft covering ALL 8 classifyAircraft
* classes — the flights layer via OpenSky extended categories (state[17]),
* the military layer via adsb.lol type codes — using the same fetch-shim +
* history-priming pattern as scripts/qa-heading-b3.mjs (positions analytic at
* serve time so every fix is self-consistent through the render delay).
*
* Machine assertions:
* B1 ingest : both layers ingest the synthetic planes
* B2 billboards : every fleet billboard carries EXACTLY the expected
* per-class glyph (image data-URI === aircraftIcon(klass),
* imported from src) and per-class scale (flights:
* CLASS_SCALE_2D; military: 0.7 × CLASS_SCALE_2D)
* B3 models : with 3D on, fleet Cesium.Model.scale === the layer's
* MODEL_SCALE (both 1; legacy scale is baked into the GLB)
* × CLASS_SCALE_3D[klass] per icao; tracked too
* B4 clean : no console errors
*
* Visual output (qa-shots/b5/, untracked): top-down 2D spreads of both rows,
* per-row close-ups, a tracked-cyan tint shot, and 3D cluster shots at two
* zooms per layer showing per-class model scale differences. The PNGs are the
* acceptance test — read them.
*
* Run: node scripts/qa-sprites-b5.mjs --url http://localhost:4300
* Exits non-zero if any assertion fails. Never commits anything.
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { classifyAircraft, CLASS_SCALE_2D, CLASS_SCALE_3D, CLASS_MODEL_REAL } from '../src/data/aircraftClass.js';
// Hangar fleet (2026-08-16): real per-class GLBs render at scale 1; military
// heavies (airliner/quadjet/glider) render airplane.glb at 1 x class; only
// fastjet/unknown keep the per-layer jet/airplane MODEL_SCALE formula.
const flightsWantScale = (klass) => (CLASS_MODEL_REAL[klass] ? 1 : FL_MODEL_SCALE * CLASS_SCALE_3D[klass]);
const MIL_PLANE_CLASSES = new Set(['airliner', 'quadjet', 'glider']);
const militaryWantScale = (klass) => (CLASS_MODEL_REAL[klass] ? 1
: MIL_PLANE_CLASSES.has(klass) ? MIL_PLANE_MODEL_SCALE * CLASS_SCALE_3D[klass]
: MIL_MODEL_SCALE * CLASS_SCALE_3D[klass]);
import { aircraftIcon } from '../src/data/aircraftIcons.js';
// ---------------------------------------------------------------------------
// Args (same shape as qa-heading-b3.mjs)
// ---------------------------------------------------------------------------
const argv = process.argv.slice(2);
const getFlag = (name) => argv.includes(name);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const APP_URL = getOpt('--url', 'http://localhost:4173');
const HEADFUL = getFlag('--headful');
const SHOT_DIR = path.resolve('qa-shots/b5');
const FL_MODEL_SCALE = 1; // flights.js (airplane.glb is transform-applied and meter-scale)
const MIL_PLANE_MODEL_SCALE = 1; // military heavy classes share the same baked airplane.glb
const MIL_MODEL_SCALE = 1; // militaryFlights.js (jet.glb is already real-world scale, native radius ~29.8 m)
const MIL_BILLBOARD_SCALE = 0.7; // militaryFlights BILLBOARD_SCALE
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try { if (fs.existsSync(candidate)) return candidate; } catch { /* skip */ }
}
return null;
}
const results = [];
function record(name, ok, detail) {
results.push({ name, ok, detail });
const tag = ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
// ---------------------------------------------------------------------------
// Synthetic straight-flying aircraft.
// Rows (2D spread): flights row at lat 30.32 (6 category-driven classes),
// military row at lat 30.28 (all 8 classes via type codes). Clusters (3D
// scale comparison): tight 130–150 m spacing so the follow-camera frames
// several models at once below the minimumPixelSize clamp distance.
// courseDeg 0 = flying north (glyph nose-up in a north-up top-down view);
// two planes fly rotated courses to confirm sprites still follow the path.
// ---------------------------------------------------------------------------
const ROW_LON0 = -97.82;
const ROW_STEP_DEG = 0.012; // ~1.15 km — full military row (8 planes) fits the unobstructed frame center
const CLUSTER_LON0 = -97.7431;
const mDegLon = (m, lat) => m / (111320 * Math.cos((lat * Math.PI) / 180));
const SPRITES = {
timeOffsetSec: 0,
// OpenSky extended categories: 2 light, 4 large airliner, 6 heavy/widebody,
// 7 high-perf fastjet, 8 rotorcraft, 9 glider. (quadjet/turboprop need type
// codes, which OpenSky lacks — the military row covers those.)
flights: [
{ icao: 'caa001', callsign: 'FL-LGT', lon0: ROW_LON0 + 0 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 0, speedMps: 80, altM: 2500, category: 2 },
{ icao: 'caa002', callsign: 'FL-AIR', lon0: ROW_LON0 + 1 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 0, speedMps: 80, altM: 2600, category: 4 },
{ icao: 'caa003', callsign: 'FL-WID', lon0: ROW_LON0 + 2 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 0, speedMps: 80, altM: 2700, category: 6 },
{ icao: 'caa004', callsign: 'FL-FJ', lon0: ROW_LON0 + 3 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 90, speedMps: 80, altM: 2800, category: 7 },
{ icao: 'caa005', callsign: 'FL-HEL', lon0: ROW_LON0 + 4 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 0, speedMps: 40, altM: 1200, category: 8 },
{ icao: 'caa006', callsign: 'FL-GLD', lon0: ROW_LON0 + 5 * ROW_STEP_DEG, lat0: 30.32, courseDeg: 0, speedMps: 40, altM: 2000, category: 9 },
// 3D cluster (150 m spacing): widebody vs airliner (tracked) vs fastjet
{ icao: 'cbb001', callsign: '3D-WID', lon0: CLUSTER_LON0 - mDegLon(150, 30.10), lat0: 30.10, courseDeg: 0, speedMps: 60, altM: 3000, category: 6 },
{ icao: 'cbb002', callsign: '3D-AIR', lon0: CLUSTER_LON0, lat0: 30.10, courseDeg: 0, speedMps: 60, altM: 3000, category: 4 },
{ icao: 'cbb003', callsign: '3D-FJ', lon0: CLUSTER_LON0 + mDegLon(150, 30.10), lat0: 30.10, courseDeg: 0, speedMps: 60, altM: 3000, category: 7 },
],
military: [
{ hex: 'dda001', flight: 'ML-LGT', t: 'C172', lon0: ROW_LON0 + 0 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 60, altFt: 9000 },
{ hex: 'dda002', flight: 'ML-GLD', t: 'DISC', lon0: ROW_LON0 + 1 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 40, altFt: 8000 },
{ hex: 'dda003', flight: 'ML-TPR', t: 'C130', lon0: ROW_LON0 + 2 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 80, altFt: 10000 },
{ hex: 'dda004', flight: 'ML-AIR', t: 'A320', lon0: ROW_LON0 + 3 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 80, altFt: 11000 },
{ hex: 'dda005', flight: 'ML-WID', t: 'C17', lon0: ROW_LON0 + 4 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 80, altFt: 12000 },
{ hex: 'dda006', flight: 'ML-QUAD', t: 'B744', lon0: ROW_LON0 + 5 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 80, altFt: 13000 },
{ hex: 'dda007', flight: 'ML-HEL', t: 'H60', lon0: ROW_LON0 + 6 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 40, altFt: 3000 },
// course 0 like the rest — keeps the row a clean size-comparison line (2D
// rotation is demoed by caa004 and gated by qa-heading-b3 anyway).
{ hex: 'dda008', flight: 'ML-FJ', t: 'F16', lon0: ROW_LON0 + 7 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 80, altFt: 15000 },
// 3D cluster: C17 vs F16 (tracked) vs B744 vs C130. jet.glb is a
// real-world-scale asset (native bounding radius ~29.8) rendered at
// MODEL_SCALE 1, so models are ~22–43 m radius — same 150 m spacing and
// 450–900 m shot ranges as the flights cluster.
{ hex: 'ddb001', flight: '3DMWID', t: 'C17', lon0: CLUSTER_LON0 - mDegLon(150, 30.05), lat0: 30.05, courseDeg: 0, speedMps: 60, altFt: 12000 },
{ hex: 'ddb002', flight: '3DMFJ', t: 'F16', lon0: CLUSTER_LON0, lat0: 30.05, courseDeg: 0, speedMps: 60, altFt: 12000 },
{ hex: 'ddb003', flight: '3DMQUA', t: 'B744', lon0: CLUSTER_LON0 + mDegLon(150, 30.05), lat0: 30.05, courseDeg: 0, speedMps: 60, altFt: 12000 },
{ hex: 'ddb004', flight: '3DMTPR', t: 'C130', lon0: CLUSTER_LON0 - mDegLon(300, 30.05), lat0: 30.05, courseDeg: 0, speedMps: 60, altFt: 12000 },
],
};
// Node-side expected classification (same inputs the layers see).
const expectedFlights = new Map(SPRITES.flights.map((p) => [p.icao, classifyAircraft({ category: p.category })]));
const expectedMilitary = new Map(SPRITES.military.map((p) => [p.hex, classifyAircraft({ typeCode: p.t })]));
// ---------------------------------------------------------------------------
async function main() {
console.log(`\nType-aware sprite QA (Batch 5 / skylight Tasks 8+9)`);
console.log(` App URL : ${APP_URL}\n`);
console.log(` Expected classes — flights: ${[...expectedFlights.entries()].map(([k, v]) => `${k}:${v}`).join(' ')}`);
console.log(` Expected classes — military: ${[...expectedMilitary.entries()].map(([k, v]) => `${k}:${v}`).join(' ')}\n`);
try {
const res = await fetch(APP_URL, { method: 'GET' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
console.error(`\x1b[31mDev server not reachable at ${APP_URL} (${e.message}).\x1b[0m`);
process.exit(2);
}
fs.mkdirSync(SHOT_DIR, { recursive: true });
const chromeExecutable = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
'--use-angle=swiftshader',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1280,800',
],
});
const consoleErrors = [];
const httpErrors = [];
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
page.on('console', (msg) => {
if (msg.type() === 'error') {
const text = msg.text();
if (!/Failed to load resource.*404/i.test(text)) consoleErrors.push(text);
}
});
page.on('pageerror', (err) => consoleErrors.push(`pageerror: ${err.message}`));
page.on('response', (response) => {
if (response.status() >= 500) httpErrors.push(`${response.status()} ${response.url()}`);
});
// ---- Straight-flight fetch shim (installed before any app code runs) ----
await page.evaluateOnNewDocument((spec) => {
window.__SPR = spec;
window.__SPR.epochMs = Date.now();
window.__SPR_HITS = { opensky: 0, mil: 0 };
// Straight line from (lon0, lat0) along courseDeg at speedMps.
window.__SPR.stateAt = (p, tSec) => {
const cRad = (p.courseDeg * Math.PI) / 180;
const dist = p.speedMps * tSec;
const east = dist * Math.sin(cRad);
const north = dist * Math.cos(cRad);
const lat = p.lat0 + north / 111320;
const lon = p.lon0 + east / (111320 * Math.cos((p.lat0 * Math.PI) / 180));
return { lon, lat, course: p.courseDeg, speedMps: p.speedMps };
};
const realFetch = window.fetch.bind(window);
const jsonResponse = (obj) => new Response(JSON.stringify(obj), {
status: 200, headers: { 'Content-Type': 'application/json' },
});
window.fetch = (input, init) => {
const url = typeof input === 'string' ? input : (input && input.url) || '';
const S = window.__SPR;
if (url.includes('/api/openai/hud-summary')) {
return Promise.resolve(jsonResponse({ summary: 'Aircraft scale QA' }));
}
const nowSec = Date.now() / 1000 + (S.timeOffsetSec || 0);
const tRel = nowSec - S.epochMs / 1000;
if (url.includes('/api/opensky-track')) return Promise.resolve(jsonResponse({ path: [] }));
if (url.includes('/api/adsblol/trace')) {
return Promise.resolve(jsonResponse({ timestamp: Math.floor(nowSec), trace: [] }));
}
if (url.includes('/api/opensky')) {
window.__SPR_HITS.opensky++;
const states = S.flights.map((f) => {
const s = S.stateAt(f, tRel);
return [
f.icao, f.callsign, 'Synthetica',
Math.floor(nowSec), Math.floor(nowSec),
s.lon, s.lat, f.altM,
false, // on_ground
s.speedMps, s.course,
0, null, null, null, false, 0,
f.category, // state[17] — extended emitter category
];
});
return Promise.resolve(jsonResponse({ time: Math.floor(nowSec), states }));
}
if (url.includes('/api/adsblol/mil')) {
window.__SPR_HITS.mil++;
const ac = S.military.map((m) => {
const s = S.stateAt(m, tRel);
return {
hex: m.hex, flight: m.flight,
lon: s.lon, lat: s.lat, alt_baro: m.altFt,
track: s.course, gs: s.speedMps * 1.9438,
t: m.t, r: `SY-${m.hex.slice(-3)}`, ownOp: 'SYNTH AF',
seen_pos: Math.max(0, -(S.timeOffsetSec || 0)),
};
});
return Promise.resolve(jsonResponse({ msg: 'No error', now: Date.now(), ac }));
}
// Synthetic categories/types already exercise every classification
// branch. ADSBDB enrichment is incidental to this visual-scale check,
// so keep an unavailable public enrichment provider from generating an
// unrelated console error.
if (url.includes('/api/adsbdb/')) return Promise.resolve(jsonResponse({ found: false }));
return realFetch(input, init);
};
}, SPRITES);
console.log('Loading app...');
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() => window.__godsEyeView && window.__godsEyeView.viewer && window.__godsEyeView.dataManager,
{ timeout: 60000, polling: 200 }
);
console.log(' App globals ready.');
// ---- In-page probes: billboard walk, model walk, tracked-model finder ---
await page.evaluate(() => {
// Every billboard (image + alignedAxis + id) in every collection.
window.__collectBillboards = function () {
const v = window.__godsEyeView.viewer;
const out = [];
const walk = (coll) => {
const n = coll.length;
for (let i = 0; i < n; i++) {
let p;
try { p = coll.get(i); } catch { continue; }
if (!p) continue;
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
if (p.image !== undefined && p.alignedAxis !== undefined) {
out.push({ id: p.id, image: p.image, scale: p.scale, show: p.show });
}
}
};
walk(v.scene.primitives);
return out;
};
// Every glTF model primitive (modelMatrix + ready flag), with id + scale.
window.__collectModels = function () {
const v = window.__godsEyeView.viewer;
const out = [];
const walk = (coll) => {
const n = coll.length;
for (let i = 0; i < n; i++) {
let p;
try { p = coll.get(i); } catch { continue; }
if (!p) continue;
if (typeof p.length === 'number' && typeof p.get === 'function') { walk(p); continue; }
if (p.modelMatrix && typeof p.ready !== 'undefined' && p.image === undefined) {
let radius = null;
try { radius = p.ready && p.boundingSphere ? p.boundingSphere.radius : null; } catch { /* not ready */ }
out.push({ id: p.id, scale: p.scale, ready: p.ready, show: p.show, radius });
}
}
};
walk(v.scene.primitives);
return out;
};
// Displayed (render-delayed) latitude of a synthetic plane right now.
window.__displayedState = function (layer, idx, renderDelaySec) {
const S = window.__SPR;
const p = S[layer][idx];
const tRel = (Date.now() - S.epochMs) / 1000 - renderDelaySec;
return S.stateAt(p, tRel);
};
// Canvas coordinates of every synthetic plane at its displayed position —
// ground truth for identifying which glyph is which in a screenshot.
window.__planeCanvasCoords = function (layer, renderDelaySec, altOf) {
const v = window.__godsEyeView.viewer;
const S = window.__SPR;
const C3 = v.camera.position.constructor;
return S[layer].map((p, i) => {
const s = window.__displayedState(layer, i, renderDelaySec);
const pos = C3.fromDegrees(s.lon, s.lat, altOf === 'ft' ? p.altFt * 0.3048 : p.altM);
const c = v.scene.cartesianToCanvasCoordinates(pos);
return `${p.icao || p.hex}=(${c ? Math.round(c.x) : '-'},${c ? Math.round(c.y) : '-'})`;
}).join(' ');
};
});
// ---- Prime history through the render delay (same pattern as b3) -------
console.log('Priming straight-flight history through the render delay (30 s / 15 s)...');
const primed = await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
const v = window.__godsEyeView.viewer;
window.__SPR.timeOffsetSec = -32;
await dm.setEnabled('flights', true);
await dm.setEnabled('military', true);
const fl = dm.layers.get('flights').module;
const mil = dm.layers.get('military').module;
for (const off of [-24, -16, -8, 0]) {
window.__SPR.timeOffsetSec = off;
await fl.update(v);
await mil.update(v);
}
window.__SPR.timeOffsetSec = 0;
window.__SPR_DRIVER = setInterval(() => { fl.update(v); mil.update(v); }, 15000);
return { fl: fl.getStats().count, mil: mil.getStats().count, hits: window.__SPR_HITS };
});
console.log(` flights count=${primed.fl} military count=${primed.mil} | shim hits opensky=${primed.hits.opensky} mil=${primed.hits.mil}`);
record('B1 ingest: synthetic planes in both layers',
primed.fl === SPRITES.flights.length && primed.mil === SPRITES.military.length,
`flights=${primed.fl}/${SPRITES.flights.length} military=${primed.mil}/${SPRITES.military.length}`);
if (!(primed.fl > 0 && primed.mil > 0)) { finish(); return; }
// ========================================================================
// B2 — billboard glyph + scale per class (machine check)
// ========================================================================
console.log('\nB2 — per-class billboard glyph + scale');
const bbs = await page.evaluate(() => window.__collectBillboards());
const bbById = new Map(bbs.map((b) => [b.id, b]));
let glyphBad = [];
let scaleBad = [];
for (const [id, klass] of expectedFlights) {
const bb = bbById.get(id);
const wantImg = aircraftIcon(klass);
const wantScale = CLASS_SCALE_2D[klass] || 1;
if (!bb || bb.image !== wantImg) glyphBad.push(`${id}(${klass})${bb ? ':wrong-image' : ':missing'}`);
if (bb && Math.abs(bb.scale - wantScale) > 1e-9) scaleBad.push(`${id}: ${bb.scale} != ${wantScale}`);
}
for (const [id, klass] of expectedMilitary) {
const bb = bbById.get(id);
const wantImg = aircraftIcon(klass);
const wantScale = MIL_BILLBOARD_SCALE * (CLASS_SCALE_2D[klass] || 1);
if (!bb || bb.image !== wantImg) glyphBad.push(`${id}(${klass})${bb ? ':wrong-image' : ':missing'}`);
if (bb && Math.abs(bb.scale - wantScale) > 1e-9) scaleBad.push(`${id}: ${bb.scale} != ${wantScale}`);
}
record('B2a: every synthetic billboard has its class glyph (data-URI match)',
glyphBad.length === 0, glyphBad.length ? glyphBad.join(' ') : `${expectedFlights.size + expectedMilitary.size} billboards matched`);
record('B2b: every synthetic billboard has its class scale',
scaleBad.length === 0, scaleBad.length ? scaleBad.join(' ') : 'all scales exact');
const distinctGlyphs = new Set([...expectedFlights.values(), ...expectedMilitary.values()].map((k) => aircraftIcon(k)));
record('B2c: all 8 classes exercised with 8 distinct glyphs',
distinctGlyphs.size === 8, `${distinctGlyphs.size} distinct data URIs`);
// ========================================================================
// 2D screenshots
// ========================================================================
console.log('\n2D screenshots → qa-shots/b5/');
const topDown = async (lat, lon, height) => {
await page.evaluate(({ lat, lon, height }) => {
const v = window.__godsEyeView.viewer;
v.camera.cancelFlight(); // the boot fly-to-Austin otherwise stomps setView
const C3 = v.camera.position.constructor;
v.camera.setView({
destination: C3.fromDegrees(lon, lat, height),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
}, { lat, lon, height });
await sleep(1800); // let fleet tick land positions + rotations at the new pose
};
// Zoom the follow-camera to an absolute range from the tracked entity.
// In tracked mode camera.position lives in the ENTITY's reference frame, so
// its magnitude IS the range — fixed zoomIn() amounts compound across phases
// (the second tracked plane inherits the previous phase's close-in offset).
const zoomToRange = async (want) => {
await page.evaluate((want) => {
const v = window.__godsEyeView.viewer;
const p = v.camera.position;
const d = Math.hypot(p.x, p.y, p.z);
if (d > 1e6) return; // not in a tracked reference frame — leave it alone
v.camera.zoomIn(d - want);
}, want);
await sleep(1500);
};
// Rows drift north while flying; recenter on the DISPLAYED (render-delayed) row.
const rowLat = async (layer, idx, delay) =>
(await page.evaluate(({ layer, idx, delay }) => window.__displayedState(layer, idx, delay), { layer, idx, delay })).lat;
const flRowLat = await rowLat('flights', 2, 30);
const milRowLat = await rowLat('military', 4, 15);
await topDown((flRowLat + milRowLat) / 2, ROW_LON0 + 3 * ROW_STEP_DEG, 16000);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-2d-spread-bothrows.png') });
console.log(' saved b5-2d-spread-bothrows.png (flights row top, military row bottom)');
// Row widths: military spans 7 steps (~8.1 km) → frame ~13.9 km at 12 km up;
// flights spans 5 steps (~5.8 km, +caa004's eastward drift) → ~10.4 km at 9 km up.
await topDown(await rowLat('military', 4, 15), ROW_LON0 + 3.5 * ROW_STEP_DEG, 17000);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-2d-military-row.png') });
console.log(' saved b5-2d-military-row.png (8 classes, amber)');
console.log(` glyph positions: ${await page.evaluate(() => window.__planeCanvasCoords('military', 15, 'ft'))}`);
await topDown(await rowLat('flights', 2, 30), ROW_LON0 + 2.5 * ROW_STEP_DEG, 12000);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-2d-flights-row.png') });
console.log(' saved b5-2d-flights-row.png (6 category classes, white)');
console.log(` glyph positions: ${await page.evaluate(() => window.__planeCanvasCoords('flights', 30, 'm'))}`);
// Tracked tint: commercial tracked plane goes cyan; neighbors stay white.
await page.evaluate((icao) => {
window.__godsEyeView.dataManager.layers.get('flights').module.trackById(icao);
}, 'caa002');
await sleep(3000);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-2d-tracked-cyan.png') });
console.log(' saved b5-2d-tracked-cyan.png (tracked airliner cyan, follow camera)');
// ========================================================================
// B3 + 3D screenshots — flights cluster
// ========================================================================
console.log('\n3D — flights cluster (airplane.glb, per-class scale)');
await page.evaluate(() => {
const dm = window.__godsEyeView.dataManager;
dm.layers.get('flights').module.setParams({ models3d: true });
dm.layers.get('flights').module.trackById('cbb002');
});
const flModelsUp = await page.waitForFunction(() => {
const models = window.__collectModels().filter((m) => m.ready && m.show);
const ids = new Set(models.map((m) => m.id));
return ids.has('cbb001') && ids.has('cbb002') && ids.has('cbb003');
}, { timeout: 40000, polling: 400 }).then(() => true).catch(() => false);
record('3D flights: tracked model + 2 fleet cluster models rendered', flModelsUp, flModelsUp ? 'all up' : 'timed out');
if (flModelsUp) {
const flModels = await page.evaluate(() => window.__collectModels().filter((m) => m.ready));
const modelScaleBad = [];
for (const [id, want] of [
['cbb001', flightsWantScale(expectedFlights.get('cbb001'))],
['cbb003', flightsWantScale(expectedFlights.get('cbb003'))],
]) {
const m = flModels.find((x) => x.id === id);
if (!m || Math.abs(m.scale - want) > 1e-9) modelScaleBad.push(`${id}: ${m ? m.scale : 'missing'} != ${want}`);
}
const trackedM = flModels.find((x) => x.id === 'cbb002');
const trackedWant = flightsWantScale(expectedFlights.get('cbb002'));
if (!trackedM || Math.abs(trackedM.scale - trackedWant) > 1e-9) {
modelScaleBad.push(`tracked: ${trackedM ? trackedM.scale : 'missing'} != ${trackedWant}`);
}
record('B3a flights: model scales match the per-class registry (all GLBs meter-scale; shared @1×class)',
modelScaleBad.length === 0,
modelScaleBad.length ? modelScaleBad.join(' ') : 'all exact per registry');
console.log(` world bounding radii (m): ${flModels.filter((m) => m.scale !== undefined).map((m) => `${m.id ?? 'tracked'}=${m.radius?.toFixed(1)}`).join(' ')}`);
await sleep(1500);
await zoomToRange(900);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-3d-flights-mid.png') });
console.log(' saved b5-3d-flights-mid.png (900 m range — near the min-pixel clamp)');
await zoomToRange(450);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-3d-flights-close.png') });
console.log(' saved b5-3d-flights-close.png (450 m range: widebody left, airliner center, fastjet right)');
}
// ========================================================================
// B3 + 3D screenshots — military cluster
// ========================================================================
console.log('\n3D — military cluster (jet.glb, per-class scale)');
await page.evaluate(() => {
const dm = window.__godsEyeView.dataManager;
dm.layers.get('military').module.setParams({ models3d: true });
dm.layers.get('military').module.trackById('ddb002');
});
const milModelsUp = await page.waitForFunction(() => {
const models = window.__collectModels().filter((m) => m.ready && m.show);
const ids = new Set(models.map((m) => m.id));
return ids.has('ddb001') && ids.has('ddb002') && ids.has('ddb003') && ids.has('ddb004');
}, { timeout: 40000, polling: 400 }).then(() => true).catch(() => false);
record('3D military: tracked model + 3 fleet cluster models rendered', milModelsUp, milModelsUp ? 'all up' : 'timed out');
if (milModelsUp) {
const milModels = await page.evaluate(() => window.__collectModels().filter((m) => m.ready));
const modelScaleBad = [];
for (const [id, want] of [
['ddb001', militaryWantScale(expectedMilitary.get('ddb001'))],
['ddb003', militaryWantScale(expectedMilitary.get('ddb003'))],
['ddb004', militaryWantScale(expectedMilitary.get('ddb004'))],
]) {
const m = milModels.find((x) => x.id === id);
if (!m || Math.abs(m.scale - want) > 1e-9) modelScaleBad.push(`${id}: ${m ? m.scale : 'missing'} != ${want}`);
}
const trackedM = milModels.find((x) => x.id === 'ddb002');
const trackedWant = militaryWantScale(expectedMilitary.get('ddb002'));
if (!trackedM || Math.abs(trackedM.scale - trackedWant) > 1e-9) {
modelScaleBad.push(`tracked: ${trackedM ? trackedM.scale : 'missing'} != ${trackedWant}`);
}
record('B3b military: model scales match the weight-class registry (real/heavy/jet assets @1×class)',
modelScaleBad.length === 0,
modelScaleBad.length ? modelScaleBad.join(' ') : 'all exact per registry');
console.log(` world bounding radii (m): ${milModels.filter((m) => m.scale !== undefined).map((m) => `${m.id ?? 'tracked'}=${m.radius?.toFixed(1)}`).join(' ')}`);
await sleep(1500);
// jet.glb at MODEL_SCALE 1 is real-world size — same shot ranges as the
// flights cluster (close zoom no longer puts the camera inside a model).
await zoomToRange(900);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-3d-military-far.png') });
console.log(' saved b5-3d-military-far.png (900 m range: C130/C17 left, F16 center, B744 right)');
await zoomToRange(450);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-3d-military-close.png') });
console.log(' saved b5-3d-military-close.png (450 m range: C17 left, F16 center, B744 right)');
}
record('B4: no console errors during QA run', consoleErrors.length === 0,
consoleErrors.length
? `${consoleErrors.length}: ${consoleErrors.slice(0, 3).join(' | ')}; HTTP: ${httpErrors.slice(0, 8).join(' | ') || 'none observed'}`
: 'clean');
finish();
} finally {
await browser.close();
}
function finish() {
const failed = results.filter((r) => r.ok === false);
const passed = results.filter((r) => r.ok === true);
console.log(`\n${'─'.repeat(60)}`);
console.log(` RESULT: ${passed.length} passed, ${failed.length} failed`);
console.log(`${'─'.repeat(60)}\n`);
process.exitCode = failed.length > 0 ? 1 : 0;
}
}
main().catch((err) => {
console.error('\n\x1b[31mQA harness error:\x1b[0m', err && err.stack ? err.stack : err);
process.exit(2);
});
+477
View File
@@ -0,0 +1,477 @@
#!/usr/bin/env node
/**
* qa-traffic-baseline.mjs — Traffic Phase 0 causal-chain capture.
*
* Drives four client/cache conditions and prints the User Timing segments
* emitted by `src/data/traffic.js` when `?trafficDebug=1` is present:
*
* 1. exact-viewport revisit (traffic module's full-road cache),
* 2. adjacent 0.4–2 km pan (new exact bbox with heavy spatial overlap),
* 3. cold city (no traffic-module entry for that city),
* 4. degraded upstream (synthetic delay on `/api/overpass` only).
*
* The proxy's actual `X-Overpass-Cache` result is printed for every network
* pass, so HIT/DISK/MISS/STALE/INFLIGHT is evidence rather than inference.
* The degraded leg delays request release in Puppeteer; it exercises the same
* fetch-start→response boundary without adding a failure switch to the
* application or proxy. Pass `--degraded-delay-ms 0` to skip that leg.
* TomTom status is intercepted as keyless so no live-flow key is required and
* the capture isolates the OSM road-to-visible-dot chain.
*
* Measurement caveat: durations include instrumentation overhead (observer
* effect), and timestamps are subject to browser clock-floor quantization.
* Per-road clock observations also sit inside `road-parse-total` and
* `waypoint-materialization`, so those segments slightly overstate production.
*
* Run with deterministic software GL (relative comparisons only):
* node scripts/qa-traffic-baseline.mjs --url http://localhost:4173
*
* Run headful on the owner's real-GPU browser surface for reportable numbers:
* node scripts/qa-traffic-baseline.mjs --url http://localhost:4173 --headful
*
* SwiftShader `scene.sampleHeight` durations are NOT representative of a real
* GPU. The script prints the detected renderer and repeats this warning when
* software rendering is detected. It does not write baseline numbers or edit
* the latency plan.
*/
import puppeteer from 'puppeteer';
import fs from 'node:fs';
const argv = process.argv.slice(2);
const getOpt = (name, dflt) => {
const index = argv.indexOf(name);
return index >= 0 && argv[index + 1] ? argv[index + 1] : dflt;
};
const getNumberOpt = (name, dflt) => {
const value = Number(getOpt(name, dflt));
return Number.isFinite(value) ? value : dflt;
};
const APP_URL = getOpt('--url', 'http://localhost:4173');
const HEADFUL = argv.includes('--headful');
const TIMEOUT_MS = Math.max(10_000, getNumberOpt('--timeout-ms', 120_000));
const DEGRADED_DELAY_MS = Math.max(0, getNumberOpt('--degraded-delay-ms', 2500));
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try { if (fs.existsSync(candidate)) return candidate; } catch { /* ignore */ }
}
return null;
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const VIEWS = {
austin: {
lon: -97.7431, lat: 30.2672, height: 1500, heading: 8, pitch: -78,
},
adjacent: {
// 0.0105° longitude at Austin's latitude is ~1.0 km.
lon: -97.7326, lat: 30.2672, height: 1500, heading: 8, pitch: -78,
},
coldCity: {
lon: -122.4194, lat: 37.7749, height: 1500, heading: 18, pitch: -78,
},
degraded: {
lon: -98.4936, lat: 29.4241, height: 1500, heading: 12, pitch: -78,
},
};
/** Add `trafficDebug=1` while preserving the caller's path/query/hash. */
function debugUrl(rawUrl) {
const url = new URL(rawUrl);
url.searchParams.set('trafficDebug', '1');
return url.toString();
}
/** Position the camera and explicitly raise the boundaries the layer observes. */
async function moveCamera(page, view) {
return page.evaluate((v) => {
const gev = window.__godsEyeView;
const module = gev.dataManager.layers.get('traffic').module;
const beforeLastUpdate = module.getStats().lastUpdate;
const boundaryTime = performance.now();
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
const radians = Math.PI / 180;
try { gev.viewer.camera.cancelFlight(); } catch { /* no active flight */ }
gev.viewer.camera.setView({
destination: ellipsoid.cartographicToCartesian({
longitude: v.lon * radians,
latitude: v.lat * radians,
height: v.height,
}),
orientation: {
heading: (v.heading || 0) * radians,
pitch: (v.pitch ?? -90) * radians,
roll: 0,
},
});
// setView normally propagates through Cesium's render loop. Raising these
// explicitly makes the capture boundary deterministic across GPU speeds.
// Pairing still flows through camera.changed's schedule-time anchor; the
// synthetic moveEnd is emitted only as the same diagnostic used naturally.
gev.viewer.camera.changed.raiseEvent();
gev.viewer.camera.moveEnd.raiseEvent();
return { beforeLastUpdate, boundaryTime };
}, view);
}
/** Wait until the triggered load finishes and its final render reaches postRender. */
async function waitForTrafficCapture(page, beforeLastUpdate, boundaryTime) {
await page.waitForFunction(
(before, boundary) => {
const module = window.__godsEyeView?.dataManager?.layers?.get('traffic')?.module;
if (!module) return false;
const stats = module.getStats();
if (stats.lastUpdate === before || stats.count <= 0 || stats.loading) return false;
const entries = performance.getEntriesByType('measure')
.filter((entry) => entry.startTime >= boundary);
const renderCount = entries.filter((entry) => entry.name.startsWith('traffic:dot-construction:')).length;
const postRenderCount = entries.filter((entry) => entry.name.startsWith('traffic:render-to-post-render:')).length;
return renderCount > 0 && postRenderCount >= renderCount;
},
{ timeout: TIMEOUT_MS, polling: 100 },
beforeLastUpdate,
boundaryTime,
);
// Let a final full-pass postRender land after `_fetching` flips false.
await sleep(150);
}
/** Return serializable Traffic User Timing measures created after a boundary. */
function readTrafficMeasures(page, boundaryTime) {
return page.evaluate((boundary) => performance.getEntriesByType('measure')
.filter((entry) => entry.name.startsWith('traffic:') && entry.startTime >= boundary)
.map((entry) => ({
name: entry.name,
startTime: entry.startTime,
duration: entry.duration,
detail: entry.detail,
})), boundaryTime);
}
/** Drive one measured state and return its raw User Timing measures. */
async function captureState(page, name, view, setDelay) {
setDelay(name === 'degraded-upstream' ? DEGRADED_DELAY_MS : 0);
const { beforeLastUpdate, boundaryTime } = await moveCamera(page, view);
await waitForTrafficCapture(page, beforeLastUpdate, boundaryTime);
const measures = await readTrafficMeasures(page, boundaryTime);
setDelay(0);
return { name, measures };
}
/** Format a duration without hiding zero-duration measurements. */
function ms(value) {
return Number.isFinite(value) ? value.toFixed(1) : '—';
}
/** Find one segment for the same trace/pass as a render measure. */
function matchingMeasure(measures, render, segment) {
return measures.find((entry) => (
entry.detail?.segment === segment
&& entry.detail?.traceId === render.detail?.traceId
&& entry.detail?.interactionId === render.detail?.interactionId
&& entry.detail?.generation === render.detail?.generation
&& entry.detail?.pass === render.detail?.pass
));
}
/** Fail loudly instead of filtering away a structurally mispaired trace. */
function assertInteractionIntegrity(capture) {
const interactionByTrace = new Map();
for (const entry of capture.measures) {
const traceId = entry.detail?.traceId;
if (traceId == null) continue;
const interactionId = entry.detail?.interactionId;
if (interactionId == null) {
throw new Error(`${capture.name}: trace ${traceId} has no scheduling interactionId`);
}
const prior = interactionByTrace.get(traceId);
if (prior != null && prior !== interactionId) {
throw new Error(
`${capture.name}: trace ${traceId} crossed interactions ${prior} and ${interactionId}`,
);
}
interactionByTrace.set(traceId, interactionId);
}
}
/** Convert raw measures into one table row per major/full visible render. */
function rowsForCapture(capture) {
// Correlation violations are capture failures, never rows silently removed
// by generation/settle predicates that are tautological after the guard.
assertInteractionIntegrity(capture);
const renders = capture.measures.filter((entry) => (
entry.detail?.segment === 'dot-construction'
&& entry.detail?.interactionId != null
));
return renders.map((render) => {
const get = (segment) => matchingMeasure(capture.measures, render, segment);
const fetchStart = get('last-camera-change-to-fetch-start');
const response = get('fetch-to-response');
const responseJson = get('response-json');
const sample = get('sample-height-total');
const materialize = get('waypoint-materialization');
const flowRace = get('flow-render-race');
const postRender = capture.measures.find((entry) => (
entry.detail?.segment === 'render-to-post-render'
&& entry.detail?.traceId === render.detail?.traceId
&& entry.detail?.interactionId === render.detail?.interactionId
&& entry.detail?.generation === render.detail?.generation
&& entry.detail?.renderId === render.detail?.renderId
));
const visible = capture.measures.find((entry) => (
entry.detail?.segment === 'last-camera-change-to-first-visible'
&& entry.detail?.traceId === render.detail?.traceId
&& entry.detail?.interactionId === render.detail?.interactionId
&& entry.detail?.generation === render.detail?.generation
&& entry.detail?.renderId === render.detail?.renderId
));
const heatLines = capture.measures.find((entry) => (
entry.detail?.segment === 'rebuild-heat-lines'
&& entry.detail?.traceId === render.detail?.traceId
&& entry.detail?.interactionId === render.detail?.interactionId
&& entry.detail?.generation === render.detail?.generation
&& entry.detail?.renderId === render.detail?.renderId
));
const metadata = response?.detail || sample?.detail || render.detail || {};
return {
State: capture.name,
Pass: render.detail?.pass || '—',
Source: render.detail?.source || '—',
'Proxy cache': metadata.proxyCache || '—',
Upstream: metadata.proxyUpstream || '—',
'Last change→fetch ms': ms(fetchStart?.duration),
'Fetch→response ms': ms(response?.duration),
'response.json ms': ms(responseJson?.duration),
'Flow race ms': ms(flowRace?.duration),
'sampleHeight ms': ms(sample?.duration),
Calls: sample?.detail?.sampleHeightCalls ?? '—',
'Mean/call ms': ms(sample?.detail?.sampleHeightMeanMs),
Cells: sample?.detail?.distinctCells ?? '—',
'Waypoints ms': ms(materialize?.duration),
'Dots ms': ms(render.duration),
'Heat lines ms': ms(heatLines?.duration),
'Render→postRender ms': ms(postRender?.duration),
'Last change→visible ms': ms(visible?.duration),
Roads: render.detail?.roadCount ?? sample?.detail?.roadCount ?? '—',
Dots: render.detail?.dotCount ?? '—',
};
});
}
/** Read the active WebGL renderer for the real-GPU/SwiftShader warning. */
function readRenderer(page) {
return page.evaluate(() => {
const gl = window.__godsEyeView?.viewer?.scene?.context?._gl;
if (!gl) return 'unknown';
const extension = gl.getExtension('WEBGL_debug_renderer_info');
return extension
? gl.getParameter(extension.UNMASKED_RENDERER_WEBGL)
: gl.getParameter(gl.RENDERER);
});
}
async function main() {
const url = debugUrl(APP_URL);
console.log('\nTraffic Phase 0 — causal-chain baseline capture');
console.log(` App URL : ${url}`);
console.log(` Browser mode : ${HEADFUL ? 'headful (real GPU expected)' : 'headless SwiftShader (relative-only)'}`);
console.log(` Degraded delay : ${DEGRADED_DELAY_MS ? `${DEGRADED_DELAY_MS} ms/request` : 'skipped'}`);
try {
const response = await fetch(APP_URL);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
} catch (error) {
console.error(`\nDev server not reachable at ${APP_URL} (${error.message}).`);
process.exit(2);
}
const executablePath = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(executablePath ? { executablePath } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1440,900',
...(HEADFUL ? [] : ['--use-gl=angle', '--use-angle=swiftshader']),
],
});
let activeProxyDelayMs = 0;
const captures = [];
const consoleErrors = [];
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.evaluateOnNewDocument(() => localStorage.clear());
page.on('console', (message) => {
if (message.type() === 'error') consoleErrors.push(message.text());
});
page.on('pageerror', (error) => consoleErrors.push(error.message));
// Force keyless traffic and optionally hold Overpass requests to model a
// degraded proxy/upstream. No server key or proxy debug endpoint is needed.
await page.setRequestInterception(true);
page.on('request', async (request) => {
if (request.url().includes('/api/tomtom/status')) {
await request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ hasKey: false }),
});
return;
}
if (activeProxyDelayMs > 0 && request.url().includes('/api/overpass')) {
await sleep(activeProxyDelayMs);
}
try { await request.continue(); } catch { /* page closed or already handled */ }
});
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
{ timeout: 60_000 },
);
// Disable all persisted overlays, park above traffic's activation ceiling,
// then enable traffic without accidentally starting an unmeasured load.
await page.evaluate(async (highView) => {
const gev = window.__godsEyeView;
await gev.dataManager.restoreEnabledLayerIds([]);
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
const radians = Math.PI / 180;
gev.viewer.camera.setView({
destination: ellipsoid.cartographicToCartesian({
longitude: highView.lon * radians,
latitude: highView.lat * radians,
height: 9000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
await gev.dataManager.setEnabled('traffic', true);
gev.viewer.camera.changed.raiseEvent();
gev.viewer.camera.moveEnd.raiseEvent();
}, VIEWS.austin);
await sleep(500);
const renderer = await readRenderer(page);
const softwareRenderer = /swiftshader|software/i.test(renderer);
console.log(` WebGL renderer : ${renderer}`);
if (!HEADFUL || softwareRenderer) {
console.log(' WARNING : sampleHeight timings are relative-only under SwiftShader/software GL.');
}
// Prime Austin once. Its measurements are deliberately not reported.
console.log('\nPriming the exact Austin viewport (not part of the table)...');
const prime = await captureState(page, 'prime', VIEWS.austin, (msValue) => {
activeProxyDelayMs = msValue;
});
if (!prime.measures.some((entry) => entry.detail?.segment === 'dot-construction')) {
throw new Error('trafficDebug instrumentation emitted no dot-construction measure');
}
// Clear the last-bounds gate without clearing the module's road cache, then
// revisit the byte-identical camera pose for a true client-cache hit.
await page.evaluate((view) => {
const gev = window.__godsEyeView;
const ellipsoid = gev.viewer.scene.globe.ellipsoid;
const radians = Math.PI / 180;
gev.viewer.camera.setView({
destination: ellipsoid.cartographicToCartesian({
longitude: view.lon * radians,
latitude: view.lat * radians,
height: 9000,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
});
gev.viewer.camera.changed.raiseEvent();
gev.viewer.camera.moveEnd.raiseEvent();
}, VIEWS.austin);
await sleep(500);
captures.push(await captureState(page, 'exact-viewport revisit', VIEWS.austin, (msValue) => {
activeProxyDelayMs = msValue;
}));
captures.push(await captureState(page, 'adjacent ~1.0 km pan', VIEWS.adjacent, (msValue) => {
activeProxyDelayMs = msValue;
}));
captures.push(await captureState(page, 'cold city (client)', VIEWS.coldCity, (msValue) => {
activeProxyDelayMs = msValue;
}));
if (DEGRADED_DELAY_MS > 0) {
captures.push(await captureState(page, 'degraded-upstream', VIEWS.degraded, (msValue) => {
activeProxyDelayMs = msValue;
}));
}
const missingStates = captures.filter((capture) => rowsForCapture(capture).length === 0);
if (missingStates.length) {
throw new Error(`no visible render captured for: ${missingStates.map((capture) => capture.name).join(', ')}`);
}
const exactRows = rowsForCapture(captures[0]);
if (!exactRows.some((row) => row.Source === 'client-cache' && row['Fetch→response ms'] === '—')) {
throw new Error('exact-viewport revisit did not use the traffic module cache');
}
for (const capture of captures.slice(1)) {
if (!capture.measures.some((entry) => entry.detail?.segment === 'fetch-to-response')) {
throw new Error(`${capture.name} did not issue an Overpass request`);
}
}
if (DEGRADED_DELAY_MS > 0) {
const degraded = captures.find((capture) => capture.name === 'degraded-upstream');
const delayed = degraded?.measures.some((entry) => (
entry.detail?.segment === 'fetch-to-response'
&& entry.duration >= DEGRADED_DELAY_MS * 0.9
));
if (!delayed) throw new Error('degraded-upstream leg did not observe the synthetic delay');
}
const rows = captures.flatMap(rowsForCapture);
if (!rows.length) throw new Error('no measured traffic renders were captured');
console.log('\nCausal segments (one row per visible major/full pass)');
console.table(rows);
console.log('\nNotes:');
console.log(' - Render→postRender is the first visible-frame boundary used by Phase 0.');
console.log(' - Proxy cache is read from X-Overpass-Cache; “—” means the client cache avoided fetch.');
console.log(' - response.json includes both body transfer and JSON decoding; the production path does not split them.');
console.log(' - Last change→fetch is approximately the 320 ms FETCH_DEBOUNCE for the first fetch by construction.');
console.log(' - Full-pass Last change→fetch also includes the intentionally sequential major pass.');
console.log(' - Cesium moveEnd is a diagnostic raised ~500 ms after stillness, typically after fetch starts; fetch never waits for it.');
console.log(' - Synthetic events bypass camera percentageChanged/inertia but use the same camera.changed schedule-time anchor as natural captures.');
console.log(' - The keyless capture zeroes Flow race and Heat lines work; live-keyed numbers will differ.');
console.log(' - Measurement caveat: timings include instrumentation overhead (observer effect) and browser clock-floor quantization.');
console.log(' - Per-road clock observations inside road-parse-total/waypoint-materialization slightly overstate production work.');
const timingDiagnostics = await page.evaluate(() => (
window.__godsEyeView.dataManager.layers.get('traffic').module.getStats().trafficTiming
));
console.log(` - Correlation drops: ${timingDiagnostics?.uncorrelatedTracesDropped ?? 'unavailable'} scheduling-anchor mismatch(es).`);
if (!HEADFUL || softwareRenderer) {
console.log(' - Do not copy these GPU-side numbers into the plan; rerun headful on a real-GPU surface.');
}
if (consoleErrors.length) {
console.log(` - Browser console errors observed: ${consoleErrors.length}`);
for (const error of consoleErrors.slice(0, 5)) console.log(` ${error}`);
}
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error('\nTraffic baseline capture failed:', error);
process.exit(3);
});
+254
View File
@@ -0,0 +1,254 @@
#!/usr/bin/env node
/**
* qa-traffic-jamviz-ab.mjs — A/B screenshot capture for the jam-viz
* congestion prototypes.
*
* For each view, renders the SAME camera framing under each jamViz mode
* (none = shipped main behavior / density / heatline / both), forcing a
* fresh camera-driven re-render between modes via an away-and-back teleport
* (setParams applies on the next load, same semantics as uncoveredRoads).
* Shots + per-shot layer stats land in --out (default qa-shots/jamviz).
*
* Views: Austin I-35 downtown corridor (owner's requested A/B target) at
* city scale + low detail, plus Mumbai Western Express Hwy — captured
* because Austin is off-peak at 22:00 CT while Mumbai is in live morning
* rush, guaranteeing jam-bucket coverage in the drama shots.
*
* Run: node scripts/qa-traffic-jamviz-ab.mjs --url http://localhost:4412
* Exits non-zero on harness failure (missing live mode, zero dots, etc.).
*/
import puppeteer from 'puppeteer';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
const argv = process.argv.slice(2);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const APP_URL = getOpt('--url', 'http://localhost:4412');
const OUT_DIR = path.resolve(REPO_ROOT, getOpt('--out', 'qa-shots/jamviz'));
const VIEWS = [
{
id: 'i35-city',
label: 'Austin I-35 corridor — city scale',
lon: -97.7365, lat: 30.235, height: 3000, heading: 8, pitch: -38,
},
{
id: 'i35-detail',
label: 'Austin I-35 at downtown — queue detail',
lon: -97.735, lat: 30.262, height: 1100, heading: 10, pitch: -45,
},
{
id: 'mumbai-wexp',
label: 'Mumbai Western Express Hwy — live morning rush',
lon: 72.851, lat: 19.115, height: 3200, heading: 0, pitch: -40,
},
];
const MODES = ['none', 'density', 'heatline', 'both'];
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
const findChromeExecutable = () =>
CHROME_EXECUTABLE_CANDIDATES.find((c) => { try { return fs.existsSync(c); } catch { return false; } }) || null;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/** Same pattern as qa-cctv-v2: wait for the 3D tileset to settle for crisp shots. */
function waitForTilesLoaded(page, timeoutMs) {
return page.waitForFunction(
() => {
const prims = window.__godsEyeView.viewer.scene.primitives;
for (let i = 0; i < prims.length; i++) {
const p = prims.get(i);
if (p && p.constructor && p.constructor.name === 'Cesium3DTileset') {
return p.tilesLoaded === true;
}
}
return true;
},
{ timeout: timeoutMs }
).then(() => true).catch(() => false);
}
/** Teleport to a view (cancelling any camera flight first). */
function setView(page, v) {
return page.evaluate((view) => {
const gev = window.__godsEyeView;
try { gev.viewer.camera.cancelFlight(); } catch { /* no flight */ }
const ell = gev.viewer.scene.globe.ellipsoid;
const d2r = Math.PI / 180;
gev.viewer.camera.setView({
destination: ell.cartographicToCartesian({
longitude: view.lon * d2r, latitude: view.lat * d2r, height: view.height,
}),
orientation: {
heading: (view.heading || 0) * d2r,
pitch: (view.pitch ?? -90) * d2r,
roll: 0,
},
});
}, v);
}
/**
* Poll layer stats until a render newer than `sinceLastUpdate` settles.
* Returns the last sample with `renderSettled` reporting whether the
* freshness predicate was actually met — on timeout the sample is stale
* (possibly still-colored buckets from the previous view), and a caller that
* cannot tell the difference will pass a capture that never rendered.
*/
function waitForFreshRender(page, sinceLastUpdate, timeoutS = 40) {
return page.evaluate(async (since, tS) => {
const mod = window.__godsEyeView.dataManager.layers.get('traffic').module;
let s = null;
let renderSettled = false;
for (let i = 0; i < tS; i++) {
await new Promise((r) => setTimeout(r, 1000));
s = mod.getStats();
if (s.lastUpdate && s.lastUpdate !== since && s.count > 0 && !s.loading) {
renderSettled = true;
break;
}
}
return { ...s, renderSettled };
}, sinceLastUpdate, timeoutS);
}
async function main() {
console.log('\nJam-viz A/B capture');
console.log(` App URL : ${APP_URL}`);
console.log(` Out dir : ${OUT_DIR}\n`);
try {
const res = await fetch(APP_URL);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
console.error(`Dev server not reachable at ${APP_URL} (${e.message}).`);
process.exit(2);
}
const status = await fetch(`${APP_URL}/api/tomtom/status`).then((r) => r.json()).catch(() => null);
if (!status?.hasKey) {
console.error('Server has no TomTom key — the A/B needs live flow.');
process.exit(2);
}
fs.mkdirSync(OUT_DIR, { recursive: true });
const browser = await puppeteer.launch({
headless: 'new',
...(findChromeExecutable() ? { executablePath: findChromeExecutable() } : {}),
args: [
'--no-sandbox', '--disable-setuid-sandbox', '--use-gl=angle', '--use-angle=swiftshader',
'--disable-dev-shm-usage', '--disable-web-security',
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
'--window-size=1600,900',
],
});
const manifest = [];
let exitCode = 0;
try {
const page = await browser.newPage();
await page.setViewport({ width: 1600, height: 900 });
console.log('Loading app...');
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
{ timeout: 60000 },
);
await page.evaluate(() => window.__godsEyeView.dataManager.setEnabled('traffic', true));
await sleep(1500);
let firstView = true;
for (const view of VIEWS) {
console.log(`\n── ${view.label} (${view.id})`);
for (const mode of MODES) {
// Apply the mode, then force a re-render AT THE EXACT SAME framing:
// hop ~8.6 km east (defeats the overlap/center-shift gate — adjacent
// views alone can sit inside it), WAIT for the away render to commit
// (otherwise its late commit satisfies the freshness poll and the
// shot captures the away render), then return — the comeback load
// re-spawns with the new mode active.
await page.evaluate((m) => {
window.__godsEyeView.dataManager.layers.get('traffic').module.setParams({ jamViz: m });
}, mode);
const preHop = await page.evaluate(
() => window.__godsEyeView.dataManager.layers.get('traffic').module.getStats().lastUpdate,
);
await setView(page, { ...view, lon: view.lon + 0.09 });
const hop = await waitForFreshRender(page, preHop, 30);
const since = await page.evaluate(
() => window.__godsEyeView.dataManager.layers.get('traffic').module.getStats().lastUpdate,
);
await setView(page, view);
const stats = await waitForFreshRender(page, since);
await waitForTilesLoaded(page, firstView ? 30000 : 15000);
await sleep(1400); // flow-recolor race + heat-line pulse mid-bright
const final = await page.evaluate(() => {
const mod = window.__godsEyeView.dataManager.layers.get('traffic').module;
return { stats: mod.getStats(), params: mod.getParams() };
});
const shot = `${view.id}--${mode}.png`;
await page.screenshot({ path: path.join(OUT_DIR, shot) });
const b = final.stats.flowBuckets || {};
// `mode` is the CONFIGURED source, not feed health: a keyed run whose
// TomTom flow died still reports mode 'live' while every dot renders
// simulated white. Health is `!error` + actually-colored dots.
const colored = (b.free || 0) + (b.slow || 0) + (b.jam || 0);
// A capture whose waits timed out shows the PREVIOUS view's dots.
// That is a failed capture, not a footnote in the manifest.
const renderSettled = Boolean(hop.renderSettled && stats.renderSettled)
&& !final.stats.loading;
const ok = renderSettled && final.stats.mode === 'live' && !final.stats.error
&& final.stats.count > 0 && colored > 0
&& final.params.jamViz === mode
&& (mode !== 'none' || final.stats.heatLines === 0);
if (!ok) exitCode = 1;
console.log(` [${ok ? 'OK' : 'FAIL'}] ${shot} dots=${final.stats.count} `
+ `free=${b.free} slow=${b.slow} jam=${b.jam} sim=${b.sim} `
+ `heatLines=${final.stats.heatLines} cov=${final.stats.flowCoveragePct}% `
+ `err=${final.stats.error || 'none'} settled=${renderSettled}`);
manifest.push({
view: view.id, label: view.label, mode, shot,
capturedAt: new Date().toISOString(),
count: final.stats.count, flowBuckets: b,
heatLines: final.stats.heatLines,
closedRoads: final.stats.closedRoads,
coveragePct: final.stats.flowCoveragePct,
error: final.stats.error || null,
renderSettled,
});
firstView = false;
}
}
} catch (e) {
console.error('Harness error:', e);
exitCode = 3;
} finally {
fs.writeFileSync(path.join(OUT_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
await browser.close();
}
console.log(`\nShots + manifest.json → ${OUT_DIR}`);
process.exit(exitCode);
}
main().catch((e) => { console.error(e); process.exit(3); });
+284
View File
@@ -0,0 +1,284 @@
#!/usr/bin/env node
/**
* qa-traffic-preset-ab.mjs — A/B screenshot capture for preset-aware
* traffic dot styling (field finding 2026-07-23: NVG/FLIR/CRT
* post-FX crush the green/amber/red congestion coding).
*
* For each view, settles the live traffic layer ONCE, then for each
* post-FX style (normal / surveillance=NVG / thermal=FLIR / retro=CRT)
* drives the REAL StyleManager.setStyle path and captures:
* A — presetDots 'off' (shipped palette under that preset), and
* B — presetDots 'on' (preset-aware luminance/size encoding),
* at identical framing with the identical dot set — the kill switch
* restyles in place, no refetch, so the pair is a true A/B.
*
* Views: Mumbai Western Express Hwy (live rush window for jam coverage)
* + Austin I-35 downtown corridor (common target).
*
* Shots + per-shot layer stats land in --out
* (default qa-shots/preset-traffic, gitignored).
*
* Run: node scripts/qa-traffic-preset-ab.mjs --url http://localhost:4390
* Exits non-zero on harness failure (no live mode, zero dots, style or
* param not adopted by the layer).
*/
import puppeteer from 'puppeteer';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
const argv = process.argv.slice(2);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const APP_URL = getOpt('--url', 'http://localhost:4390');
const OUT_DIR = path.resolve(REPO_ROOT, getOpt('--out', 'qa-shots/preset-traffic'));
// Framing note: at shallow oblique pitch the camera sits 5+ km from the
// roads — free/slow dots (2 km depth punch) cull under the mesh and the
// rest fade to ~2 px, so no styling is judgeable there. Both views keep the
// camera close/steep enough that every bucket renders (same reason
// qa-traffic's proof shot is near-nadir).
const VIEWS = [
{
id: 'i35-close',
label: 'Austin I-35 jam cluster — close queue read',
lon: -97.7365, lat: 30.252, height: 1400, heading: 8, pitch: -65,
},
{
id: 'i35-nadir',
label: 'Austin I-35 corridor — near-nadir city scale',
lon: -97.7365, lat: 30.25, height: 2400, heading: 8, pitch: -70,
},
];
/**
* StyleManager preset names with user-facing labels + expected profile.
* `ironbow` flips the thermal palette uniform (0 = grayscale WHOT,
* 1 = Ironbow "Predator" ramp) — round 2 requires the dots to read in both.
*/
const STYLES = [
{ id: 'normal', name: 'normal', label: 'Normal', profile: 'normal' },
{ id: 'surveillance', name: 'surveillance', label: 'NVG', profile: 'mono' },
{ id: 'thermal', name: 'thermal', label: 'FLIR WHOT', profile: 'mono', ironbow: false },
{ id: 'thermal-ironbow', name: 'thermal', label: 'FLIR Ironbow', profile: 'mono', ironbow: true },
{ id: 'retro', name: 'retro', label: 'CRT', profile: 'crt' },
];
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
const findChromeExecutable = () =>
CHROME_EXECUTABLE_CANDIDATES.find((c) => { try { return fs.existsSync(c); } catch { return false; } }) || null;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/** Same pattern as qa-traffic-jamviz-ab: wait for the 3D tileset to settle. */
function waitForTilesLoaded(page, timeoutMs) {
return page.waitForFunction(
() => {
const prims = window.__godsEyeView.viewer.scene.primitives;
for (let i = 0; i < prims.length; i++) {
const p = prims.get(i);
if (p && p.constructor && p.constructor.name === 'Cesium3DTileset') {
return p.tilesLoaded === true;
}
}
return true;
},
{ timeout: timeoutMs }
).then(() => true).catch(() => false);
}
/** Enable traffic + teleport, then poll the layer until settled. */
function settleTraffic(page, view, { minCount = 100, timeoutS = 45 } = {}) {
return page.evaluate(async (v, minC, tS) => {
const gev = window.__godsEyeView;
await gev.dataManager.setEnabled('traffic', true);
const mod = gev.dataManager.layers.get('traffic').module;
try { gev.viewer.camera.cancelFlight(); } catch { /* no flight */ }
const ell = gev.viewer.scene.globe.ellipsoid;
const d2r = Math.PI / 180;
gev.viewer.camera.setView({
destination: ell.cartographicToCartesian({
longitude: v.lon * d2r, latitude: v.lat * d2r, height: v.height,
}),
orientation: {
heading: (v.heading || 0) * d2r, pitch: (v.pitch ?? -90) * d2r, roll: 0,
},
});
let s = null;
// `renderSettled` separates a real settle from a timeout returning the
// last (stale, possibly still-loading) sample — without it a capture of
// the previous view's dots passes.
let renderSettled = false;
for (let i = 0; i < tS; i++) {
await new Promise((r) => setTimeout(r, 1000));
s = mod.getStats();
if (s.count >= minC && !s.loading) {
renderSettled = true;
break;
}
}
return { ...s, renderSettled };
}, view, minCount, timeoutS);
}
async function main() {
console.log('\nPreset-aware traffic styling A/B capture');
console.log(` App URL : ${APP_URL}`);
console.log(` Out dir : ${OUT_DIR}\n`);
try {
const res = await fetch(APP_URL);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
console.error(`Dev server not reachable at ${APP_URL} (${e.message}).`);
process.exit(2);
}
const status = await fetch(`${APP_URL}/api/tomtom/status`).then((r) => r.json()).catch(() => null);
if (!status?.hasKey) {
console.error('Server has no TomTom key — the A/B needs live flow.');
process.exit(2);
}
fs.mkdirSync(OUT_DIR, { recursive: true });
const browser = await puppeteer.launch({
headless: 'new',
...(findChromeExecutable() ? { executablePath: findChromeExecutable() } : {}),
args: [
'--no-sandbox', '--disable-setuid-sandbox', '--use-gl=angle', '--use-angle=swiftshader',
'--disable-dev-shm-usage', '--disable-web-security',
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
'--window-size=1600,900',
],
});
const manifest = [];
let exitCode = 0;
try {
const page = await browser.newPage();
await page.setViewport({ width: 1600, height: 900 });
console.log('Loading app...');
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
{ timeout: 60000 },
);
await sleep(1500);
let firstView = true;
for (const view of VIEWS) {
console.log(`\n── ${view.label} (${view.id})`);
const settled = await settleTraffic(page, view);
// `mode` only says a TomTom key is configured; `error` says whether the
// flow feed is actually up. A degraded run renders simulated white dots
// and must not pass as a live styling capture — and a wait that timed
// out never rendered this view at all.
if (!settled || !settled.renderSettled || settled.count === 0
|| settled.mode !== 'live' || settled.error) {
console.error(' [FAIL] traffic never settled live '
+ `(count=${settled?.count} mode=${settled?.mode} `
+ `settled=${Boolean(settled?.renderSettled)} err=${settled?.error || 'none'})`);
exitCode = 1;
continue;
}
await waitForTilesLoaded(page, firstView ? 30000 : 15000);
firstView = false;
for (const style of STYLES) {
// Real user path: StyleManager.setStyle drives the post-FX stage AND
// the gev:style-change event the traffic layer listens to. The
// thermal palette uniform selects WHOT vs Ironbow.
await page.evaluate((s) => {
const sm = window.__godsEyeView.styleManager;
sm.setStyle(s.name, { applyPreset: true });
if (sm.stages.thermal) sm.stages.thermal.uniforms.palette = s.ironbow ? 1.0 : 0.0;
}, style);
await sleep(1800); // 500 ms intensity lerp + settle
// Round 2 legs: dots alone (DETECT off) vs dots + bucket-colored
// brackets (DENSE) — presetDots stays at its default 'on'.
for (const detect of ['off', 'on']) {
await page.evaluate((d) => {
window.__godsEyeView.styleManager._setDetectionMode(d === 'on' ? 'DENSE' : 'OFF');
}, detect);
await sleep(1200); // label-arbiter solve + a few rendered frames
const final = await page.evaluate(() => {
const mod = window.__godsEyeView.dataManager.layers.get('traffic').module;
return { stats: mod.getStats(), params: mod.getParams() };
});
const shot = `${view.id}--${style.id}--detect-${detect}.png`;
await page.screenshot({ path: path.join(OUT_DIR, shot) });
const b = final.stats.flowBuckets || {};
// Bucket colors are the whole point of this capture, so a degraded
// feed (mode 'live', flow down, every dot white) has to fail here.
const colored = (b.free || 0) + (b.slow || 0) + (b.jam || 0);
// A style swap can kick a re-render; capturing mid-load screenshots
// the previous style's dots.
const ok = !final.stats.loading
&& final.stats.mode === 'live' && !final.stats.error
&& final.stats.count > 0 && colored > 0
&& final.params.presetDots === 'on'
&& final.stats.stylePreset === style.name
&& final.stats.styleProfile === style.profile;
if (!ok) exitCode = 1;
console.log(` [${ok ? 'OK' : 'FAIL'}] ${shot} dots=${final.stats.count} `
+ `free=${b.free} slow=${b.slow} jam=${b.jam} sim=${b.sim} `
+ `stylePreset=${final.stats.stylePreset} profile=${final.stats.styleProfile} `
+ `err=${final.stats.error || 'none'} loading=${final.stats.loading}`);
manifest.push({
view: view.id, label: view.label,
style: style.id, styleLabel: style.label, detect, shot,
capturedAt: new Date().toISOString(),
count: final.stats.count, flowBuckets: b,
stylePreset: final.stats.stylePreset,
styleProfile: final.stats.styleProfile,
coveragePct: final.stats.flowCoveragePct,
// A rejected capture must look rejected in the manifest too, not
// just in the console verdict — record the gating fields.
ok,
loading: final.stats.loading,
error: final.stats.error || null,
renderSettled: final.renderSettled,
});
}
}
// Leave the page in the shipped default state between views.
await page.evaluate(() => {
const sm = window.__godsEyeView.styleManager;
sm.setStyle('normal', { applyPreset: true });
sm._setDetectionMode('OFF');
if (sm.stages.thermal) sm.stages.thermal.uniforms.palette = 0.0;
});
await sleep(1000);
}
} catch (e) {
console.error('Harness error:', e);
exitCode = 3;
} finally {
fs.writeFileSync(path.join(OUT_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
await browser.close();
}
console.log(`\nShots + manifest.json → ${OUT_DIR}`);
process.exit(exitCode);
}
main().catch((e) => { console.error(e); process.exit(3); });
+316
View File
@@ -0,0 +1,316 @@
#!/usr/bin/env node
/**
* qa-traffic.mjs — headless proof for the TomTom live-flow traffic layer.
*
* Drives the REAL app in headless Chromium against a dev server that has a
* TomTom key (default :4410) and asserts the live-mode contract, then uses
* request interception to fabricate the keyless state deterministically
* (no key removal needed, no upstream traffic).
*
* (i) LIVE mode — stats.mode 'live', dots rendered, colored buckets
* non-empty (free+slow+jam > 0), coverage > 0, tiles fetched > 0.
* (ii) C4 oblique bounds — at 2.5 km / -20° pitch the road-fetch box
* center lands within 12 km of the camera (look-at point), never
* the pre-fix horizon-biased midpoint (>25 km).
* (iii) Budget honesty — /api/tomtom/status dailyCount grows by no more
* than the tiles the page actually fetched this run.
* (iv) Uncovered-roads param — 'hide' renders zero sim dots; 'sim'
* restores them (Mumbai has partial coverage; Austin may be 100%,
* so this asserts on whichever view has sim dots, else records
* INCONCLUSIVE rather than a false failure).
* (v) KEYLESS fallback — with /api/tomtom/status intercepted to
* {hasKey:false}: mode 'sim', every dot white (buckets.sim ===
* count), and ZERO /api/tomtom/flow requests issued.
*
* Visual proof saved to qa-shots/ (gitignored).
*
* Run: node scripts/qa-traffic.mjs --url http://localhost:4410
* Exits non-zero on any FAIL. Does not commit anything.
*/
import puppeteer from 'puppeteer';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
const SHOTS_DIR = path.join(REPO_ROOT, 'qa-shots');
const argv = process.argv.slice(2);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const APP_URL = getOpt('--url', 'http://localhost:4410');
const HEADFUL = argv.includes('--headful');
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try { if (fs.existsSync(candidate)) return candidate; } catch { /* ignore */ }
}
return null;
}
const results = [];
function record(name, ok, detail) {
results.push({ name, ok, detail });
const tag = ok === null ? '\x1b[33mINCONCLUSIVE\x1b[0m' : ok ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
console.log(` [${tag}] ${name}${detail ? ` — ${detail}` : ''}`);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/** Enable traffic + teleport, then poll the layer until settled. */
async function settleTraffic(page, view, { minCount = 1, timeoutS = 30 } = {}) {
return page.evaluate(async (v, minC, tS) => {
const gev = window.__godsEyeView;
const dm = gev.dataManager;
await dm.setEnabled('traffic', true);
const mod = dm.layers.get('traffic').module;
const ell = gev.viewer.scene.globe.ellipsoid;
const d2r = Math.PI / 180;
// The app's intro flyTo animation clobbers a setView issued mid-flight —
// cancel any active tween before teleporting.
try { gev.viewer.camera.cancelFlight(); } catch { /* no flight active */ }
gev.viewer.camera.setView({
destination: ell.cartographicToCartesian({ longitude: v.lon * d2r, latitude: v.lat * d2r, height: v.height }),
orientation: { heading: (v.heading || 0) * d2r, pitch: (v.pitch ?? -90) * d2r, roll: 0 },
});
let s = null;
for (let i = 0; i < tS; i++) {
await new Promise((r) => setTimeout(r, 1000));
s = mod.getStats();
if (s.count >= minC && !s.loading) break;
}
return s;
}, view, minCount, timeoutS);
}
async function main() {
console.log('\nTomTom Live-Flow Traffic Proof (qa-traffic)');
console.log(` App URL : ${APP_URL}\n`);
try {
const res = await fetch(APP_URL);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
console.error(`\x1b[31mDev server not reachable at ${APP_URL} (${e.message}).\x1b[0m`);
process.exit(2);
}
const statusBefore = await fetch(`${APP_URL}/api/tomtom/status`).then((r) => r.json()).catch(() => null);
if (!statusBefore?.hasKey) {
console.error('\x1b[31mServer has no TomTom key — run against the keyed dev server (:4410).\x1b[0m');
process.exit(2);
}
fs.mkdirSync(SHOTS_DIR, { recursive: true });
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(findChromeExecutable() ? { executablePath: findChromeExecutable() } : {}),
args: [
'--no-sandbox', '--disable-setuid-sandbox', '--use-gl=angle', '--use-angle=swiftshader',
'--disable-dev-shm-usage', '--disable-web-security',
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
'--window-size=1440,900',
],
});
let exitCode = 0;
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
// Track flow-tile requests + traffic console lines for (ii)/(iii)/(v).
const flowRequests = [];
page.on('request', (req) => {
if (req.url().includes('/api/tomtom/flow/')) flowRequests.push(req.url());
});
const trafficLogs = [];
page.on('console', (msg) => {
const t = msg.text();
if (t.includes('[Data:Traffic]')) trafficLogs.push(t);
});
console.log('Loading app...');
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
{ timeout: 60000 },
);
await sleep(1500);
// ── (i) LIVE mode: San Antonio — fast Overpass extract, partial TomTom
// coverage (sim dots exist for (iv)). Mumbai proved too Overpass-cold for
// a deterministic harness; congestion colors assert the same either way.
console.log('\n(i) LIVE mode — San Antonio (partial coverage)...');
let mumbai = await settleTraffic(page, { lon: -98.4936, lat: 29.4241, height: 2800, heading: 23, pitch: -72 }, { minCount: 300, timeoutS: 45 });
if (!mumbai || mumbai.count === 0) {
// One retry with a nudged center to defeat the overlap gate (a slow
// first Overpass response can strand the initial load).
mumbai = await settleTraffic(page, { lon: -98.487, lat: 29.43, height: 2800, heading: 23, pitch: -72 }, { minCount: 300, timeoutS: 45 });
}
{
const b = mumbai.flowBuckets || {};
const colored = (b.free || 0) + (b.slow || 0) + (b.jam || 0);
record('LIVE: stats.mode === "live"', mumbai.mode === 'live', `mode=${mumbai.mode}`);
record('LIVE: dots rendered', mumbai.count > 0, `count=${mumbai.count}`);
record('LIVE: colored flow dots present (free+slow+jam > 0)', colored > 0,
`free=${b.free} slow=${b.slow} jam=${b.jam} sim=${b.sim}`);
record('LIVE: flow coverage > 0 and tiles fetched > 0',
mumbai.flowCoveragePct > 0 && mumbai.tilesFetched > 0,
`coverage=${mumbai.flowCoveragePct}% tiles=${mumbai.tilesFetched}`);
if (mumbai.mode !== 'live' || !(mumbai.count > 0) || !(colored > 0)) exitCode = 1;
await sleep(1200);
await page.screenshot({ path: path.join(SHOTS_DIR, 'traffic-live-flow.png') });
}
// ── (iv) uncovered-roads param — hide vs sim ─────────────────────────────
console.log('\n(iv) uncoveredRoads param — hide vs sim...');
if ((mumbai.flowBuckets?.sim || 0) > 0) {
const hid = await page.evaluate(async () => {
const gev = window.__godsEyeView;
const mod = gev.dataManager.layers.get('traffic').module;
const before = mod.getStats().lastUpdate;
mod.setParams({ uncoveredRoads: 'hide' });
const ell = gev.viewer.scene.globe.ellipsoid;
const d2r = Math.PI / 180;
// Shift far enough to defeat the overlap gate and force a re-render.
gev.viewer.camera.setView({
destination: ell.cartographicToCartesian({ longitude: -98.51 * d2r, latitude: 29.435 * d2r, height: 2800 }),
orientation: { heading: 0.4, pitch: -1.25, roll: 0 },
});
// Poll for a NEW render (lastUpdate changes) — the pre-shift stats
// would otherwise satisfy a count>0 check instantly.
let s = null;
for (let i = 0; i < 40; i++) {
await new Promise((r) => setTimeout(r, 1000));
s = mod.getStats();
if (s.lastUpdate !== before && s.count > 0 && !s.loading) break;
}
mod.setParams({ uncoveredRoads: 'sim' });
return s;
});
record('PARAM: hide mode renders zero sim (white) dots',
(hid.flowBuckets?.sim || 0) === 0 && hid.count > 0,
`count=${hid.count} sim=${hid.flowBuckets?.sim}`);
if ((hid.flowBuckets?.sim || 0) !== 0) exitCode = 1;
await page.screenshot({ path: path.join(SHOTS_DIR, 'traffic-live-hide-mode.png') });
} else {
record('PARAM: hide mode renders zero sim (white) dots', null,
'view had 100% coverage (no sim dots to hide) — inconclusive here, covered by unit tests');
}
// ── (ii) C4 oblique bounds ───────────────────────────────────────────────
console.log('\n(ii) C4 — oblique fetch bounds land at the look-at point...');
trafficLogs.length = 0;
await settleTraffic(page, { lon: -97.72, lat: 30.245, height: 2500, heading: 315, pitch: -20 }, { minCount: 100 });
{
const fetchLine = trafficLogs.find((t) => t.includes('fetch') && t.includes('['));
let ok = false; let detail = 'no fetch log captured';
if (fetchLine) {
const m = fetchLine.match(/\[(-?[\d.]+),(-?[\d.]+),(-?[\d.]+),(-?[\d.]+)\]/);
if (m) {
const [s, w, n, e] = m.slice(1).map(Number);
const cLat = (s + n) / 2; const cLon = (w + e) / 2;
const dKm = Math.sqrt(((cLat - 30.245) * 111) ** 2 + ((cLon - (-97.72)) * 111 * Math.cos(30.27 * Math.PI / 180)) ** 2);
ok = dKm > 1 && dKm <= 13; // look-at ~6.9 km ahead; clamp allows ≤12 (+margin)
detail = `box center (${cLat.toFixed(4)},${cLon.toFixed(4)}) is ${dKm.toFixed(1)} km from camera (want 1–13 km; pre-fix bug: 25+ km)`;
}
}
record('C4: oblique fetch box centers on the look-at point', ok, detail);
if (!ok) exitCode = 1;
await page.screenshot({ path: path.join(SHOTS_DIR, 'traffic-c4-oblique.png') });
}
// ── (iii) budget honesty ─────────────────────────────────────────────────
console.log('\n(iii) Budget — dailyCount grew by ≤ requests this run...');
{
const statusAfter = await fetch(`${APP_URL}/api/tomtom/status`).then((r) => r.json());
const grew = statusAfter.dailyCount - statusBefore.dailyCount;
const ok = grew >= 0 && grew <= flowRequests.length;
record('BUDGET: /api/tomtom/status growth ≤ page tile requests', ok,
`before=${statusBefore.dailyCount} after=${statusAfter.dailyCount} pageRequests=${flowRequests.length}`);
if (!ok) exitCode = 1;
}
// ── (v) KEYLESS fallback (intercepted — server key untouched) ────────────
console.log('\n(v) KEYLESS — intercepted status, expect pure simulation...');
await page.setRequestInterception(true);
const keylessFlowReqs = [];
page.on('request', (req) => {
const url = req.url();
if (url.includes('/api/tomtom/status')) {
req.respond({ status: 200, contentType: 'application/json', body: JSON.stringify({ hasKey: false }) });
return;
}
if (url.includes('/api/tomtom/flow/')) {
keylessFlowReqs.push(url);
req.respond({ status: 503, contentType: 'application/json', body: JSON.stringify({ error: 'no_key' }) });
return;
}
try { req.continue(); } catch { /* already handled */ }
});
await page.reload({ waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
{ timeout: 60000 },
);
await sleep(1500);
let simStats = await settleTraffic(page, { lon: -98.4936, lat: 29.4241, height: 3000 }, { minCount: 100, timeoutS: 45 });
if (!simStats || simStats.count === 0) {
// Public Overpass can throttle bursts across harness runs — one retry.
simStats = await settleTraffic(page, { lon: -98.487, lat: 29.43, height: 3000 }, { minCount: 100, timeoutS: 45 });
}
{
const b = simStats.flowBuckets || {};
const allWhite = simStats.count > 0 && b.sim === simStats.count && !b.free && !b.slow && !b.jam;
record('KEYLESS: stats.mode === "sim"', simStats.mode === 'sim', `mode=${simStats.mode}`);
record('KEYLESS: every dot is white simulation', allWhite,
`count=${simStats.count} sim=${b.sim} free=${b.free} slow=${b.slow} jam=${b.jam}`);
record('KEYLESS: zero flow-tile requests issued', keylessFlowReqs.length === 0,
`flowRequests=${keylessFlowReqs.length}`);
// Keyless is a designed fallback, not a fault: it must read SIMULATED
// without ever raising a layer error.
const keylessHonest = !simStats.error
&& String(simStats.loadingLabel || '').startsWith('SIMULATED');
record('KEYLESS: no error, and the label reads SIMULATED', keylessHonest,
`err=${simStats.error || 'none'} label="${simStats.loadingLabel}"`);
if (simStats.mode !== 'sim' || !allWhite || keylessFlowReqs.length !== 0 || !keylessHonest) {
exitCode = 1;
}
await sleep(1000);
await page.screenshot({ path: path.join(SHOTS_DIR, 'traffic-sim-keyless.png') });
}
} catch (e) {
console.error('\x1b[31mHarness error:\x1b[0m', e);
exitCode = 3;
} finally {
await browser.close();
}
const pass = results.filter((r) => r.ok === true).length;
const fail = results.filter((r) => r.ok === false).length;
const inconclusive = results.filter((r) => r.ok === null).length;
console.log('\n' + '─'.repeat(60));
console.log(` RESULT: ${pass} passed, ${fail} failed, ${inconclusive} inconclusive`);
console.log(` Shots : ${SHOTS_DIR}/traffic-*.png`);
console.log('─'.repeat(60) + '\n');
process.exit(exitCode || (fail > 0 ? 1 : 0));
}
main().catch((e) => { console.error(e); process.exit(3); });
+416
View File
@@ -0,0 +1,416 @@
#!/usr/bin/env node
/**
* qa-vessel-cards.mjs — operator-side visual proof harness for AIS cards in
* the shared world-overlay host.
*
* Drives the REAL app in headless Chromium, frames a busy port, waits for the
* photoreal tileset + vessel refresh to settle, and captures a full-viewport
* screenshot to the gitignored qa-shots/. Live AISStream data remains the
* default. An explicit synthetic mode uses the layer's dev-only evidence seam
* to exercise the production reconciliation/render path when AISStream is open
* but silent (https://github.com/aisstream/aisstream/issues/23 and
* https://github.com/aisstream/aisstream/issues/15).
*
* Usage:
* node scripts/qa-vessel-cards.mjs --tag new # current code
* node scripts/qa-vessel-cards.mjs --tag old # after swapping in the old layer file
* node scripts/qa-vessel-cards.mjs --ports rotterdam # subset
* node scripts/qa-vessel-cards.mjs --url http://localhost:4173
* node scripts/qa-vessel-cards.mjs --data synthetic # deterministic test fixture
* node scripts/qa-vessel-cards.mjs --data synthetic --headful # real-GPU capture
*
* A/B flow: run --tag new, `git show <old-rev>:src/data/aisLiveVessels.js >
* src/data/aisLiveVessels.js`, run --tag old, then `git restore` the file.
* Retained screenshots and same-basename JSON manifests land at
* qa-shots/vessel-cards-<live-aisstream|synthetic-fixture>-<tag>-<port>.*.
* The name and manifest both record the data source; the manifest also records
* headless/headful launch mode and the browser's WebGL renderer.
*/
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import puppeteer from 'puppeteer';
const argv = process.argv.slice(2);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const APP_URL = getOpt('--url', 'http://localhost:4173');
const TAG = getOpt('--tag', 'new');
const PORT_KEYS = getOpt('--ports', 'rotterdam,houston').split(',').map((s) => s.trim()).filter(Boolean);
const OUT_DIR = getOpt('--out', 'qa-shots');
export function isHeadfulMode(args = []) {
return args.includes('--headful');
}
const HEADFUL = isHeadfulMode(argv);
export const DEFAULT_DATA_MODE = 'live';
const DATA_PROVENANCE_SLUGS = Object.freeze({
live: 'live-aisstream',
synthetic: 'synthetic-fixture',
});
/** Parse the explicit evidence-source mode, defaulting to the live feed. */
export function parseDataMode(args = []) {
const index = args.indexOf('--data');
const mode = index >= 0 && args[index + 1] ? args[index + 1] : DEFAULT_DATA_MODE;
if (!Object.hasOwn(DATA_PROVENANCE_SLUGS, mode)) {
throw new Error(`Unknown --data mode '${mode}' (have: live, synthetic)`);
}
return mode;
}
/** Build a screenshot name whose data provenance cannot collide with another mode. */
export function vesselCardScreenshotFilename({ dataMode, tag, portKey }) {
const provenanceSlug = DATA_PROVENANCE_SLUGS[dataMode];
if (!provenanceSlug) throw new Error(`Unsupported vessel-card data mode '${dataMode}'`);
return `vessel-cards-${provenanceSlug}-${tag}-${portKey}.png`;
}
/** Build the same-basename provenance manifest filename for one PNG. */
export function vesselCardManifestFilename(screenshotFilename) {
if (!/\.png$/i.test(screenshotFilename)) {
throw new Error(`Expected a PNG screenshot filename, received '${screenshotFilename}'`);
}
return screenshotFilename.replace(/\.png$/i, '.json');
}
/** True when renderer evidence is available and does not identify a software rasterizer. */
export function isHardwareRenderer(renderer = {}) {
const description = `${renderer.vendor || ''} ${renderer.renderer || ''}`.trim();
return Boolean(description)
&& !/(swiftshader|software|llvmpipe|unavailable|unknown)/i.test(description);
}
/** Human-readable provenance emitted before any screenshot is captured. */
export function vesselCardProvenanceLines(dataMode) {
if (dataMode === 'live') {
return ['[PROVENANCE] LIVE AISStream feed; no synthetic vessel rows are injected.'];
}
if (dataMode === 'synthetic') {
return [
'[PROVENANCE] SYNTHETIC FIXTURE; this is not evidence of live AISStream availability.',
'[UPSTREAM] AISStream Issue #23: https://github.com/aisstream/aisstream/issues/23',
'[UPSTREAM] AISStream Issue #15: https://github.com/aisstream/aisstream/issues/15',
];
}
throw new Error(`Unsupported vessel-card data mode '${dataMode}'`);
}
/** Build the provenance record retained beside one screenshot. */
export function buildVesselCardManifest({
dataMode,
tag,
portKey,
screenshot,
headful,
renderer,
captureEvidence,
appUrl,
capturedAt,
}) {
const provenanceSlug = DATA_PROVENANCE_SLUGS[dataMode];
if (!provenanceSlug) throw new Error(`Unsupported vessel-card data mode '${dataMode}'`);
return {
schemaVersion: 1,
screenshot,
dataMode,
provenance: provenanceSlug,
source: dataMode === 'live' ? 'AISStream live feed' : 'GEV synthetic AIS fixture',
liveAisStreamAvailabilityAsserted: dataMode === 'live',
launchMode: headful ? 'headful' : 'headless',
renderer,
captureEvidence,
tag,
port: portKey,
appUrl,
capturedAt,
upstreamContext: dataMode === 'synthetic' ? [
'https://github.com/aisstream/aisstream/issues/23',
'https://github.com/aisstream/aisstream/issues/15',
] : [],
};
}
let DATA_MODE;
try {
DATA_MODE = parseDataMode(argv);
} catch (error) {
console.error(error.message);
process.exit(2);
}
/** Busy-port camera framings (lon/lat deg, height m, heading/pitch rad). */
const PORTS = {
rotterdam: { lon: 4.05, lat: 51.93, height: 18000, heading: 0.3, pitch: -1.25 },
houston: { lon: -95.08, lat: 29.72, height: 16000, heading: 5.9, pitch: -1.3 },
};
const SYNTHETIC_OFFSETS = [
[0, 0],
[-0.055, -0.025],
[-0.02, 0.045],
[0.045, -0.04],
[0.065, 0.03],
[-0.08, 0.055],
[0.01, -0.075],
[0.09, -0.015],
];
function syntheticVesselRows(portKey, port) {
const mmsiBase = portKey === 'rotterdam' ? 990100000 : 990200000;
return SYNTHETIC_OFFSETS.map(([lonOffset, latOffset], index) => ({
mmsi: String(mmsiBase + index),
name: `QA ${portKey.toUpperCase()} ${String(index + 1).padStart(2, '0')}`,
lat: port.lat + latOffset,
lon: port.lon + lonOffset,
speed: 3 + index,
course: (25 + index * 41) % 360,
heading: (25 + index * 41) % 360,
type: index % 3 === 0 ? 'Cargo' : index % 3 === 1 ? 'Tug' : 'Tanker',
destination: portKey.toUpperCase(),
}));
}
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try {
if (fs.existsSync(candidate)) return candidate;
} catch { /* fall through to Puppeteer's cache */ }
}
return null;
}
async function main() {
console.log(`\nAIS Vessel Cards — visual proof harness`);
console.log(` App URL : ${APP_URL}`);
console.log(` Tag : ${TAG}`);
console.log(` Data : ${DATA_MODE}`);
console.log(` Browser : ${HEADFUL ? 'headful (hardware renderer requested)' : 'headless (SwiftShader)'}`);
console.log(` Ports : ${PORT_KEYS.join(', ')}\n`);
for (const line of vesselCardProvenanceLines(DATA_MODE)) console.log(` ${line}`);
console.log('');
try {
const res = await fetch(APP_URL, { method: 'GET' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
console.error(`\x1b[31mDev server not reachable at ${APP_URL} (${e.message}).\x1b[0m`);
console.error(`Start it first: ./scripts/dev-fresh.sh`);
process.exit(2);
}
fs.mkdirSync(OUT_DIR, { recursive: true });
const chromeExecutable = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
...(!HEADFUL ? ['--use-gl=angle', '--use-angle=swiftshader'] : []),
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1600,900',
],
});
let failures = 0;
try {
for (const key of PORT_KEYS) {
const port = PORTS[key];
if (!port) {
console.error(` [\x1b[31mFAIL\x1b[0m] unknown port '${key}' (have: ${Object.keys(PORTS).join(', ')})`);
failures += 1;
continue;
}
const outPath = path.join(OUT_DIR, vesselCardScreenshotFilename({
dataMode: DATA_MODE,
tag: TAG,
portKey: key,
}));
console.log(` ▸ ${key} → ${outPath}`);
const page = await browser.newPage();
await page.setViewport({ width: 1600, height: 900 });
page.on('pageerror', (err) => console.error(` [page-error] ${err.message}`));
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 60000 });
// Frame the port (kill the intro flight first) and enable the layer.
const syntheticRows = DATA_MODE === 'synthetic' ? syntheticVesselRows(key, port) : [];
const dataEvidence = await page.evaluate(async ({ p, dataMode, fixtureRows }) => {
const gev = window.__godsEyeView;
const v = gev.viewer;
v.camera.cancelFlight?.();
v.scene.tweens?.removeAll?.();
const carto = {
longitude: (p.lon * Math.PI) / 180,
latitude: (p.lat * Math.PI) / 180,
height: p.height,
};
v.camera.setView({
destination: v.scene.globe.ellipsoid.cartographicToCartesian(carto),
orientation: { heading: p.heading, pitch: p.pitch, roll: 0 },
});
await gev.dataManager.setEnabled('ais-live-vessels', true);
if (dataMode !== 'synthetic') return { mode: 'live', injected: 0 };
const seam = gev.dataManager.layers.get('ais-live-vessels')?.module?.__focusEvidence;
if (!seam?.setVessels) throw new Error('Synthetic AIS evidence seam is unavailable');
const result = seam.setVessels(fixtureRows);
if (!result?.ok || result.count !== fixtureRows.length) {
throw new Error(`Synthetic AIS injection failed (${result?.count || 0}/${fixtureRows.length})`);
}
v.scene.requestRender?.();
return { mode: 'synthetic', injected: result.count };
}, { p: port, dataMode: DATA_MODE, fixtureRows: syntheticRows });
console.log(
` [DATA] ${dataEvidence.mode}${dataEvidence.injected ? ` (${dataEvidence.injected} injected)` : ''}; `
+ `artifact provenance=${DATA_PROVENANCE_SLUGS[DATA_MODE]}`,
);
// Wait for the photoreal tileset and at least one vessel refresh.
const settled = await page
.waitForFunction(() => {
const gev = window.__godsEyeView;
const t = gev.tileset;
const ais = gev.dataManager.getAll().find((l) => l.id === 'ais-live-vessels');
const count = ais?.stats?.count ?? 0;
return t?.tilesLoaded && count > 0;
}, { timeout: 90000, polling: 500 })
.then(() => true)
.catch(() => false);
// Re-assert the port framing: on slow (SwiftShader) runs the app's
// intro flight can start AFTER the first cancelFlight and land mid-wait,
// dragging the camera back to the boot city before the screenshot.
await page.evaluate((p) => {
const v = window.__godsEyeView.viewer;
v.camera.cancelFlight?.();
v.scene.tweens?.removeAll?.();
const carto = {
longitude: (p.lon * Math.PI) / 180,
latitude: (p.lat * Math.PI) / 180,
height: p.height,
};
v.camera.setView({
destination: v.scene.globe.ellipsoid.cartographicToCartesian(carto),
orientation: { heading: p.heading, pitch: p.pitch, roll: 0 },
});
}, port);
const resettled = settled ? true : await page
.waitForFunction(() => {
const gev = window.__godsEyeView;
const ais = gev.dataManager.getAll().find((l) => l.id === 'ais-live-vessels');
return gev.tileset?.tilesLoaded && (ais?.stats?.count ?? 0) > 0;
}, { timeout: 60000, polling: 500 })
.then(() => true)
.catch(() => false);
// Let two visibility/declutter cycles (800 ms) and host rendering settle.
await new Promise((r) => setTimeout(r, 2500));
const expectedFixtureIds = syntheticRows.map((row) => row.mmsi);
const overlayEvidence = await page.evaluate((fixtureIds) => {
const diagnostics = window.__gevWorldOverlay?.getDiagnostics?.();
const gl = window.__godsEyeView?.viewer?.scene?.context?._gl;
const debugInfo = gl?.getExtension?.('WEBGL_debug_renderer_info');
const seam = window.__godsEyeView?.dataManager?.layers
?.get('ais-live-vessels')?.module?.__focusEvidence;
const snapshotIds = new Set((seam?.snapshot?.() || []).map((record) => String(record.id)));
return {
hostCanvas: Boolean(document.getElementById('world-overlay-canvas')),
legacyCanvas: Boolean(document.getElementById('vessel-labels')),
entries: diagnostics?.entriesBySource?.['ais-live-vessels'] || 0,
painted: diagnostics?.paintedBySource?.['ais-live-vessels'] || 0,
fixtureRowsRetained: fixtureIds.length
? fixtureIds.every((id) => snapshotIds.has(String(id)))
: null,
renderer: gl ? {
vendor: String(gl.getParameter(debugInfo?.UNMASKED_VENDOR_WEBGL || gl.VENDOR) || 'unknown'),
renderer: String(gl.getParameter(debugInfo?.UNMASKED_RENDERER_WEBGL || gl.RENDERER) || 'unknown'),
} : { vendor: 'unavailable', renderer: 'unavailable' },
};
}, expectedFixtureIds);
await page.screenshot({ path: outPath });
const st = fs.statSync(outPath);
const hardwareRenderer = isHardwareRenderer(overlayEvidence.renderer);
const capturePassed = resettled
&& overlayEvidence.hostCanvas
&& !overlayEvidence.legacyCanvas
&& overlayEvidence.entries > 0
&& overlayEvidence.painted > 0
&& (DATA_MODE !== 'synthetic' || overlayEvidence.fixtureRowsRetained)
&& (!HEADFUL || hardwareRenderer);
const manifestPath = path.join(OUT_DIR, vesselCardManifestFilename(path.basename(outPath)));
const manifest = buildVesselCardManifest({
dataMode: DATA_MODE,
tag: TAG,
portKey: key,
screenshot: path.basename(outPath),
headful: HEADFUL,
renderer: overlayEvidence.renderer,
captureEvidence: {
tilesAndVesselsSettled: resettled,
injectedRows: dataEvidence.injected,
syntheticFixtureRowsRetained: overlayEvidence.fixtureRowsRetained,
overlayEntries: overlayEvidence.entries,
overlayPainted: overlayEvidence.painted,
hostCanvas: overlayEvidence.hostCanvas,
legacyCanvas: overlayEvidence.legacyCanvas,
screenshotBytes: st.size,
hardwareAccelerated: hardwareRenderer,
passed: capturePassed,
},
appUrl: APP_URL,
capturedAt: new Date().toISOString(),
});
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
const tiles = resettled ? 'tiles+vessels settled' : 'TIMEOUT waiting for tiles/vessels (shot anyway)';
console.log(` [\x1b[32mSHOT\x1b[0m] ${Math.round(st.size / 1024)} KB — ${tiles}`);
console.log(` [RENDERER] ${overlayEvidence.renderer.vendor} / ${overlayEvidence.renderer.renderer}`);
console.log(` [GPU] hardware accelerated=${hardwareRenderer}`);
if (DATA_MODE === 'synthetic') {
console.log(` [FIXTURE] retained at capture=${overlayEvidence.fixtureRowsRetained}`);
}
console.log(` [MANIFEST] ${manifestPath}`);
console.log(` [HOST] ${overlayEvidence.entries} entries / ${overlayEvidence.painted} painted`);
if (!capturePassed) failures += 1;
await page.close();
}
} finally {
await browser.close();
}
console.log('\nRetained PNG+JSON pairs are local QA evidence; each basename records live or synthetic provenance.');
console.log(`\nRESULT: ${PORT_KEYS.length - failures} passed, ${failures} failed, 0 skipped`);
process.exit(failures ? 1 : 0);
}
const invokedPath = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : '';
if (import.meta.url === invokedPath) {
main().catch((e) => {
console.error(e);
process.exit(1);
});
}
+282
View File
@@ -0,0 +1,282 @@
#!/usr/bin/env node
/**
* qa-vessel-datum.mjs — assertion harness for the AIS vessel vertical-datum
* pass described in docs/CURRENT-STATE.md.
*
* Drives the REAL app in headless Chromium against the LIVE AISStream feed
* and asserts, per port:
* 1. every vessel billboard is depth-test-free
* (disableDepthTestDistance === Infinity — locked principle #2), and
* 2. near-port anchors sit at the SEA SURFACE, not the ellipsoid: median
* ellipsoidal height within the EGM96 band (N + 3 m lift):
* Rotterdam N ≈ +43.7 → ≈ +46.7 m, Houston N ≈ −28.4 → ≈ −25.4 m.
* (At the pre-fix ellipsoid datum both medians would be ≈ 0–3 m.)
* Then captures a screenshot to the gitignored qa-shots/ as visual proof
* (Rotterdam chevrons were fully sea-mesh-occluded before the fix).
*
* Usage:
* node scripts/qa-vessel-datum.mjs
* node scripts/qa-vessel-datum.mjs --ports rotterdam --url http://localhost:4173
*/
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
const argv = process.argv.slice(2);
const getOpt = (name, dflt) => {
const i = argv.indexOf(name);
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
};
const APP_URL = getOpt('--url', 'http://localhost:4173');
const PORT_KEYS = getOpt('--ports', 'rotterdam,houston').split(',').map((s) => s.trim()).filter(Boolean);
const OUT_DIR = getOpt('--out', 'qa-shots');
/**
* Camera framings match qa-vessel-cards.mjs; heightBand is the expected
* near-port anchor ellipsoidal height (N + VESSEL_LIFT_M, ±~7 m for N drift
* across the port area + tolerance).
*/
const PORTS = {
rotterdam: {
lon: 4.05, lat: 51.93, height: 18000, heading: 0.3, pitch: -1.25,
heightBand: [40, 54],
},
houston: {
lon: -95.08, lat: 29.72, height: 16000, heading: 5.9, pitch: -1.3,
heightBand: [-33, -18],
},
};
/** Degrees of lat/lon around the port center counted as "near-port". */
const NEAR_DEG = 1.0;
/** Max billboards sampled for the depth-test sweep. */
const SAMPLE_CAP = 500;
const CHROME_EXECUTABLE_CANDIDATES = [
process.env.PUPPETEER_EXECUTABLE_PATH,
// Prefer puppeteer's version-pinned Chrome-for-Testing over the system
// Chrome: /Applications auto-updates underneath the harnesses, and its
// software-GL behavior shifts across majors (system Chrome 150 blew the
// tile-gated drain budget under SwiftShader on 2026-07-30 — six
// false-negative qa-cctv-v2 runs against a healthy build). A deterministic
// pinned browser beats the newest one for regression harnesses.
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
].filter(Boolean);
function findChromeExecutable() {
for (const candidate of CHROME_EXECUTABLE_CANDIDATES) {
try {
if (fs.existsSync(candidate)) return candidate;
} catch { /* fall through to Puppeteer's cache */ }
}
return null;
}
/**
* In-page probe: locate the vessel BillboardCollection (duck-typed — its
* billboards carry the vessel record as `id`, so `id.mmsi` exists), then
* report depth-test flags across a sample and ellipsoidal heights of
* near-port billboards.
*/
function probeVessels({ portLat, portLon, nearDeg, sampleCap }) {
const gev = window.__godsEyeView;
const viewer = gev?.viewer;
if (!viewer) return { error: 'no viewer' };
const ellipsoid = viewer.scene.globe.ellipsoid;
const prims = viewer.scene.primitives;
let collection = null;
for (let i = 0; i < prims.length; i += 1) {
const p = prims.get(i);
if (!p || typeof p.get !== 'function' || typeof p.length !== 'number' || p.length === 0) continue;
const first = p.get(0);
if (first && first.id && typeof first.id === 'object' && 'mmsi' in first.id
&& first.disableDepthTestDistance !== undefined) {
collection = p;
break;
}
}
if (!collection) return { error: 'vessel billboard collection not found' };
let depthFreeCount = 0;
let depthTestedCount = 0;
const nearHeights = [];
const step = Math.max(1, Math.floor(collection.length / sampleCap));
for (let i = 0; i < collection.length; i += step) {
const b = collection.get(i);
if (!b) continue;
if (b.disableDepthTestDistance === Number.POSITIVE_INFINITY) depthFreeCount += 1;
else depthTestedCount += 1;
const rec = b.id;
if (Number.isFinite(rec?.lat) && Number.isFinite(rec?.lon)
&& Math.abs(rec.lat - portLat) <= nearDeg && Math.abs(rec.lon - portLon) <= nearDeg) {
const carto = ellipsoid.cartesianToCartographic(b.position);
if (carto) nearHeights.push(carto.height);
}
}
nearHeights.sort((a, b) => a - b);
const median = nearHeights.length ? nearHeights[Math.floor(nearHeights.length / 2)] : null;
return {
total: collection.length,
sampled: depthFreeCount + depthTestedCount,
depthFreeCount,
depthTestedCount,
nearCount: nearHeights.length,
medianHeight: median,
minHeight: nearHeights[0] ?? null,
maxHeight: nearHeights[nearHeights.length - 1] ?? null,
};
}
async function main() {
console.log(`\nAIS Vessel Datum — assertion harness`);
console.log(` App URL : ${APP_URL}`);
console.log(` Ports : ${PORT_KEYS.join(', ')}\n`);
try {
const res = await fetch(APP_URL, { method: 'GET' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
console.error(`\x1b[31mDev server not reachable at ${APP_URL} (${e.message}).\x1b[0m`);
console.error(`Start it first: ./scripts/dev-fresh.sh`);
process.exit(2);
}
fs.mkdirSync(OUT_DIR, { recursive: true });
const chromeExecutable = findChromeExecutable();
const browser = await puppeteer.launch({
headless: 'new',
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
'--use-angle=swiftshader',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=1600,900',
],
});
let failures = 0;
try {
for (const key of PORT_KEYS) {
const port = PORTS[key];
if (!port) {
console.error(` [\x1b[31mFAIL\x1b[0m] unknown port '${key}' (have: ${Object.keys(PORTS).join(', ')})`);
failures += 1;
continue;
}
console.log(` ▸ ${key}`);
const page = await browser.newPage();
await page.setViewport({ width: 1600, height: 900 });
page.on('pageerror', (err) => console.error(` [page-error] ${err.message}`));
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 60000 });
// Frame the port (kill the intro flight first) and enable the layer.
await page.evaluate(async (p) => {
const gev = window.__godsEyeView;
const v = gev.viewer;
v.camera.cancelFlight?.();
v.scene.tweens?.removeAll?.();
const carto = {
longitude: (p.lon * Math.PI) / 180,
latitude: (p.lat * Math.PI) / 180,
height: p.height,
};
v.camera.setView({
destination: v.scene.globe.ellipsoid.cartographicToCartesian(carto),
orientation: { heading: p.heading, pitch: p.pitch, roll: 0 },
});
await gev.dataManager.setEnabled('ais-live-vessels', true);
}, port);
// Wait for rows + the geoid re-floor: poll until near-port anchors sit
// inside the expected band (both ports' bands exclude the pre-fix 0–3 m
// ellipsoid datum, so this only settles once the lift is applied).
const probeArgs = { portLat: port.lat, portLon: port.lon, nearDeg: NEAR_DEG, sampleCap: SAMPLE_CAP };
const deadline = Date.now() + 120000;
let probe = null;
let settled = false;
while (Date.now() < deadline) {
probe = await page.evaluate(probeVessels, probeArgs);
if (probe && !probe.error && probe.nearCount >= 3
&& probe.medianHeight !== null
&& probe.medianHeight >= port.heightBand[0]
&& probe.medianHeight <= port.heightBand[1]) {
settled = true;
break;
}
await new Promise((r) => setTimeout(r, 2000));
}
const fmt = (v) => (v === null || v === undefined ? '—' : v.toFixed ? v.toFixed(1) : String(v));
if (!probe || probe.error) {
console.error(` [\x1b[31mFAIL\x1b[0m] probe error: ${probe?.error || 'no probe result'}`);
failures += 1;
} else {
const heightOk = settled;
const depthOk = probe.depthTestedCount === 0 && probe.depthFreeCount > 0;
console.log(
` vessels=${probe.total} sampled=${probe.sampled} nearPort=${probe.nearCount} ` +
`medianH=${fmt(probe.medianHeight)}m (band ${port.heightBand[0]}..${port.heightBand[1]}) ` +
`range=${fmt(probe.minHeight)}..${fmt(probe.maxHeight)}m`
);
console.log(
` [${depthOk ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m'}] ` +
`depth-test-free sprites: ${probe.depthFreeCount}/${probe.sampled} (locked principle #2)`
);
console.log(
` [${heightOk ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m'}] ` +
`sea-surface anchor datum (N + lift, locked principle #1)`
);
if (!depthOk) failures += 1;
if (!heightOk) failures += 1;
}
// Visual proof — re-assert the framing (the app's intro flight can land
// mid-probe on slow runs), wait for the photoreal tileset, screenshot.
await page.evaluate((p) => {
const v = window.__godsEyeView.viewer;
v.camera.cancelFlight?.();
v.scene.tweens?.removeAll?.();
const carto = {
longitude: (p.lon * Math.PI) / 180,
latitude: (p.lat * Math.PI) / 180,
height: p.height,
};
v.camera.setView({
destination: v.scene.globe.ellipsoid.cartographicToCartesian(carto),
orientation: { heading: p.heading, pitch: p.pitch, roll: 0 },
});
}, port);
await page
.waitForFunction(() => window.__godsEyeView?.tileset?.tilesLoaded, { timeout: 60000, polling: 500 })
.catch(() => console.log(' (tileset settle timeout — screenshotting anyway)'));
await new Promise((r) => setTimeout(r, 2500));
const outPath = path.join(OUT_DIR, `vessel-datum-${key}.png`);
await page.screenshot({ path: outPath });
console.log(` [SHOT] ${outPath}`);
await page.close();
}
} finally {
await browser.close();
}
console.log(failures ? `\n\x1b[31m${failures} failure(s)\x1b[0m` : '\n\x1b[32mAll datum checks passed\x1b[0m');
process.exit(failures ? 1 : 0);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
+349
View File
@@ -0,0 +1,349 @@
#!/usr/bin/env node
/**
* Regression proof for the view-target prewarm crash.
*
* `getViewTargetCartographic()` converts whatever `scene.pickPosition()` hands
* back into a Cartographic. Over empty sky the depth sample is degenerate, the
* pick yields a non-finite / zero-magnitude Cartesian, and
* `Cartographic.fromCartesian` throws `DeveloperError: normalized result is not
* a number`. Because the prewarm runs inside `requestIdleCallback`, the throw
* escapes as an UNCAUGHT page error on a plain camera flight — a red console
* error on the demo path, with no scene and no tracking involved.
*
* This harness flies the camera the way the demo does and asserts the flight
* produces zero uncaught errors and zero unhandled rejections.
*
* QA_BASE_URL=http://localhost:4173 node scripts/qa-view-target-prewarm.mjs
*
* `QA_LABEL=before` names the evidence files so a before/after pair can sit
* side by side in qa-shots/prewarm/.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import puppeteer from 'puppeteer';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const shotsDir = path.join(repoRoot, 'qa-shots', 'prewarm');
const appUrl = process.env.QA_BASE_URL || 'http://localhost:4173';
const label = process.env.QA_LABEL || 'after';
const headful = process.argv.includes('--headful');
fs.mkdirSync(shotsDir, { recursive: true });
const chromeCandidates = [
process.env.PUPPETEER_EXECUTABLE_PATH,
(() => { try { return puppeteer.executablePath(); } catch { return null; } })(),
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
].filter(Boolean);
const executablePath = chromeCandidates.find((candidate) => fs.existsSync(candidate));
const browser = await puppeteer.launch({
headless: headful ? false : 'new',
...(executablePath ? { executablePath } : {}),
args: ['--use-angle=metal', '--enable-gpu', '--no-sandbox'],
});
const page = await browser.newPage();
const failures = [];
const pageErrors = [];
const consoleErrors = [];
// Keep the message AND the stack: an ownerless promise rejection is reported
// here with an "Uncaught (in promise)" message but a stack that does not say
// so, while a Cesium DeveloperError puts the useful frames only in the stack.
page.on('pageerror', (error) => pageErrors.push(
`${String(error?.message || error)}\n${String(error?.stack || '')}`.trim(),
));
page.on('console', (message) => {
if (message.type() !== 'error') return;
const text = message.text();
if (/Failed to load resource/i.test(text)) return;
consoleErrors.push(text);
});
// Unhandled rejections do not surface through `pageerror`, so capture them in
// the page and read the buffer back after the flight.
await page.evaluateOnNewDocument(() => {
window.__qaPrewarmRejections = [];
window.addEventListener('unhandledrejection', (event) => {
const reason = event.reason;
window.__qaPrewarmRejections.push(String(reason?.stack || reason?.message || reason));
});
});
const check = (name, passed, detail) => {
console.log(` [${passed ? 'PASS' : 'FAIL'}] ${name}${detail ? ` — ${detail}` : ''}`);
if (!passed) failures.push(name);
};
/** High, straight-down, and horizon-grazing views all miss the rendered mesh. */
const FLIGHT_LEGS = [
{ name: 'austin-high', lon: -97.7431, lat: 30.2672, height: 900_000, pitch: -90 },
{ name: 'ocean-horizon', lon: -140.0, lat: 5.0, height: 2_500_000, pitch: -12 },
{ name: 'polar-limb', lon: 10.0, lat: 82.0, height: 6_000_000, pitch: -20 },
{ name: 'globe-out', lon: -30.0, lat: 12.0, height: 14_000_000, pitch: -90 },
];
let rejections = [];
try {
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 90_000 });
await page.waitForFunction(() => window.__godsEyeView?.viewer, { timeout: 90_000 });
await page.waitForFunction(
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 90_000 },
);
// Boot noise (tile / network warm-up) is not what this harness is about.
await new Promise((resolve) => setTimeout(resolve, 4_000));
pageErrors.length = 0;
consoleErrors.length = 0;
await page.evaluate(() => { window.__qaPrewarmRejections.length = 0; });
const legReports = [];
for (const leg of FLIGHT_LEGS) {
const report = await page.evaluate(async (spec) => {
const { viewer } = window.__godsEyeView;
// No window.Cesium global (qa-cctv-v2 / qa-height-datum precedent) —
// borrow Cartesian3's statics off a live camera-position instance.
const Cartesian3 = viewer.camera.position.constructor;
await new Promise((resolve) => {
viewer.camera.flyTo({
destination: Cartesian3.fromDegrees(spec.lon, spec.lat, spec.height),
orientation: { heading: 0, pitch: (spec.pitch * Math.PI) / 180, roll: 0 },
duration: 1.2,
complete: resolve,
cancel: resolve,
});
});
// Let moveEnd (120 ms debounce) + requestIdleCallback (500 ms timeout) run.
await new Promise((resolve) => setTimeout(resolve, 1_500));
// Record what the center pick actually yields so the root cause is
// visible in the evidence dump rather than merely asserted.
const scene = viewer.scene;
const canvas = scene.canvas;
const center = {
x: (canvas.clientWidth || canvas.width) / 2,
y: (canvas.clientHeight || canvas.height) / 2,
};
let picked;
try {
const raw = scene.pickPosition(center);
picked = raw ? { x: raw.x, y: raw.y, z: raw.z } : null;
} catch (error) {
picked = { threw: String(error?.message || error) };
}
return { leg: spec.name, picked };
}, leg);
legReports.push(report);
console.log(` · ${report.leg}: center pick = ${JSON.stringify(report.picked)}`);
}
await page.screenshot({ path: path.join(shotsDir, `${label}-final-view.png`) });
// ── Phase 2: the degenerate depth pick, injected ──────────────────────────
// A depth pick over empty sky can hand back a Cartesian that is not a place.
// Cesium produces those only under conditions this harness cannot schedule
// reliably, so the precondition is injected directly: replace
// `scene.pickPosition` with one that returns each degenerate shape in turn.
// Everything downstream — the moveEnd prewarm inside requestIdleCallback and
// the HUD summary interval — is the real shipped code.
const injection = await page.evaluate(async () => {
const { viewer, styleManager } = window.__godsEyeView;
const Cartesian3 = viewer.camera.position.constructor;
const shapes = [
// Throws inside Cesium's normalize.
{ name: 'nan', value: new Cartesian3(Number.NaN, Number.NaN, Number.NaN) },
{ name: 'origin', value: new Cartesian3(0, 0, 0) },
{ name: 'infinite', value: new Cartesian3(Number.POSITIVE_INFINITY, 0, 0) },
// Converts SILENTLY to a point 6,378 km underground — the quiet failure a
// bare non-zero check lets through, which then reverse-geocodes 0°, 0°.
{ name: 'core-interior', value: new Cartesian3(500, 0, 0) },
// Finite, but large enough to overflow the geodetic iteration into NaN.
{ name: 'absurd-magnitude', value: new Cartesian3(1e155, 0, 0) },
];
const scene = viewer.scene;
const originalPick = Object.getOwnPropertyDescriptor(scene, 'pickPosition');
let current = shapes[0].value;
// The counter proves the prewarm re-picked under injection, so a clean
// result cannot be clean merely because nothing ran.
let pickCalls = 0;
scene.pickPosition = () => { pickCalls += 1; return current; };
const hudErrors = [];
const nudge = (step) => {
const carto = viewer.camera.positionCartographic;
viewer.camera.setView({
destination: Cartesian3.fromRadians(
carto.longitude + 0.0015 * (step + 1),
carto.latitude,
carto.height,
),
orientation: { heading: viewer.camera.heading, pitch: viewer.camera.pitch, roll: 0 },
});
viewer.camera.moveEnd.raiseEvent();
};
try {
for (let step = 0; step < shapes.length; step += 1) {
current = shapes[step].value;
// Nudge the camera so the 2.5 s view-target cache misses and the
// moveEnd prewarm actually re-picks.
nudge(step);
// moveEnd debounce (120 ms) + requestIdleCallback timeout (500 ms).
await new Promise((resolve) => setTimeout(resolve, 900));
// The HUD summary walks the same view target. Enter through the REAL
// boundary the 15 s interval uses — `void this._updateSummary(true)`,
// with nobody owning the promise — so a rejection lands in the page's
// unhandledrejection buffer exactly as it would in production.
void styleManager.hud._updateSummary(true);
await new Promise((resolve) => setTimeout(resolve, 250));
// …and separately confirm the context itself is clean, with its error
// attributed to this shape rather than pooled into the page buffer.
try {
await styleManager.hud._summaryContext();
} catch (error) {
hudErrors.push(`${shapes[step].name}: ${String(error?.message || error)}`);
}
}
} finally {
if (originalPick) Object.defineProperty(scene, 'pickPosition', originalPick);
else delete scene.pickPosition;
}
return { hudErrors, shapes: shapes.length, pickCalls };
});
console.log(` · injected ${injection.shapes} degenerate pick shapes; ${injection.pickCalls} pick(s) served; HUD context threw ${injection.hudErrors.length}×`);
await new Promise((resolve) => setTimeout(resolve, 1_500));
check(
'the injected picks actually reached the shipped code',
injection.pickCalls > 0,
`${injection.pickCalls} pick call(s) served`,
);
check(
'a degenerate depth pick does not break the HUD view-target context',
injection.hudErrors.length === 0,
injection.hudErrors.length ? injection.hudErrors.slice(0, 5).join(' | ').slice(0, 500) : 'clean',
);
// ── Phase 3: the HUD summary's own async boundary ─────────────────────────
// Phase 2 proves the guard keeps the context healthy. This proves the
// boundary holds even when it is NOT — `_updateSummary` awaits the context
// before its own try, and every caller invokes it as `void`, so any rejection
// from anywhere in that walk is ownerless. Isolate the boundary by making the
// context reject, then enter through the interval's exact statement.
// The QA marker makes this round's rejection attributable no matter which
// channel reports it.
const boundaryMarker = 'QA: scene context unavailable';
const pageErrorsBeforeBoundary = pageErrors.length;
const rejectionsBeforeBoundary = await page.evaluate(() => window.__qaPrewarmRejections.length);
const boundary = await page.evaluate((marker) => {
const hud = window.__godsEyeView.styleManager.hud;
window.__qaPriorSummaryContext = hud._summaryContext;
window.__qaStubCalls = 0;
hud._summaryContext = async () => {
window.__qaStubCalls += 1;
throw new Error(marker);
};
// `_updateSummary` returns before the await unless metrics exist and the
// summary is dirty. Satisfy both, or this check passes vacuously.
hud._latestMetrics = hud._latestMetrics || {};
hud._summaryRequest = null;
hud._summaryDirty = true;
// Byte-for-byte the statement in the HUD's summary interval. Nothing waits
// inside this evaluate: an ownerless rejection must be observed from
// OUTSIDE the page, because the browser reports it to the driver rather
// than always reaching an in-page listener.
void hud._updateSummary(true);
return window.__qaStubCalls;
}, boundaryMarker);
await new Promise((resolve) => setTimeout(resolve, 1_500));
const boundaryStubCalls = await page.evaluate(() => {
const hud = window.__godsEyeView.styleManager.hud;
hud._summaryContext = window.__qaPriorSummaryContext;
return window.__qaStubCalls;
});
const boundaryRejections = [
...pageErrors.slice(pageErrorsBeforeBoundary),
...(await page.evaluate((n) => window.__qaPrewarmRejections.slice(n), rejectionsBeforeBoundary)),
].filter((text) => text.includes(boundaryMarker));
console.log(` · void _updateSummary() reached the context ${boundaryStubCalls}× (sync ${boundary}) → ${boundaryRejections.length} escaped rejection(s)`);
check(
'the void _updateSummary() call actually reached its context await',
boundaryStubCalls > 0,
`${boundaryStubCalls} call(s)`,
);
check(
'a rejecting summary context does not escape the void _updateSummary() call',
boundaryRejections.length === 0,
boundaryRejections.length ? boundaryRejections.join(' | ').slice(0, 400) : 'clean',
);
// An ownerless rejection reaches the driver as a page error prefixed
// "Uncaught (in promise)"; the in-page listener is a second, less reliable
// channel. Union both so neither can hide one.
const inPageRejections = await page.evaluate(() => window.__qaPrewarmRejections.slice());
rejections = [
...pageErrors.filter((text) => /Uncaught \(in promise\)/i.test(text)),
...inPageRejections,
];
// Cesium's DeveloperError stringifies as "DeveloperError: DeveloperError",
// so the diagnostic text only survives on the caught HUD errors — scan those
// too or this check reads clean while the page is throwing.
const allText = [...pageErrors, ...consoleErrors, ...rejections, ...injection.hudErrors];
const normalizeErrors = allText
.filter((text) => /normalized result is not a number/i.test(text));
check(
'no uncaught page errors across the flight and the degenerate picks',
pageErrors.length === 0,
pageErrors.length
? `${pageErrors.length}: ${pageErrors.slice(0, 3).join(' | ').slice(0, 400)}`
: 'clean',
);
check(
'no unhandled promise rejections across the flight and the degenerate picks',
rejections.length === 0,
rejections.length
? `${rejections.length}: ${rejections.slice(0, 3).join(' | ').slice(0, 400)}`
: 'clean',
);
check(
'no "normalized result is not a number" DeveloperError on the flight',
normalizeErrors.length === 0,
normalizeErrors.length ? `${normalizeErrors.length} occurrence(s)` : 'clean',
);
check(
'no console errors on the flight',
consoleErrors.length === 0,
consoleErrors.length
? `${consoleErrors.length}: ${consoleErrors.slice(0, 3).join(' | ').slice(0, 400)}`
: 'clean',
);
fs.writeFileSync(
path.join(shotsDir, `${label}-console.json`),
`${JSON.stringify({
appUrl,
label,
capturedAt: new Date().toISOString(),
legs: legReports,
injectedShapes: injection.shapes,
injectedPickCalls: injection.pickCalls,
hudContextErrors: injection.hudErrors,
voidBoundaryRejections: boundaryRejections,
pageErrors,
consoleErrors,
unhandledRejections: rejections,
normalizeErrors,
}, null, 2)}\n`,
);
} catch (error) {
check('harness ran to completion', false, String(error?.message || error));
} finally {
await browser.close();
}
console.log(`\n${failures.length ? `FAIL (${failures.length})` : 'PASS'} — evidence in ${path.relative(repoRoot, shotsDir)}/`);
process.exit(failures.length ? 1 : 0);
File diff suppressed because it is too large. Load diff
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env node
/**
* Credentialed AI voice acceptance using a prerecorded Chromium microphone.
*
* Run: node scripts/qa-voice-wav.mjs http://localhost:4189
*/
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import puppeteer from 'puppeteer';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const appUrl = process.argv[2] || 'http://localhost:4189';
const wavPath = process.argv[3]
|| path.join(repoRoot, 'scripts', 'fixtures', 'voice', 'full-globe-turn-on-radio.wav');
const expectedFixtureSha256 = 'b57af70db1922b72fec2c6c58348ccd3309e10aa1e8edec2890277dff26cc7bb';
if (!wavPath || !fs.existsSync(wavPath)) {
console.error(`WAV fixture not found: ${wavPath || '(missing argument)'}`);
process.exit(2);
}
const fixtureSha256 = createHash('sha256').update(fs.readFileSync(wavPath)).digest('hex');
if (fixtureSha256 !== expectedFixtureSha256) {
console.error(`Unexpected WAV fixture SHA-256: ${fixtureSha256}`);
process.exit(2);
}
const appOrigin = new URL(appUrl).origin;
const browser = await puppeteer.launch({
headless: 'new',
executablePath: puppeteer.executablePath(),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--use-fake-device-for-media-stream',
`--use-file-for-fake-audio-capture=${wavPath}%noloop`,
'--autoplay-policy=no-user-gesture-required',
'--window-size=1440,900',
],
});
try {
const context = browser.defaultBrowserContext();
await context.overridePermissions(appOrigin, ['microphone']);
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
const consoleErrors = [];
page.on('console', (message) => {
if (message.type() === 'error') consoleErrors.push(message.text());
});
page.on('pageerror', (error) => consoleErrors.push(error.message));
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });
await page.waitForFunction(() => (
window.__godsEyeView?.voiceCommands
&& document.getElementById('gev-voice-button')
), { timeout: 30_000 });
const readState = () => page.evaluate(() => {
const voice = window.__godsEyeView?.voiceCommands;
const radio = window.__godsEyeView?.dataManager?.layers?.get('radio')?.module;
const radioState = radio?.getUIState?.() || null;
const camera = window.__godsEyeView?.viewer?.camera;
const cartographic = camera?.positionCartographic;
return {
at: Date.now(),
voiceStatus: voice?.status || null,
voiceDetail: document.getElementById('gev-voice-detail')?.textContent?.trim() || null,
cameraHeightM: cartographic?.height ?? null,
radioEnabled: radioState?.enabled ?? null,
radioAudioState: radioState?.audioState ?? null,
radioStation: radioState?.selected?.name || null,
radioVoiceDucked: radioState?.voiceDucked ?? null,
};
});
const initial = await readState();
await page.evaluate(() => document.getElementById('gev-voice-button').click());
const timeline = [initial];
let lastSignature = JSON.stringify(initial);
const deadline = Date.now() + 75_000;
let finalState = initial;
while (Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 500));
finalState = await readState();
const signature = JSON.stringify({
voiceStatus: finalState.voiceStatus,
voiceDetail: finalState.voiceDetail,
cameraHeightM: Math.round(finalState.cameraHeightM || 0),
radioEnabled: finalState.radioEnabled,
radioAudioState: finalState.radioAudioState,
radioStation: finalState.radioStation,
radioVoiceDucked: finalState.radioVoiceDucked,
});
if (signature !== lastSignature) {
timeline.push(finalState);
lastSignature = signature;
}
if (
finalState.voiceStatus === 'idle'
&& finalState.radioAudioState === 'playing'
&& finalState.radioVoiceDucked === false
&& Number(finalState.cameraHeightM) >= 10_000_000
) break;
if (finalState.voiceStatus === 'error') break;
}
const result = {
ok: finalState.voiceStatus === 'idle'
&& finalState.radioAudioState === 'playing'
&& finalState.radioVoiceDucked === false
&& Number(finalState.cameraHeightM) >= 10_000_000,
fixture: wavPath,
fixtureSha256,
appUrl,
elapsedMs: finalState.at - initial.at,
finalState,
timeline,
consoleErrors,
};
console.log(JSON.stringify(result, null, 2));
process.exitCode = result.ok ? 0 : 1;
} finally {
await browser.close();
}
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env node
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadEnv } from 'vite';
/**
* Read one dotenv key with Vite's parser; no file content is executed.
*
* Reports what the FILES say. `loadEnv` lets `process.env` override the parsed
* files, so an inherited export — an empty one above all, which is how a shell
* says "unset" to this script's callers — would otherwise mask the value the
* user wrote down. The key is hidden for the duration of the read and restored
* afterwards, leaving the caller's environment untouched.
*/
export function readDotenvValue(variableName, rootDir = process.cwd(), mode = 'development') {
const key = String(variableName || '').trim();
if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) return '';
const inherited = Object.prototype.hasOwnProperty.call(process.env, key)
? process.env[key]
: undefined;
if (inherited !== undefined) delete process.env[key];
try {
const env = loadEnv(mode, path.resolve(rootDir), '');
return String(env[key] ?? '');
} finally {
if (inherited !== undefined) process.env[key] = inherited;
}
}
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : '';
if (invokedPath === fileURLToPath(import.meta.url)) {
process.stdout.write(readDotenvValue(process.argv[2], process.argv[3] || process.cwd()));
}
+102
View File
@@ -0,0 +1,102 @@
import { readdirSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
export const ALLOCATION_TEST_FILES = Object.freeze([
'src/data/focusAllocations.test.mjs',
'src/overlays/worldOverlayAllocation.test.mjs',
]);
/** Whether this runtime matches the one the allocation budgets were calibrated on. */
export function isCalibratedAllocationRuntime(version = process.versions.node) {
return Number.parseInt(String(version).split('.')[0], 10) === 24;
}
/** Require the runtime on which allocation budgets were calibrated. */
export function assertNode24AllocationRuntime(version = process.versions.node) {
if (!isCalibratedAllocationRuntime(version)) {
throw new Error(`Allocation budgets require the calibrated Node 24 runtime; received ${version}`);
}
return version;
}
/** Discover repository unit tests in stable path order. */
export function discoverUnitTestFiles(root = process.cwd()) {
const sourceRoot = path.join(root, 'src');
const files = [];
const visit = (directory) => {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) visit(absolute);
else if (entry.isFile() && entry.name.endsWith('.test.mjs')) {
files.push(path.relative(root, absolute).split(path.sep).join('/'));
}
}
};
visit(sourceRoot);
return files.sort();
}
/** Partition ordinary parallel tests from the two GC-bracketed probes. */
export function buildUnitTestPlan(files) {
const known = new Set(files);
const missingAllocationTests = ALLOCATION_TEST_FILES.filter((file) => !known.has(file));
if (missingAllocationTests.length) {
throw new Error(`Missing allocation microbenchmarks: ${missingAllocationTests.join(', ')}`);
}
const allocationSet = new Set(ALLOCATION_TEST_FILES);
return {
parallel: files.filter((file) => !allocationSet.has(file)).sort(),
serializedAllocations: [...ALLOCATION_TEST_FILES],
};
}
/** Build the isolated Node invocation for one GC-bracketed allocation probe. */
export function allocationTestArgs(file) {
if (!ALLOCATION_TEST_FILES.includes(file)) {
throw new Error(`Not an allocation microbenchmark: ${file}`);
}
return ['--expose-gc', '--test', '--test-concurrency=1', file];
}
function runTests(args) {
const result = spawnSync(process.execPath, args, {
cwd: process.cwd(),
stdio: 'inherit',
env: process.env,
});
if (result.error) throw result.error;
return result.status ?? 1;
}
export function runUnitTests() {
const plan = buildUnitTestPlan(discoverUnitTestFiles());
const parallelStatus = runTests(['--test', ...plan.parallel]);
if (parallelStatus !== 0) return parallelStatus;
// The GC-bracketed budgets are calibrated on Node 24 and are meaningless on
// other allocators. A contributor's suite must stay green on any supported
// engine (package.json permits >=24), so uncalibrated runtimes skip the
// probes with a warning. Set GEV_REQUIRE_ALLOCATION_GATE=1 (pinned CI /
// release batteries) to make an uncalibrated runtime a hard failure.
if (!isCalibratedAllocationRuntime()) {
if (process.env.GEV_REQUIRE_ALLOCATION_GATE === '1') {
assertNode24AllocationRuntime();
}
console.warn(
`[unit] SKIPPED ${ALLOCATION_TEST_FILES.length} allocation microbenchmarks: `
+ `budgets are calibrated for Node 24, running ${process.versions.node}. `
+ 'Run under Node 24 (or set GEV_REQUIRE_ALLOCATION_GATE=1 to fail instead).',
);
return 0;
}
for (const file of plan.serializedAllocations) {
const status = runTests(allocationTestArgs(file));
if (status !== 0) return status;
}
return 0;
}
const invokedPath = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : '';
if (import.meta.url === invokedPath) process.exitCode = runUnitTests();
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env node
// Minimal screenshot sink: the browser POSTs a data:image/... URL (grabbed from
// the real-GPU Cesium canvas) and this writes it to qa-shots/height-datum/<name>.
// Keeps large image payloads OUT of the agent's context — browser → sink → disk.
// Usage: node scripts/shot-sink.mjs (listens on :4399)
import http from 'node:http';
import { writeFileSync, mkdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const OUT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'qa-shots', 'height-datum');
mkdirSync(OUT, { recursive: true });
const PORT = 4399;
const cors = (res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
};
http.createServer((req, res) => {
cors(res);
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
const url = new URL(req.url, 'http://localhost');
if (req.method === 'POST' && url.pathname === '/save') {
const name = (url.searchParams.get('name') || 'shot').replace(/[^a-zA-Z0-9._-]/g, '_');
let body = '';
req.on('data', (c) => { body += c; });
req.on('end', () => {
try {
const m = /^data:image\/(png|jpeg);base64,(.+)$/s.exec(body.trim());
if (!m) { res.writeHead(400); res.end('bad dataurl'); return; }
const ext = m[1] === 'jpeg' ? 'jpg' : 'png';
const file = path.join(OUT, `${name}.${ext}`);
writeFileSync(file, Buffer.from(m[2], 'base64'));
res.writeHead(200); res.end(file);
console.log(`saved ${file} (${Math.round(m[2].length / 1024)} KB b64)`);
} catch (e) { res.writeHead(500); res.end(String(e)); }
});
return;
}
res.writeHead(404); res.end('nope');
}).listen(PORT, () => console.log(`shot-sink on http://localhost:${PORT} → ${OUT}`));
File diff suppressed because it is too large. Load diff