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

+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);
});