/**
* qa-cctv-v2.mjs — CCTV v2 subsystem proof harness
*
* Replaces qa-cctv-b9.mjs + qa-cctv-drift-b9b.mjs, which proved fixes in
* systems v2 deleted outright (chunked auto-cal raycast storms, periodic
* ground re-sync deadband/oscillation). v2 has no per-frame scene queries at
* all, no auto-calibration, and no periodic re-grounding — so this harness
* asserts the NEW invariants instead of re-proving retired ones:
*
* 1. Raycast invariant (design §9.1-amended): scene.pickFromRay fires
* exactly once per camera ACTIVATION (the §9.1 obstruction probe) and
* never again in steady state; shared mesh-floor sampling fires at most
* once per coarse cell during the staggered geometry-load drain, and
* actually COMPLETES once tiles load in photoreal mode (>=1 real sample
* — the ceiling alone would pass trivially at Δ=0 if the snap never
* finished). Globe mode instead proves the rendered mount uses a real
* terrain height with zero mesh samples. Sampling
* stays flat while idle, and stays flat for non-positional pose edits.
* 2. Geometry contract: the 5-polyline frustum wireframe + far-cap plane
* exist for the active camera, the plane's corners coincide with the
* 4 corner-ray endpoints, and the whole geometry is byte-stable over a
* 30s idle window (nothing resamples/rewrites without a reason to).
* 3. Calibration round-trip: a manual patch writes the v2 localStorage key
* with source:'manual', rotates the geometry, flips the panel calBadge
* to 'calibrated'; reset removes the entry and restores base geometry.
* 4. Frame-loop health: the plane's projection runtime starts on a
* placeholder and reaches a "frame considered fresh" state, and the
* panel
+ the plane's underlying fetch share the same
* /api/cctv/frame/ URL family.
* 5. Empty-space ownership: real canvas gestures prove ADJUST and sibling
* picks preserve the active camera, while a true empty-space click
* publishes exactly one null transition without moving the camera or
* emitting a focus request; a repeat click is idempotent.
* 6. Viewshed mode (v3, 2026-07-05): coverage tri-state round-trip +
* boolean compat, color-coded volume primitives exist only in
* 'viewshed' for the visible set, idle-stable by object identity,
* raycast counters flat across cycling.
* 7. Calibration gizmo (v3): 8 handle entities in ADJUST mode, synthetic
* E-arrow drag edits the live pose while freezing mount elevation and
* issuing zero transient samples, without touching the save-gated store;
* camera-controller inputs restore and parts hide on mode off.
*
* SwiftShader caveat (proven in Task 3): under headless software GL, plane
* image materials do not visually FILL — screenshotting for a pixel-based
* pass/fail is not viable here. All four assertion groups therefore check
* entity/counter/localStorage/network STATE (GL-independent), never rendered
* pixels. Screenshots are still captured for human visual review but do not
* gate the exit code.
*
* No `cesium` package import for in-page math: the app does not expose a
* `window.Cesium` global, so every in-page evaluate() either (a) reads plain
* numbers off live Cesium objects (Cartesian3 -> {x,y,z}, Quaternion ->
* {x,y,z,w}) and does its own vector/quaternion math in plain JS, or
* (b) borrows static methods off the *constructor* of a live instance, which
* works because Cesium classes' statics live on the constructor, not a
* namespace object. Separately, this harness DOES import
* `computeFrustumGeometry` directly from `src/data/cctv.js` (a pure,
* side-effect-free export with no Cesium/scene dependency) to use as a
* Node-side ground-truth oracle — see the "safe pose" search below.
*
* Ground-clearance note (2026-09-13): `computeFrustumGeometry` lifts the
* whole far cap RIGIDLY by the largest clearance deficit over a 3×3 grid of
* support points against the ground under each (the mount's ground where no
* footprint measurement exists), so the plane and the wireframe corners
* always coincide and the bottom edge clears the ground it was measured
* against (except where the footprint lift cap, PLANE_FOOTPRINT_LIFT_CAP_M,
* lets a plane accept a tall building under its far edge). Austin's fabricated pose priors (pitch -18/-24°, FOV
* 44/56°, range 145/210 m, mount 8/10 m) therefore render as planes lifted
* tens of metres above their mount. The "safe pose" search below predates
* that change: it still guarantees a pose that clears the ground with margin
* so the coincide check is deterministic whichever camera activates, but it
* is no longer needed to avoid a plane/wireframe disagreement.
*
* Run: node scripts/qa-cctv-v2.mjs --url http://localhost:4173
*
* Exit 0 = all asserts passed. Non-zero = a hard failure.
*/
import puppeteer from 'puppeteer';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
computeFrustumGeometry,
FRUSTUM_GROUND_CLEARANCE_M,
} from '../src/data/cctv.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 CCTV_URL = new URL(getOpt('--url', BASE_URL));
CCTV_URL.searchParams.set('welcome', '0');
const APP_URL = CCTV_URL.href;
const HEADFUL = getFlag('--headful');
const SHOTS_DIR = path.join(REPO_ROOT, 'qa-shots', 'cctv-v2');
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.
await puppeteer.executablePath().catch(() => 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));
// Each page.evaluate is bounded by puppeteer's `protocolTimeout` (default
// 180s). Waits never come close: they poll from Node (below). The headroom is
// for the few long evaluates, such as enabling the full ~3,700-camera catalog
// on a real GPU, and matches the other qa-*.mjs harnesses.
const PROTOCOL_TIMEOUT_MS = 300_000;
/**
* Polls an in-page predicate from Node, one short page.evaluate per tick,
* until it holds or the budget runs out. Used instead of page.waitForFunction
* for two reasons: its default rAF polling stops in a minimised or hidden
* headful window (timers keep running, rAF does not), and its result is one
* CDP call that protocolTimeout cuts off, while the headful drain budget
* reaches ~50 minutes. A predicate that throws fails the run instead of
* silently spending the budget.
* @param {import('puppeteer').Page} page
* @param {Function} predicate - In-page predicate.
* @param {object} opts
* @param {number} opts.timeoutMs - Overall budget.
* @param {number} [opts.intervalMs=250] - Delay between checks.
* @param {unknown[]} [opts.args=[]] - Arguments passed to the predicate.
* The predicate must return a boolean: its result crosses CDP by value.
* @returns {Promise} True if the predicate held within the budget.
*/
async function waitForPredicate(
page,
predicate,
{ timeoutMs, intervalMs = 250, args = [] },
) {
// performance.now() is monotonic and doesn't advance while the machine
// sleeps, so neither a clock change nor a laptop sleep eats the budget.
const deadline = performance.now() + timeoutMs;
for (;;) {
try {
if (await page.evaluate(predicate, ...args)) return true;
} catch (err) {
// A navigation or Vite reload replaces the execution context: poll again.
if (
!/Execution context was destroyed|Cannot find context/.test(
err?.message ?? '',
)
) {
throw err;
}
}
const remaining = deadline - performance.now();
if (remaining <= 0) return false;
await sleep(Math.min(intervalMs, remaining));
}
}
// SwiftShader can spend tens of seconds servicing the hover drillPick emitted
// by page.mouse.move(), before the LEFT_DOWN under test even runs. Position the
// pointer while ADJUST is disabled, then re-enable it and explicitly render a
// few frames so the real press/drag still exercises Cesium's input + pick path.
async function positionPointerForGizmoDrag(page, point, frameCount = 3) {
await page.evaluate(() => {
window.__godsEyeView.dataManager.layers
.get('cctv')
.module.setParams({ calibrationMode: false });
});
await page.mouse.move(point.x, point.y);
return page.evaluate(async (count) => {
const gev = window.__godsEyeView;
gev.dataManager.layers
.get('cctv')
.module.setParams({ calibrationMode: true });
return (await window.__qaRenderFrames(count, 5000)) >= count;
}, frameCount);
}
/**
* Saves a screenshot for human review. These are never a gate, so a capture
* that fails (for example when the renderer stalls in a hidden headful window)
* is logged instead of ending the run.
* @param {import('puppeteer').Page} page
* @param {string} file
* @param {string} label
*/
async function saveReviewShot(page, file, label) {
try {
await page.screenshot({ path: file });
console.log(` screenshot (${label}) → ${path.relative(REPO_ROOT, file)}`);
} catch (err) {
console.warn(
` screenshot skipped (${label}): ${path.relative(REPO_ROOT, file)}: ${err?.message ?? err}`,
);
}
}
/**
* Installs window.__qaRenderFrames(count, capMs) in the page: requests renders
* until `count` frames have passed scene.postRender or capMs runs out, and
* resolves with the number of frames rendered. The cap keeps a stalled
* renderer (headful window asleep or hidden) from hanging the CDP call.
* @param {import('puppeteer').Page} page
*/
function installRenderFramesHelper(page) {
return page.evaluate(() => {
window.__qaRenderFrames = (count, capMs) =>
new Promise((resolve) => {
const scene = window.__godsEyeView.viewer.scene;
let frames = 0;
const finish = () => {
clearTimeout(timer);
remove();
resolve(frames);
};
const remove = scene.postRender.addEventListener(() => {
if (++frames >= count) {
finish();
return;
}
scene.requestRender();
});
const timer = setTimeout(finish, capMs);
scene.requestRender();
});
});
}
// In-page helper (stringified and re-defined inside each evaluate that needs
// it, since functions can't cross the puppeteer boundary): serializes the
// 5 frustum polylines + plane position/orientation/dimensions for a camera
// into plain arrays of numbers, using only the camera's own clock time (no
// Cesium namespace needed — Property.getValue just needs *a* JulianDate,
// and the viewer's own clock.currentTime is a valid live instance of one).
const SERIALIZE_GEOM_SRC = `
function serializeGeom(camId) {
const viewer = window.__godsEyeView.viewer;
const time = viewer.clock.currentTime;
const roles = ['ray-tl', 'ray-tr', 'ray-br', 'ray-bl', 'cap'];
const poly = {};
for (const role of roles) {
const ent = viewer.entities.getById('cctv-' + camId + '-' + role);
poly[role] = ent && ent.polyline
? ent.polyline.positions.getValue(time).map((p) => [p.x, p.y, p.z])
: null;
}
const planeEnt = viewer.entities.getById('cctv-' + camId + '-plane');
let plane = null;
if (planeEnt) {
const pos = planeEnt.position.getValue(time);
const orient = planeEnt.orientation.getValue(time);
const dims = planeEnt.plane.dimensions.getValue(time);
plane = {
position: [pos.x, pos.y, pos.z],
orientation: [orient.x, orient.y, orient.z, orient.w],
dimensions: [dims.x, dims.y],
};
}
return { poly, plane };
}
`;
// In-page helper: bearing (degrees, mount -> cap-center) shift between two
// serializeGeom() snapshots of the SAME camera. Plain-JS spherical bearing
// math over ECEF points (no Cesium namespace) — a local east/north basis is
// built from the mount's geocentric "up" via cross products, which is
// equivalent to a proper ENU bearing at these ranges (hundreds of metres;
// cctv.js's own math already treats this scale as flat-earth, see
// projectPoint). Used both to prove a heading patch rotates the geometry AND
// that reset un-rotates it — comparing BEARING (not absolute cap-center
// position) is deliberate: the far-cap's ALTITUDE can legitimately drift a
// few metres between two ground-clamp actions because scene.sampleHeight
// against a still-streaming 3D tileset is not perfectly deterministic call to
// call (observed while writing this harness — see report), which would give
// a false-negative on an absolute-position epsilon that heading math alone
// doesn't share.
const BEARING_SHIFT_SRC = `
function bearingShiftDeg(before, after) {
function capCenter(g) {
const pts = g.poly.cap; // [tl, tr, br, bl, tl]
const avg = [0, 0, 0];
for (let i = 0; i < 4; i++) { avg[0] += pts[i][0]; avg[1] += pts[i][1]; avg[2] += pts[i][2]; }
return [avg[0] / 4, avg[1] / 4, avg[2] / 4];
}
const mount = before.poly['ray-tl'][0];
const up = mount;
const upLen = Math.hypot(up[0], up[1], up[2]);
const upN = [up[0] / upLen, up[1] / upLen, up[2] / upLen];
const ref = Math.abs(upN[2]) < 0.9 ? [0, 0, 1] : [1, 0, 0];
const eastRaw = [
ref[1] * upN[2] - ref[2] * upN[1],
ref[2] * upN[0] - ref[0] * upN[2],
ref[0] * upN[1] - ref[1] * upN[0],
];
const eastLen = Math.hypot(eastRaw[0], eastRaw[1], eastRaw[2]);
const east = [eastRaw[0] / eastLen, eastRaw[1] / eastLen, eastRaw[2] / eastLen];
const north = [
upN[1] * east[2] - upN[2] * east[1],
upN[2] * east[0] - upN[0] * east[2],
upN[0] * east[1] - upN[1] * east[0],
];
function bearingTo(target) {
const v = [target[0] - mount[0], target[1] - mount[1], target[2] - mount[2]];
const e = v[0] * east[0] + v[1] * east[1] + v[2] * east[2];
const n = v[0] * north[0] + v[1] * north[1] + v[2] * north[2];
return (Math.atan2(e, n) * 180 / Math.PI + 360) % 360;
}
const b0 = bearingTo(capCenter(before));
const b1 = bearingTo(capCenter(after));
let delta = b1 - b0;
delta = ((delta + 540) % 360) - 180; // normalize to [-180, 180]
return delta;
}
`;
/**
* Waits for the scene's 3D tileset to report tilesLoaded===true. Cesium
* flips tilesLoaded back to false whenever a view/pose change causes new
* tiles to be requested (observed directly while writing this harness: it
* can go false again even after an earlier wait already saw it true) — so
* every call site that depends on a REAL ground sample landing (not the
* B9c guard's pure recompute from the cached real ground) must wait fresh
* immediately before the action, not rely on an earlier wait.
* @param {import('puppeteer').Page} page
* @param {number} [timeoutMs=15000]
* @returns {Promise} True if tilesLoaded was observed within the timeout.
*/
function waitForTilesLoaded(page, timeoutMs = 15000) {
return waitForPredicate(
page,
() => {
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; // no tileset in the scene — nothing to wait for
},
{ timeoutMs },
);
}
/**
* Searches for a calibration patch (pitchDeg/fovDeg/rangeScale offsets) that
* clears the ground clamp for a given base pose, using the SAME
* computeFrustumGeometry the app renders with as the oracle. Pushes pitch to
* its allowed maximum, shrinks range to its allowed minimum, then shrinks FOV
* in 2-degree steps until every corner sits comfortably above
* `groundAltM + FRUSTUM_GROUND_CLEARANCE_M` (3m safety margin beyond the
* clamp floor). Returns null if no patch in the legal offset ranges clears it
* (shouldn't happen for the catalog's fabricated poses, but the caller must
* treat that as INCONCLUSIVE, not a hard failure).
* @param {Object} basePose - { pitchDeg, fovDeg, rangeM, mountHeightM }.
* @param {number} groundAltM - Ground altitude at the mount.
* @returns {{ pitchDeg: number, fovDeg: number, rangeScale: number }|null} Calibration patch offsets.
*/
function findSafeCalibrationPatch(basePose, groundAltM) {
const pitchDeg = Math.max(-70, Math.min(10, basePose.pitchDeg + 45));
const pitchOffset = Number((pitchDeg - basePose.pitchDeg).toFixed(1));
const rangeScale = 0.35; // minimum legal rangeScale (normalizeCalibration clamp)
const rangeM = Math.max(120, basePose.rangeM * rangeScale);
const clampFloor = groundAltM + FRUSTUM_GROUND_CLEARANCE_M;
for (let fovOffset = 0; fovOffset >= -50; fovOffset -= 2) {
const fovDeg = Math.max(20, Math.min(130, basePose.fovDeg + fovOffset));
const camera = {
lat: 30,
lon: -97,
headingDeg: 0,
pitchDeg,
fovDeg,
rangeM,
mountHeightM: basePose.mountHeightM,
};
const geom = computeFrustumGeometry(camera, groundAltM, rangeM);
const minAlt = Math.min(
geom.corners.tl.alt,
geom.corners.tr.alt,
geom.corners.br.alt,
geom.corners.bl.alt,
);
if (minAlt > clampFloor + 3) {
return { pitchDeg: pitchOffset, fovDeg: fovOffset, rangeScale };
}
}
return null;
}
async function main() {
console.log(`\nCCTV v2 Subsystem 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 });
const chromeExecutable = findChromeExecutable();
const browser = await puppeteer.launch({
headless: HEADFUL ? false : 'new',
protocolTimeout: PROTOCOL_TIMEOUT_MS,
// Talk CDP over a pipe, so no debugging port stays open on localhost for
// the length of a headful run.
pipe: true,
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
...(HEADFUL ? [] : ['--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 });
const appReady = await waitForPredicate(
page,
() =>
Boolean(
window.__godsEyeView &&
window.__godsEyeView.viewer &&
window.__godsEyeView.dataManager,
),
{ timeoutMs: 60000 },
);
if (!appReady) throw new Error('app did not initialise within 60s');
await installRenderFramesHelper(page);
// Let the initial fly-to Austin and first tiles settle.
await sleep(4000);
// -----------------------------------------------------------------------
// Install raycast counters BEFORE the layer is enabled (§9.1-amended: the
// zero-raycast invariant is steady-state-only; the single activation
// obstruction probe is the one legal pickFromRay). Wrapping happens
// in-page against the live scene, counting ALL calls (shared scene) — the
// harness only ever asserts DELTAS around CCTV-specific actions, so noise
// from unrelated subsystems cancels out by construction.
// -----------------------------------------------------------------------
const wrapInstalled = await page.evaluate(() => {
const scene = window.__godsEyeView?.viewer?.scene;
if (
!scene ||
typeof scene.pickFromRay !== 'function' ||
typeof scene.sampleHeight !== 'function'
) {
return false;
}
window.__qaCounters = { pickFromRay: 0, sampleHeight: 0 };
const origPick = scene.pickFromRay.bind(scene);
scene.pickFromRay = (...args) => {
window.__qaCounters.pickFromRay += 1;
return origPick(...args);
};
const origSample = scene.sampleHeight.bind(scene);
scene.sampleHeight = (...args) => {
window.__qaCounters.sampleHeight += 1;
return origSample(...args);
};
return true;
});
if (!wrapInstalled) {
console.error(
'\x1b[31mCould not wrap scene.pickFromRay/sampleHeight before layer enable — aborting.\x1b[0m',
);
process.exit(2);
}
const readCounters = () =>
page.evaluate(() => ({ ...window.__qaCounters }));
console.log('Enabling CCTV layer...');
const c0 = await readCounters();
await page.evaluate(async () => {
const dm = window.__godsEyeView.dataManager;
const entry = dm.layers.get('cctv');
if (!entry.enabled) await dm.toggle('cctv');
});
// Wait for the staggered geometry-load queue to drain (shared mesh-floor
// sampling: <=1 sampleHeight per coarse cell, gated on tiles-ready). The
// budget scales with catalog size: the queue staggers ~4 records/120ms
// and the one-shot tiles-ready completion pass can re-enqueue the whole
// catalog once — 30s was calibrated for the old 36-camera default and
// times out spuriously at the 250-camera default (2026-07-04).
const camCount = await page.evaluate(
() =>
window.__godsEyeView.dataManager.layers.get('cctv').module.getUIState()
.count,
);
// ~800ms per real ground sample measured under SwiftShader (each
// scene.sampleHeight forces tile loads at the probe point) — real GPU is
// far faster, so this is a headless-CI ceiling, not an app expectation.
const drainBudgetMs = HEADFUL
? Math.max(30000, camCount * 800 + 30000)
: Math.min(120000, Math.max(30000, camCount * 800 + 30000));
console.log(
`Waiting for geometry-load queue to drain (N=${camCount}, budget ${Math.round(drainBudgetMs / 1000)}s)...`,
);
const drained = await waitForPredicate(
page,
() => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
const ui = mod.getUIState();
return Boolean(ui.loading) && ui.loading.active === false;
},
{ timeoutMs: drainBudgetMs, intervalMs: 1000 },
);
record(
`geometry-load queue drains within ${Math.round(drainBudgetMs / 1000)}s (N=${camCount})`,
drained ? true : HEADFUL ? false : null,
drained
? 'loading.active === false'
: HEADFUL
? 'timed out waiting for drain on the real-GPU sign-off path'
: 'headless GL geometry sampling did not settle; current headful evidence is required',
);
const cAfterDrain = await readCounters();
record(
'pickFromRay stays 0 before any activation',
cAfterDrain.pickFromRay - c0.pickFromRay === 0,
`Δ=${cAfterDrain.pickFromRay - c0.pickFromRay}`,
);
// Every camera maps to at most one shared coarse cell, so ≤1×N remains a
// safe ceiling while nearby cameras reuse the same accepted floor.
const sampleAfterDrain = cAfterDrain.sampleHeight - c0.sampleHeight;
record(
`sampleHeight <= 1×N after drain (N=${camCount})`,
sampleAfterDrain <= camCount,
`Δ=${sampleAfterDrain}, ceiling=${camCount} (one-shot shared mesh-floor cell)`,
);
// Pick the active camera (or nearest) to activate.
console.log('Activating a camera (focusNearest)...');
const activeIdBeforeActivation = await page.evaluate(
() =>
window.__godsEyeView.dataManager.layers.get('cctv').module.getUIState()
.activeCameraId,
);
const cBeforeActivate = await readCounters();
await page.evaluate(() => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.focusNearest({ durationSec: 0.1 });
});
await sleep(500); // let the activation's synchronous work (probe + geometry rewrite) land
const cAfterActivate = await readCounters();
const activeId = await page.evaluate(
() =>
window.__godsEyeView.dataManager.layers.get('cctv').module.getUIState()
.activeCameraId,
);
const pickDeltaActivation =
cAfterActivate.pickFromRay - cBeforeActivate.pickFromRay;
record(
'pickFromRay fires exactly once for the activation (§9.1 probe)',
pickDeltaActivation === 1,
`Δ=${pickDeltaActivation} (camera=${activeId}, was=${activeIdBeforeActivation})`,
);
// Re-selecting the ALREADY-ACTIVE camera is a no-op (owner field test
// 2026-07-04: every click on the monitor plane picks its own camera, and
// re-running activation rewrote the plane entity → visible flash). No new
// probe, no geometry rewrite.
const geomBeforeReselect = await page.evaluate(`(() => {
${SERIALIZE_GEOM_SRC}
return JSON.stringify(serializeGeom(${JSON.stringify(activeId)}));
})()`);
await page.evaluate((id) => {
window.__godsEyeView.dataManager.layers
.get('cctv')
.module.setParams({ selectedCameraId: id });
}, activeId);
await sleep(400);
const cAfterReselect = await readCounters();
const geomAfterReselect = await page.evaluate(`(() => {
${SERIALIZE_GEOM_SRC}
return JSON.stringify(serializeGeom(${JSON.stringify(activeId)}));
})()`);
record(
're-selecting the active camera is a no-op (no probe, no geometry rewrite)',
cAfterReselect.pickFromRay - cAfterActivate.pickFromRay === 0 &&
geomBeforeReselect === geomAfterReselect,
`probeΔ=${cAfterReselect.pickFromRay - cAfterActivate.pickFromRay}, geometryChanged=${geomBeforeReselect !== geomAfterReselect}`,
);
// -----------------------------------------------------------------------
// Completion FLOOR (locks update()'s one-shot tiles-ready re-enqueue):
// the "<=1×N" ceiling above passes trivially at Δ=0 — e.g. if every drain
// pass ran before tiles loaded (fallback height, record left unresolved)
// and nothing ever completed the snap. Once tilesLoaded has been observed
// true, at least ONE real scene.sampleHeight must have fired since enable
// (existence, not just ceiling). The wait below tolerates the update-tick
// latency of the completion pass (update interval is 10s).
// -----------------------------------------------------------------------
const globeMode = await page.evaluate(
() => window.__godsEyeView.viewer.scene.globe?.show !== false,
);
if (globeMode) {
// Globe stacks intentionally perform no scene.sampleHeight query.
// Require independently fetched DEM heights and the actual rendered
// mount, so a catalog/geoid fallback cannot turn zero samples into a pass.
console.log(
'Checking terrain-globe completion against live DEM heights...',
);
const terrain = await page.evaluate(async () => {
const camera = window.__godsEyeView.dataManager.layers
.get('cctv')
.module.getUIState().activeCamera;
const exact = `${camera.lon.toFixed(5)},${camera.lat.toFixed(5)}`;
const cell = `${camera.lon.toFixed(3)},${camera.lat.toFixed(3)}`;
const response = await fetch(
`/api/terrain/heights?points=${encodeURIComponent(`${exact};${cell}`)}`,
{ signal: AbortSignal.timeout(30000) },
);
const body = await response.json();
return { ok: response.ok, results: body.results };
});
const heights = terrain.results?.map((result) => result?.ellipsoid);
const terrainReady =
terrain.ok && heights?.length === 2 && heights.every(Number.isFinite);
const mountResolved =
terrainReady &&
(await waitForPredicate(
page,
(expected, base) => {
const { viewer, dataManager } = window.__godsEyeView;
const camera = dataManager.layers
.get('cctv')
.module.getUIState().activeCamera;
const ray = viewer.entities.getById(`cctv-${camera.id}-ray-tl`);
const mount = ray?.polyline?.positions.getValue(
viewer.clock.currentTime,
)?.[0];
const height =
mount &&
viewer.scene.globe.ellipsoid.cartesianToCartographic(mount)
.height;
return (
Number.isFinite(camera.groundPriorM) &&
Math.abs(camera.groundPriorM - expected[0]) < 0.01 &&
Number.isFinite(height) &&
expected.some(
(ground) =>
Math.abs(height - ground - camera.mountHeightM) < 0.05,
) &&
window.__qaCounters.sampleHeight === base
);
},
{ timeoutMs: 30000, args: [heights, c0.sampleHeight] },
));
record(
'terrain-globe completion uses a real DEM height at the rendered mount without mesh samples',
Boolean(mountResolved),
`terrain=${JSON.stringify(heights)}, mesh sample Δ=${(await readCounters()).sampleHeight - c0.sampleHeight}`,
);
} else {
console.log('Waiting for tiles + the one-shot completion pass...');
const tilesSeenLoaded = await waitForTilesLoaded(page, 45000);
let sampleFloorOk = false;
if (tilesSeenLoaded) {
sampleFloorOk = await waitForPredicate(
page,
(base) => window.__qaCounters.sampleHeight - base >= 1,
{ timeoutMs: 30000, args: [c0.sampleHeight] },
);
}
const cFloor = await readCounters();
record(
'>=1 real ground sample once tilesLoaded observed (one-shot snap completed)',
tilesSeenLoaded ? sampleFloorOk : null,
tilesSeenLoaded
? `total real samples since enable=${cFloor.sampleHeight - c0.sampleHeight}`
: 'tilesLoaded never observed within 45s — floor not assessable this run',
);
}
// Let the completion pass finish before the steady-state window: require
// the sampleHeight counter quiet for 12s (> the 10s update interval, so
// the one-shot latch has provably had a tick in which to fire and drain)
// — otherwise the idle assertion below could observe the legitimate tail
// of the one-shot pass and misread it as steady-state sampling. The cap
// scales with N like the drain budget: when the ENABLE-time drain ran
// before tiles loaded (fallback path, zero real samples), the completion
// pass re-enqueues the WHOLE catalog and each real sampleHeight costs
// ~1s under SwiftShader — at N=250 that tail is ~4 minutes, and the old
// fixed 60s cap expired mid-pass, bleeding legitimate one-shot samples
// into the idle window (observed 2026-07-05: idle Δ=12, pose-edit Δ=5).
let completionPassQuiet = false;
{
const settleBudgetMs = Math.max(60000, camCount * 1200 + 30000);
console.log(
`Waiting for the one-shot completion pass to go quiet (budget ${Math.round(settleBudgetMs / 1000)}s)...`,
);
let last = (await readCounters()).sampleHeight;
let quietMs = 0;
const settleStart = Date.now();
while (quietMs < 12000 && Date.now() - settleStart < settleBudgetMs) {
await sleep(1000);
const cur = (await readCounters()).sampleHeight;
if (cur === last) {
quietMs += 1000;
} else {
quietMs = 0;
last = cur;
}
}
completionPassQuiet = quietMs >= 12000;
}
// -----------------------------------------------------------------------
// Steady-state idle window (>=15s): pickFromRay must not grow at all;
// sampleHeight must stay flat (queue already drained, no periodic re-sync
// in v2).
// -----------------------------------------------------------------------
console.log('Idling 15s to check steady-state raycast invariant...');
const cIdleStart = await readCounters();
await sleep(15000);
const cIdleEnd = await readCounters();
const pickDuringIdle = cIdleEnd.pickFromRay - cIdleStart.pickFromRay;
const sampleDuringIdle = cIdleEnd.sampleHeight - cIdleStart.sampleHeight;
record(
'pickFromRay does not grow during 15s steady-state idle',
pickDuringIdle === 0,
`Δ=${pickDuringIdle}`,
);
record(
'sampleHeight stays flat during 15s steady-state idle',
completionPassQuiet ? sampleDuringIdle === 0 : null,
completionPassQuiet
? `Δ=${sampleDuringIdle}`
: `one-shot floor completion did not become quiet under this GL stack; observed tail Δ=${sampleDuringIdle}`,
);
// -----------------------------------------------------------------------
// A rotational calibration edit preserves the existing shared floor and
// therefore adds zero sampleHeight calls. The activation obstruction probe
// remains activation-only.
console.log('Applying a calibration patch (heading +30°)...');
const cBeforeCal = await readCounters();
await page.evaluate((camId) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({
calibration: { cameraId: camId, patch: { headingDeg: 30 } },
});
}, activeId);
await sleep(500);
const cAfterCal = await readCounters();
const sampleDeltaCal = cAfterCal.sampleHeight - cBeforeCal.sampleHeight;
const pickDeltaCal = cAfterCal.pickFromRay - cBeforeCal.pickFromRay;
record(
'sampleHeight stays flat after a heading-only calibration edit',
completionPassQuiet ? sampleDeltaCal === 0 : null,
completionPassQuiet
? `Δ=${sampleDeltaCal}`
: `one-shot floor completion was still unresolved; observed tail Δ=${sampleDeltaCal}`,
);
record(
'pickFromRay does not fire on a calibration patch (activation-only)',
pickDeltaCal === 0,
`Δ=${pickDeltaCal}`,
);
// Undo that patch before the geometry-contract assertions below so they
// observe the record's steady, unpatched frustum.
await page.evaluate((camId) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({ calibration: { cameraId: camId, reset: true } });
}, activeId);
await sleep(300);
// =========================================================================
// Group 2: geometry contract
// =========================================================================
console.log('Checking geometry contract (frustum + plane)...');
const geomInfo = await page.evaluate(`(function(camId){
${SERIALIZE_GEOM_SRC}
const g = serializeGeom(camId);
const viewer = window.__godsEyeView.viewer;
// Overlay unification: the plane label is no longer a native entity —
// it publishes as a protected host entry under the cctv-projection
// source. Assert the migrated surface via the host diagnostics.
const diag = window.__gevWorldOverlay?.getDiagnostics?.();
g.hasLabel = (diag?.entriesBySource?.['cctv-projection'] || 0) >= 1;
const planeEnt = viewer.entities.getById('cctv-' + camId + '-plane');
g.planeShow = planeEnt ? planeEnt.show : null;
return g;
})(${JSON.stringify(activeId)})`);
const allPolylinesPresent = [
'ray-tl',
'ray-tr',
'ray-br',
'ray-bl',
'cap',
].every((r) => Array.isArray(geomInfo.poly[r]));
record(
'all 5 frustum polyline entities exist for the active camera',
allPolylinesPresent,
Object.keys(geomInfo.poly)
.map((k) => `${k}=${geomInfo.poly[k] ? 'ok' : 'MISSING'}`)
.join(', '),
);
record(
'plane + plane-label entities exist for the active camera',
!!geomInfo.plane && geomInfo.hasLabel,
`plane=${!!geomInfo.plane} label=${geomInfo.hasLabel} show=${geomInfo.planeShow}`,
);
// Plane corner positions must coincide with the 4 corner-ray endpoints
// (ε < 0.5m) — but ONLY when the frustum doesn't hit the ground clamp
// (see the file-header "Ground-clamp caveat"). Austin's fabricated pitch
// priors clamp routinely at the base pose, so apply a temporary
// calibration patch (computed against the same computeFrustumGeometry
// oracle the app uses) that pushes pitch up + shrinks FOV/range enough to
// clear the clamp with margin, run the coincidence check against THAT
// pose, then reset back to the base pose before the byte-stability check.
const activeCameraForPatch = await page.evaluate((camId) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
return mod.getUIState().cameras.find((c) => c.id === camId);
}, activeId);
const basePose = activeCameraForPatch.basePose;
const groundAltMForPatch =
activeCameraForPatch.elevationM - activeCameraForPatch.mountHeightM;
const safePatch = basePose
? findSafeCalibrationPatch(basePose, groundAltMForPatch)
: null;
record(
'found a calibration patch that clears the ground clamp (test setup, not an app assertion)',
safePatch !== null,
safePatch
? JSON.stringify(safePatch)
: 'no legal offset combination cleared the clamp — corner-coincidence check below will run against the (possibly clamped) base pose',
);
// updateRecordGeometry's B9c guard now recomputes PURELY from the cached
// real ground when tilesLoaded flips false at the instant of the patch
// (final-review fix — previously it skipped the rewrite entirely, leaving
// the pre-patch polylines/plane dimensions stale), so a pose patch always
// lands in the entity geometry. Cesium can still re-flip tilesLoaded
// false on a pose change even after an earlier wait already saw it true
// (a shrunk FOV/pitch change requests a different tile set), so the
// wait-then-patch retry loop is kept as belt-and-braces, verifying the
// plane's dimensions actually changed before trusting geomForCorners.
let geomForCorners = geomInfo;
let patchGeometryApplied = !safePatch;
if (safePatch) {
const dimsBefore = geomInfo.plane
? JSON.stringify(geomInfo.plane.dimensions)
: null;
for (let attempt = 0; attempt < 3 && !patchGeometryApplied; attempt++) {
await waitForTilesLoaded(page, 15000);
await page.evaluate(
({ camId, patch }) => {
const mod =
window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({ calibration: { cameraId: camId, patch } });
},
{ camId: activeId, patch: safePatch },
);
await sleep(400);
geomForCorners = await page.evaluate(
`(function(camId){ ${SERIALIZE_GEOM_SRC} return serializeGeom(camId); })(${JSON.stringify(activeId)})`,
);
const dimsAfter = geomForCorners.plane
? JSON.stringify(geomForCorners.plane.dimensions)
: null;
patchGeometryApplied = !!dimsAfter && dimsAfter !== dimsBefore;
}
}
record(
'calibration patch geometry landed (test setup, not an app assertion)',
patchGeometryApplied,
patchGeometryApplied
? 'plane dimensions changed vs pre-patch'
: 'plane dimensions never changed — tiles kept flipping mid-stream; corner-coincidence check below may be running against a stale pose',
);
let cornerEps = null;
if (allPolylinesPresent && geomForCorners.plane) {
cornerEps = await page.evaluate((geom) => {
// Rotate vector v by quaternion q=[x,y,z,w] (standard formula).
function rotate(q, v) {
const [qx, qy, qz, qw] = q;
const ix = qw * v[0] + qy * v[2] - qz * v[1];
const iy = qw * v[1] + qz * v[0] - qx * v[2];
const iz = qw * v[2] + qx * v[1] - qy * v[0];
const iw = -qx * v[0] - qy * v[1] - qz * v[2];
return [
ix * qw + iw * -qx + iy * -qz - iz * -qy,
iy * qw + iw * -qy + iz * -qx - ix * -qz,
iz * qw + iw * -qz + ix * -qy - iy * -qx,
];
}
const dist = (a, b) =>
Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
const { poly, plane } = geom;
const center = plane.position;
const halfW = plane.dimensions[0] / 2;
const halfH = plane.dimensions[1] / 2;
// planeOrientationFor builds the frame as columns [right, up, normal]
// — local +X is right, +Y is up (matches Cesium's row/col Matrix3
// constructor call order in cctv.js's planeOrientationFor).
const corner = (sx, sy) => {
const local = [sx * halfW, sy * halfH, 0];
const world = rotate(plane.orientation, local);
return [
center[0] + world[0],
center[1] + world[1],
center[2] + world[2],
];
};
const planeCorners = {
tl: corner(-1, 1),
tr: corner(1, 1),
br: corner(1, -1),
bl: corner(-1, -1),
};
const rayEndpoint = (role) => poly[role][poly[role].length - 1];
const rayCorners = {
tl: rayEndpoint('ray-tl'),
tr: rayEndpoint('ray-tr'),
br: rayEndpoint('ray-br'),
bl: rayEndpoint('ray-bl'),
};
return {
tl: dist(planeCorners.tl, rayCorners.tl),
tr: dist(planeCorners.tr, rayCorners.tr),
br: dist(planeCorners.br, rayCorners.br),
bl: dist(planeCorners.bl, rayCorners.bl),
};
}, geomForCorners);
const maxEps = Math.max(...Object.values(cornerEps));
const epsLabel = safePatch
? 'plane corners coincide with wireframe corner-ray endpoints (ε < 0.5m, ground-clamp-cleared pose)'
: 'plane corners coincide with wireframe corner-ray endpoints (ε < 0.5m)';
record(
epsLabel,
maxEps < 0.5,
`tl=${cornerEps.tl.toFixed(3)}m tr=${cornerEps.tr.toFixed(3)}m br=${cornerEps.br.toFixed(3)}m bl=${cornerEps.bl.toFixed(3)}m`,
);
} else {
record(
'plane corners coincide with wireframe corner-ray endpoints (ε < 0.5m)',
null,
'skipped — geometry entities missing',
);
}
// Restore the base (unpatched) pose before the byte-stability window so
// that check observes the record's normal steady-state frustum, not the
// synthetic clamp-clearing test pose.
if (safePatch) {
await page.evaluate((camId) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({ calibration: { cameraId: camId, reset: true } });
}, activeId);
await sleep(300);
}
// Byte-stable over 30s idle: serialize + compare after 30s. Nothing
// resamples on a timer in v2, so this is a pure drift regression.
console.log('Checking geometry byte-stability over 30s idle...');
const serializeGeom = (camId) =>
page.evaluate(`(function(camId){
${SERIALIZE_GEOM_SRC}
return serializeGeom(camId);
})(${JSON.stringify(camId)})`);
const geomBefore30s = await serializeGeom(activeId);
await sleep(30000);
const geomAfter30s = await serializeGeom(activeId);
const geomStable =
JSON.stringify(geomBefore30s) === JSON.stringify(geomAfter30s);
record(
'geometry is byte-stable over 30s idle (no per-frame/timer resample)',
geomStable,
geomStable
? 'identical serialization'
: 'serialization CHANGED — see harness output above',
);
if (!geomStable) {
console.log(' before:', JSON.stringify(geomBefore30s));
console.log(' after :', JSON.stringify(geomAfter30s));
}
// =========================================================================
// Group 3: calibration round-trip
// =========================================================================
console.log('Checking calibration round-trip (patch -> save -> reset)...');
const baseGeom = await serializeGeom(activeId);
const baseBadge = await page.evaluate((camId) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
const ui = mod.getUIState();
return ui.cameras.find((c) => c.id === camId)?.calBadge;
}, activeId);
const baseStoreEmpty = await page.evaluate((camId) => {
const raw = localStorage.getItem('godsEyeView.cctv.calibration.v2');
const map = raw ? JSON.parse(raw) : {};
return !(camId in map);
}, activeId);
record(
'CAL badge is raw-prior (or curated) before any manual save',
baseBadge !== 'calibrated',
`calBadge=${baseBadge}`,
);
record(
'v2 store has no entry for this camera before any manual save',
baseStoreEmpty,
baseStoreEmpty ? 'no entry' : 'unexpected pre-existing entry',
);
await page.evaluate((camId) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({
calibration: { cameraId: camId, patch: { headingDeg: 30 } },
});
}, activeId);
await sleep(300);
// Save-gated persistence (v3 design §3e): a PATCH edits the live pose
// only — the store must stay untouched and the camera must read as
// dirty/EDITED until the explicit save action below.
const afterPatch = await page.evaluate((camId) => {
const raw = localStorage.getItem('godsEyeView.cctv.calibration.v2');
const map = raw ? JSON.parse(raw) : {};
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
const cam = mod.getUIState().cameras.find((c) => c.id === camId);
return {
stored: camId in map,
calDirty: cam?.calDirty,
calBadge: cam?.calBadge,
};
}, activeId);
record(
'patch does NOT write the v2 store (save-gated)',
afterPatch.stored === false,
afterPatch.stored
? 'entry written on patch — save-gating broken'
: 'store untouched',
);
record(
'patch marks the camera calDirty (EDITED chip)',
afterPatch.calDirty === true,
`calDirty=${afterPatch.calDirty}`,
);
record(
'calBadge stays non-calibrated after an unsaved patch',
afterPatch.calBadge !== 'calibrated',
`calBadge=${afterPatch.calBadge}`,
);
await page.evaluate((camId) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({ calibration: { cameraId: camId, save: true } });
}, activeId);
await sleep(200);
const storeEntry = await page.evaluate((camId) => {
const raw = localStorage.getItem('godsEyeView.cctv.calibration.v2');
const map = raw ? JSON.parse(raw) : {};
return map[camId] || null;
}, activeId);
record(
'v2 localStorage key written with source:manual + savedAt on SAVE',
!!(
storeEntry &&
storeEntry.source === 'manual' &&
Number.isFinite(storeEntry.savedAt)
),
storeEntry
? `source=${storeEntry.source} savedAt=${storeEntry.savedAt}`
: 'no entry written',
);
const afterSave = await page.evaluate((camId) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
const cam = mod.getUIState().cameras.find((c) => c.id === camId);
return { calBadge: cam?.calBadge, calDirty: cam?.calDirty };
}, activeId);
record(
'calBadge flips to calibrated after SAVE (and dirty clears)',
afterSave.calBadge === 'calibrated' && afterSave.calDirty === false,
`calBadge=${afterSave.calBadge} calDirty=${afterSave.calDirty}`,
);
// Geometry rotated: cap-center bearing from the mount should have shifted
// ~30 degrees vs the pre-patch baseline (shared BEARING_SHIFT_SRC helper
// — see its header comment for why bearing, not absolute position).
const geomAfterCal = await serializeGeom(activeId);
const bearingShiftDegPatch = await page.evaluate(`(function(before, after){
${BEARING_SHIFT_SRC}
return bearingShiftDeg(before, after);
})(${JSON.stringify(baseGeom)}, ${JSON.stringify(geomAfterCal)})`);
const rotatedByAbout30 = Math.abs(Math.abs(bearingShiftDegPatch) - 30) < 5;
record(
'geometry cap-center bearing shifts ~30° after headingDeg+30 patch',
rotatedByAbout30,
`Δbearing=${bearingShiftDegPatch.toFixed(2)}°`,
);
// Reset: entry removed, base geometry restored, badge back to raw-prior.
await page.evaluate((camId) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({ calibration: { cameraId: camId, reset: true } });
}, activeId);
await sleep(300);
const storeAfterReset = await page.evaluate((camId) => {
const raw = localStorage.getItem('godsEyeView.cctv.calibration.v2');
const map = raw ? JSON.parse(raw) : {};
return camId in map;
}, activeId);
record(
'v2 localStorage entry removed after reset',
!storeAfterReset,
storeAfterReset ? 'entry still present' : 'removed',
);
const badgeAfterReset = await page.evaluate((camId) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
return mod.getUIState().cameras.find((c) => c.id === camId)?.calBadge;
}, activeId);
record(
'calBadge returns to raw-prior (or curated) after reset',
badgeAfterReset !== 'calibrated',
`calBadge=${badgeAfterReset}`,
);
// Reset must undo the heading+30 rotation back to ~0°. Ground now resolves
// through the stable shared floor cache instead of repeated raw samples.
const geomAfterReset = await serializeGeom(activeId);
const bearingShiftDegReset = await page.evaluate(`(function(before, after){
${BEARING_SHIFT_SRC}
return bearingShiftDeg(before, after);
})(${JSON.stringify(baseGeom)}, ${JSON.stringify(geomAfterReset)})`);
record(
'cap-center bearing restored to base (~0° vs baseline) after reset',
Math.abs(bearingShiftDegReset) < 5,
`Δbearing=${bearingShiftDegReset.toFixed(2)}°`,
);
// =========================================================================
// Group 4: frame-loop health (STATE, not pixels — SwiftShader caveat)
// =========================================================================
console.log(
'Checking frame-loop health (placeholder -> frame swap, URL family)...',
);
// The plane's material.image is always defined (placeholder canvas from
// the moment the runtime is created, real frame content painted onto the
// same canvas/video reference once the fetch resolves) — assert it's
// never null/undefined once the runtime exists, i.e. there's no gap where
// the plane shows nothing.
const materialInfo = await page.evaluate((camId) => {
const viewer = window.__godsEyeView.viewer;
const time = viewer.clock.currentTime;
const planeEnt = viewer.entities.getById('cctv-' + camId + '-plane');
const mat = planeEnt?.plane?.material;
const image = mat?.image;
const resolved =
image && typeof image.getValue === 'function'
? image.getValue(time)
: image;
return {
hasMaterial: !!mat,
hasImage: !!resolved,
tagName: resolved?.tagName || null,
show: planeEnt?.show,
};
}, activeId);
record(
'plane material + image are defined for the active camera (no blank gap)',
materialInfo.hasMaterial && materialInfo.hasImage,
`material=${materialInfo.hasMaterial} image=${materialInfo.hasImage} tag=${materialInfo.tagName} show=${materialInfo.show}`,
);
// Poll /api/cctv/frame/ directly (the same endpoint frameUrlFor
// targets) to confirm the backend serves SOMETHING for this camera
// (upstream / streetview / synthetic fallback all count as "the frame
// loop is healthy" — a hard 5xx/network failure would not).
const frameFetch = await page.evaluate(async (camId) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
const cam = mod.getUIState().cameras.find((c) => c.id === camId);
const res = await fetch(cam.frameUrl);
return {
ok: res.ok,
status: res.status,
contentType: res.headers.get('content-type'),
url: cam.frameUrl,
mediaUrl: cam.mediaUrl,
};
}, activeId);
record(
'backend serves a frame for the active camera (2xx, image content-type)',
frameFetch.ok && /^image\//.test(frameFetch.contentType || ''),
`status=${frameFetch.status} content-type=${frameFetch.contentType} url=${frameFetch.url}`,
);
// Panel
src and the plane's frame fetch share the same
// /api/cctv/frame/ URL family (same endpoint + camera id segment).
const panelImgSrc = await page.evaluate(
() => document.getElementById('cctv-frame')?.getAttribute('src') || null,
);
const sameFamily =
!!panelImgSrc &&
panelImgSrc.includes(`/api/cctv/frame/${encodeURIComponent(activeId)}`);
record(
'panel
and plane share the same /api/cctv/frame/ URL family',
sameFamily,
`panelSrc=${panelImgSrc}`,
);
// =========================================================================
// Group 5: installed canvas-click ownership + true-empty deselection
// =========================================================================
console.log(
'Checking installed canvas click ownership and empty-space deselection...',
);
const emptyClickPoint = await page.evaluate(async () => {
const gev = window.__godsEyeView;
const viewer = gev.viewer;
const scene = viewer.scene;
const canvas = scene.canvas;
const rect = canvas.getBoundingClientRect();
const { hitTestWorldOverlay } =
await import('/src/overlays/worldOverlay.js');
const candidates = [
[0.55, 0.72],
[0.45, 0.72],
[0.62, 0.62],
[0.38, 0.62],
[0.58, 0.22],
[0.42, 0.22],
[0.5, 0.52],
];
const resolveId = (picked) => {
const direct = picked?.id?.id ?? picked?.id;
if (direct !== undefined && direct !== null) return String(direct);
const primitive = picked?.primitive?.id?.id ?? picked?.primitive?.id;
return primitive === undefined || primitive === null
? null
: String(primitive);
};
for (const [fx, fy] of candidates) {
const x = Math.round(canvas.clientWidth * fx);
const y = Math.round(canvas.clientHeight * fy);
const picked = scene.pick({ x, y });
const card = hitTestWorldOverlay(x, y, { sourceId: 'cctv' });
const top = document.elementFromPoint(rect.left + x, rect.top + y);
if (
resolveId(picked) === null &&
!card &&
(top === canvas || canvas.contains(top))
) {
return { x: rect.left + x, y: rect.top + y, canvasX: x, canvasY: y };
}
}
return null;
});
record(
'true-empty canvas target is available (no scene owner or CCTV card)',
!!emptyClickPoint,
emptyClickPoint
? `canvas=(${emptyClickPoint.canvasX},${emptyClickPoint.canvasY})`
: 'no clean canvas point found',
);
const clickEvidenceSetup = await page.evaluate(() => {
const gev = window.__godsEyeView;
const viewer = gev.viewer;
const mod = gev.dataManager.layers.get('cctv').module;
const snapshotPose = () => ({
position: [
viewer.camera.positionWC.x,
viewer.camera.positionWC.y,
viewer.camera.positionWC.z,
],
direction: [
viewer.camera.directionWC.x,
viewer.camera.directionWC.y,
viewer.camera.directionWC.z,
],
up: [viewer.camera.upWC.x, viewer.camera.upWC.y, viewer.camera.upWC.z],
right: [
viewer.camera.rightWC.x,
viewer.camera.rightWC.y,
viewer.camera.rightWC.z,
],
transform: Array.from(
{ length: 16 },
(_, i) => viewer.camera.transform[i],
),
heading: viewer.camera.heading,
pitch: viewer.camera.pitch,
roll: viewer.camera.roll,
trackedId: viewer.trackedEntity?.id ?? null,
});
const cameraPoseMatches = (a, b, epsilon = 1e-5) => {
const vectorKeys = [
'position',
'direction',
'up',
'right',
'transform',
];
const scalarKeys = ['heading', 'pitch', 'roll'];
return (
vectorKeys.every(
(key) =>
Array.isArray(a?.[key]) &&
Array.isArray(b?.[key]) &&
a[key].length === b[key].length &&
a[key].every(
(value, index) => Math.abs(value - b[key][index]) <= epsilon,
),
) &&
scalarKeys.every((key) => Math.abs(a?.[key] - b?.[key]) <= epsilon) &&
a?.trackedId === b?.trackedId
);
};
window.__qaCctvClickEvidence?.dispose?.();
const evidence = {
activeId: mod.getUIState().activeCameraId,
publications: [],
activeTransitions: [],
focusEvents: 0,
pose: snapshotPose(),
snapshotPose,
cameraPoseMatches,
};
evidence.lastActiveId = evidence.activeId;
evidence.unsubscribe = mod.subscribe((state) => {
evidence.publications.push(state.activeCameraId);
if (state.activeCameraId !== evidence.lastActiveId) {
evidence.activeTransitions.push([
evidence.lastActiveId,
state.activeCameraId,
]);
evidence.lastActiveId = state.activeCameraId;
}
});
evidence.baselineTransitions = evidence.activeTransitions.length;
evidence.onFocus = () => {
evidence.focusEvents += 1;
};
window.addEventListener('gev:cctv-request-focus', evidence.onFocus);
evidence.dispose = () => {
evidence.unsubscribe?.();
window.removeEventListener('gev:cctv-request-focus', evidence.onFocus);
};
window.__qaCctvClickEvidence = evidence;
return {
activeId: evidence.activeId,
baselineTransitions: evidence.baselineTransitions,
};
});
if (emptyClickPoint) {
await page.evaluate(() => {
window.__godsEyeView.dataManager.layers
.get('cctv')
.module.setParams({ calibrationMode: true });
});
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
await sleep(250);
const adjustClick = await page.evaluate(() => {
const evidence = window.__qaCctvClickEvidence;
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({ calibrationMode: false });
evidence.siblingBaselineTransitions = evidence.activeTransitions.length;
return {
activeId: mod.getUIState().activeCameraId,
publications: evidence.publications.length,
focusEvents: evidence.focusEvents,
poseSame: evidence.cameraPoseMatches(
evidence.snapshotPose(),
evidence.pose,
),
};
});
record(
'ADJUST-mode true-empty canvas click preserves active camera',
adjustClick.activeId === clickEvidenceSetup.activeId &&
adjustClick.focusEvents === 0 &&
adjustClick.poseSame,
`active=${adjustClick.activeId} focus=${adjustClick.focusEvents} poseSame=${adjustClick.poseSame}`,
);
} else {
record(
'ADJUST-mode true-empty canvas click preserves active camera',
false,
'no true-empty target',
);
}
const siblingTarget = await page.evaluate(async (emptyPoint) => {
if (!emptyPoint) return null;
const gev = window.__godsEyeView;
const viewer = gev.viewer;
const scene = viewer.scene;
const Cesium = await import('/node_modules/cesium/Build/Cesium/index.js');
const canvasPoint = new Cesium.Cartesian2(
emptyPoint.canvasX,
emptyPoint.canvasY,
);
const ray = viewer.camera.getPickRay(canvasPoint);
if (!ray) return null;
let position = null;
let projected = null;
for (const distance of [1_000, 10_000, 100_000, 1_000_000]) {
const candidate = Cesium.Ray.getPoint(
ray,
distance,
new Cesium.Cartesian3(),
);
const screen = Cesium.SceneTransforms.worldToWindowCoordinates(
scene,
candidate,
);
if (
screen &&
Math.hypot(screen.x - canvasPoint.x, screen.y - canvasPoint.y) < 2
) {
position = candidate;
projected = screen;
break;
}
}
if (!position) return null;
const owner = viewer.entities.add({
id: 'qa-cctv-sibling-owner',
position,
point: {
pixelSize: 64,
color: Cesium.Color.MAGENTA,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
window.__qaCctvSiblingOwner = owner;
const framesRendered = await window.__qaRenderFrames(3, 10000);
const rect = scene.canvas.getBoundingClientRect();
const picked = scene.pick(canvasPoint);
const pickedId = picked?.id?.id ?? picked?.id ?? null;
window.__qaCctvClickEvidence.siblingBaselineTransitions =
window.__qaCctvClickEvidence.activeTransitions.length;
return {
x: rect.left + emptyPoint.canvasX,
y: rect.top + emptyPoint.canvasY,
pickedId:
typeof pickedId === 'string' ? pickedId : (pickedId?.id ?? null),
ownerId: owner.id,
ownsExactObject: picked?.id === owner,
framesRendered,
projectionDelta: Math.hypot(
projected.x - canvasPoint.x,
projected.y - canvasPoint.y,
),
};
}, emptyClickPoint);
const siblingOwnsPick =
siblingTarget?.pickedId === 'qa-cctv-sibling-owner' &&
siblingTarget.ownsExactObject;
// No frame at all within the 10s cap points at a stalled renderer rather
// than at picking, though the harness can't rule out both. Fail it on the
// real-GPU sign-off path; leave it inconclusive under headless GL. A slow
// SwiftShader frame (1-2 of 3) still counts as a real miss.
const siblingRendererStalled =
siblingTarget && !siblingOwnsPick && siblingTarget.framesRendered === 0;
record(
'sibling test object owns its canvas pick',
!siblingTarget
? null
: siblingOwnsPick ||
(siblingRendererStalled && !HEADFUL ? null : false),
siblingTarget
? `${siblingRendererStalled ? 'renderer stalled: ' : ''}frames=${siblingTarget.framesRendered}/3 picked=${siblingTarget.pickedId} exactOwner=${siblingTarget.ownsExactObject} projectionΔ=${siblingTarget.projectionDelta}`
: 'no previously verified empty canvas ray was available',
);
if (siblingOwnsPick) {
await page.mouse.click(siblingTarget.x, siblingTarget.y);
await sleep(250);
}
const siblingClick = await page.evaluate(() => {
const evidence = window.__qaCctvClickEvidence;
const gev = window.__godsEyeView;
const mod = gev.dataManager.layers.get('cctv').module;
if (window.__qaCctvSiblingOwner) {
gev.viewer.entities.remove(window.__qaCctvSiblingOwner);
delete window.__qaCctvSiblingOwner;
}
return {
activeId: mod.getUIState().activeCameraId,
transitions: evidence.activeTransitions.length,
baselineTransitions: evidence.siblingBaselineTransitions,
focusEvents: evidence.focusEvents,
poseSame: evidence.cameraPoseMatches(
evidence.snapshotPose(),
evidence.pose,
),
};
});
record(
'sibling canvas click passes through without CCTV selection or deselection',
siblingOwnsPick
? siblingClick.activeId === clickEvidenceSetup.activeId &&
siblingClick.transitions === siblingClick.baselineTransitions &&
siblingClick.focusEvents === 0 &&
siblingClick.poseSame
: null,
`active=${siblingClick.activeId} transitions=${siblingClick.transitions - siblingClick.baselineTransitions} focus=${siblingClick.focusEvents} poseSame=${siblingClick.poseSame}`,
);
if (emptyClickPoint) {
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
await sleep(250);
}
const firstEmptyClick = await page.evaluate(() => {
const evidence = window.__qaCctvClickEvidence;
const state = window.__godsEyeView.dataManager.layers
.get('cctv')
.module.getUIState();
return {
activeId: state.activeCameraId,
enabled: state.enabled,
transitions: evidence.activeTransitions.length,
baselineTransitions: evidence.siblingBaselineTransitions,
lastTransition: evidence.activeTransitions.at(-1) ?? null,
focusEvents: evidence.focusEvents,
poseSame: evidence.cameraPoseMatches(
evidence.snapshotPose(),
evidence.pose,
),
};
});
record(
'real true-empty canvas click publishes one active-to-null transition',
!!emptyClickPoint &&
firstEmptyClick.activeId === null &&
firstEmptyClick.enabled === true &&
firstEmptyClick.transitions ===
firstEmptyClick.baselineTransitions + 1 &&
firstEmptyClick.lastTransition?.[0] === clickEvidenceSetup.activeId &&
firstEmptyClick.lastTransition?.[1] === null,
`active=${firstEmptyClick.activeId} enabled=${firstEmptyClick.enabled} transitions=${firstEmptyClick.transitions - firstEmptyClick.baselineTransitions}`,
);
record(
'real true-empty deselection preserves pose/tracking and emits no focus request',
!!emptyClickPoint &&
firstEmptyClick.poseSame &&
firstEmptyClick.focusEvents === 0,
`poseSame=${firstEmptyClick.poseSame} focus=${firstEmptyClick.focusEvents}`,
);
if (emptyClickPoint) {
await page.mouse.click(emptyClickPoint.x, emptyClickPoint.y);
await sleep(250);
}
const repeatEmptyClick = await page.evaluate(() => {
const evidence = window.__qaCctvClickEvidence;
const state = window.__godsEyeView.dataManager.layers
.get('cctv')
.module.getUIState();
evidence.dispose();
delete window.__qaCctvClickEvidence;
return {
activeId: state.activeCameraId,
transitions: evidence.activeTransitions.length,
baselineTransitions: evidence.siblingBaselineTransitions,
focusEvents: evidence.focusEvents,
};
});
record(
'repeat true-empty canvas click is null-idempotent',
!!emptyClickPoint &&
repeatEmptyClick.activeId === null &&
repeatEmptyClick.transitions ===
repeatEmptyClick.baselineTransitions + 1 &&
repeatEmptyClick.focusEvents === 0,
`active=${repeatEmptyClick.activeId} transitions=${repeatEmptyClick.transitions - repeatEmptyClick.baselineTransitions} focus=${repeatEmptyClick.focusEvents}`,
);
// Explicit navigation from null must remain available for the remaining
// coverage and gizmo groups. NEXT is the product route pinned by N4.
const resumedId = await page.evaluate(() => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.cycleCamera(1);
return mod.getUIState().activeCameraId;
});
record(
'explicit NEXT resumes from null at the first catalog camera',
typeof resumedId === 'string' && !!resumedId,
`active=${resumedId}`,
);
// =========================================================================
// Group 6: viewshed mode (v3 design §3b — coverage tri-state + volumes)
// =========================================================================
console.log(
'Checking viewshed mode (coverage tri-state + color-coded volumes)...',
);
const countVolumes = () =>
page.evaluate(() => {
const prims = window.__godsEyeView.viewer.scene.primitives;
let n = 0;
for (let i = 0; i < prims.length; i++) {
if (prims.get(i) && prims.get(i)._gevViewshed) n += 1;
}
return n;
});
const cBeforeViewshed = await readCounters();
const modeAfterSet = await page.evaluate(() => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({ coverageMode: 'viewshed' });
return mod.getUIState().coverageMode;
});
await sleep(400);
record(
'coverageMode setParams round-trip (viewshed)',
modeAfterSet === 'viewshed',
`coverageMode=${modeAfterSet}`,
);
const volumesOn = await countVolumes();
record(
'viewshed volumes exist for the visible set (1..15)',
volumesOn >= 1 && volumesOn <= 15,
`${volumesOn} volume primitives (visible-set cap is 14 + active)`,
);
// Boolean back-compat shim: showCoverage=false → 'off' (0 volumes),
// showCoverage=true → 'on' (wireframes, still 0 volumes).
const compat = await page.evaluate(() => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({ showCoverage: false });
const off = mod.getUIState().coverageMode;
mod.setParams({ showCoverage: true });
const on = mod.getUIState().coverageMode;
return { off, on };
});
await sleep(200);
const volumesInOnMode = await countVolumes();
record(
'boolean showCoverage compat (false→off, true→on)',
compat.off === 'off' && compat.on === 'on',
`false→${compat.off}, true→${compat.on}`,
);
record(
"mode 'on' renders zero volumes (wireframes only)",
volumesInOnMode === 0,
`${volumesInOnMode} volumes in 'on' mode`,
);
// Back to viewshed: volumes must be STABLE while idle (no churn — the
// rebuild sites are pose edits and style refreshes only). Object identity
// over 8s proves nothing recreated them.
await page.evaluate(() => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({ coverageMode: 'viewshed' });
});
await sleep(400);
await page.evaluate(() => {
const prims = window.__godsEyeView.viewer.scene.primitives;
window.__qaViewshedRefs = [];
for (let i = 0; i < prims.length; i++) {
if (prims.get(i) && prims.get(i)._gevViewshed)
window.__qaViewshedRefs.push(prims.get(i));
}
});
await sleep(8000);
const viewshedIdle = await page.evaluate(() => {
const prims = window.__godsEyeView.viewer.scene.primitives;
const now = [];
for (let i = 0; i < prims.length; i++) {
if (prims.get(i) && prims.get(i)._gevViewshed) now.push(prims.get(i));
}
const before = window.__qaViewshedRefs || [];
const sameCount = now.length === before.length;
const sameIdentity = sameCount && before.every((p) => now.includes(p));
return { sameCount, sameIdentity, count: now.length };
});
const cAfterViewshed = await readCounters();
const lateFloorSamples =
cAfterViewshed.sampleHeight - cBeforeViewshed.sampleHeight;
record(
'viewshed volumes idle-stable over 8s (same primitives, no churn)',
lateFloorSamples > 0 ? null : viewshedIdle.sameIdentity,
lateFloorSamples > 0
? `late one-shot floor completion sampled ${lateFloorSamples} cells and legitimately rebuilt geometry`
: `count=${viewshedIdle.count} sameCount=${viewshedIdle.sameCount} sameIdentity=${viewshedIdle.sameIdentity}`,
);
record(
'raycast counters flat across viewshed cycling + idle',
lateFloorSamples > 0
? null
: cAfterViewshed.pickFromRay === cBeforeViewshed.pickFromRay,
lateFloorSamples > 0
? `late one-shot floor completion: ΔsampleHeight=${lateFloorSamples}; ΔpickFromRay=${cAfterViewshed.pickFromRay - cBeforeViewshed.pickFromRay}`
: `ΔpickFromRay=${cAfterViewshed.pickFromRay - cBeforeViewshed.pickFromRay} ΔsampleHeight=0`,
);
const viewshedShot = path.join(SHOTS_DIR, 'cctv-v3-viewshed.png');
await saveReviewShot(page, viewshedShot, 'visual review only');
// =========================================================================
// Group 7: calibration gizmo (v3 design §3c — ADJUST mode + drag)
// =========================================================================
console.log('Checking calibration gizmo (ADJUST mode + synthetic drag)...');
const GIZMO_PARTS = [
'ring-heading',
'ring-pitch',
'move-east',
'move-north',
'move-up',
'handle-range',
'handle-fov-l',
'handle-fov-r',
];
const gizmoStates = await page.evaluate((parts) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({ calibrationMode: true });
const viewer = window.__godsEyeView.viewer;
return parts.map((p) => {
const e = viewer.entities.getById('cctv-gizmo-' + p);
return e ? (e.show ? 'shown' : 'hidden') : 'missing';
});
}, GIZMO_PARTS);
record(
'all 8 gizmo parts exist and show in ADJUST mode',
gizmoStates.every((s) => s === 'shown'),
gizmoStates.map((s, i) => `${GIZMO_PARTS[i]}=${s}`).join(' '),
);
// Synthetic CDP drag on the EAST arrow — the owner's field-test
// regression. Sample effective elevation after every mouse move: it must
// stay frozen until release, with no transient sampleHeight calls.
const eastDrag = await page.evaluate(() => {
const viewer = window.__godsEyeView.viewer;
const e = viewer.entities.getById('cctv-gizmo-move-east');
if (!e) return null;
const pts = e.polyline.positions.getValue(viewer.clock.currentTime);
const scene = viewer.scene;
const w = scene.canvas.clientWidth;
const h = scene.canvas.clientHeight;
const a = scene.cartesianToCanvasCoordinates(pts[0]);
const b = scene.cartesianToCanvasCoordinates(pts[pts.length - 1]);
if (
!a ||
!b ||
!Number.isFinite(a.x) ||
!Number.isFinite(a.y) ||
!Number.isFinite(b.x) ||
!Number.isFinite(b.y)
)
return null;
const dx = b.x - a.x;
const dy = b.y - a.y;
const mag = Math.hypot(dx, dy);
if (mag < 10) return null;
const candidates = [
0.12, 0.2, 0.28, 0.36, 0.44, 0.52, 0.6, 0.68, 0.76, 0.84, 0.92,
]
.map((t) => ({ x: a.x + dx * t, y: a.y + dy * t }))
.filter((p) => p.x > 60 && p.y > 60 && p.x < w - 60 && p.y < h - 60);
if (b.x > 60 && b.y > 60 && b.x < w - 60 && b.y < h - 60) {
candidates.unshift({ x: b.x, y: b.y });
}
if (!candidates.length) return null;
return { ...candidates[0], candidates, ux: dx / mag, uy: dy / mag };
});
const calBeforeDrag = await page.evaluate(() => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
const cam = mod.getUIState().activeCamera;
return cam
? {
calibration: cam.calibration,
elevationM: cam.elevationM,
groundResolveCount: cam.groundResolveCount,
groundMeshSampleRequestCount: cam.groundMeshSampleRequestCount,
}
: null;
});
const transientDragStates = [];
const countersBeforeDrag = await readCounters();
let countersBeforeRelease = countersBeforeDrag;
if (eastDrag) {
// Batched polylines can miss one synthetic LEFT_DOWN while their pick
// buffer settles. Try several visible shaft points; stop after the first
// actual offset change so a successful drag still has exactly one release.
for (const grab of eastDrag.candidates) {
const pickBufferReady = await positionPointerForGizmoDrag(page, grab);
if (!pickBufferReady) continue;
const ownsPick = await page.evaluate((point) => {
const scene = window.__godsEyeView.viewer.scene;
const partFrom = (picked) => {
const id = picked?.id?.id ?? picked?.id;
return typeof id === 'string' && id.startsWith('cctv-gizmo-')
? id.slice('cctv-gizmo-'.length)
: null;
};
const direct = partFrom(scene.pick(point, 14, 14));
if (direct) return direct === 'move-east';
const firstGizmo = (scene.drillPick(point, 6, 14, 14) || [])
.map(partFrom)
.find(Boolean);
return firstGizmo === 'move-east';
}, grab);
if (!ownsPick) continue;
await page.mouse.down();
for (let i = 1; i <= 5; i++) {
await page.mouse.move(
grab.x + eastDrag.ux * i * 12,
grab.y + eastDrag.uy * i * 12,
);
await sleep(40);
transientDragStates.push(
await page.evaluate(() => {
const mod =
window.__godsEyeView.dataManager.layers.get('cctv').module;
const cam = mod.getUIState().activeCamera;
return cam
? {
elevationM: cam.elevationM,
eastOffsetM: cam.calibration?.offsetEastM,
groundMeshSampleRequestCount:
cam.groundMeshSampleRequestCount,
}
: null;
}),
);
}
const candidateCountersBeforeRelease = await readCounters();
await page.mouse.up();
await sleep(400);
const candidateEast = await page.evaluate(() => {
const mod =
window.__godsEyeView.dataManager.layers.get('cctv').module;
return (
mod.getUIState().activeCamera?.calibration?.offsetEastM ?? null
);
});
if (
Number.isFinite(candidateEast) &&
Math.abs(
candidateEast - (calBeforeDrag?.calibration?.offsetEastM ?? 0),
) > 0.05
) {
countersBeforeRelease = candidateCountersBeforeRelease;
break;
}
}
}
const dragOutcome = await page.evaluate((pt) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
const viewer = window.__godsEyeView.viewer;
const cam = mod.getUIState().activeCamera;
const raw = localStorage.getItem('godsEyeView.cctv.calibration.v2');
const map = raw ? JSON.parse(raw) : {};
let pickable = null;
let ownsPick = null;
if (pt) {
try {
const picked =
viewer.scene.drillPick({ x: pt.x, y: pt.y }, 6, 14, 14) || [];
pickable = picked.some((r) =>
String(r?.id?.id ?? r?.id ?? '').startsWith('cctv-gizmo-'),
);
const firstGizmo = picked
.map((r) => String(r?.id?.id ?? r?.id ?? ''))
.find((id) => id.startsWith('cctv-gizmo-'));
ownsPick = firstGizmo === 'cctv-gizmo-move-east';
} catch {
pickable = null;
}
}
return {
eastOffsetM: cam?.calibration?.offsetEastM ?? null,
groundResolveCount: cam?.groundResolveCount ?? null,
calDirty: cam?.calDirty ?? null,
stored: cam ? cam.id in map : null,
inputsEnabled: viewer.scene.screenSpaceCameraController.enableInputs,
pickable,
ownsPick,
};
}, eastDrag);
const eastChanged =
eastDrag &&
Number.isFinite(dragOutcome.eastOffsetM) &&
Math.abs(
dragOutcome.eastOffsetM -
(calBeforeDrag?.calibration?.offsetEastM ?? 0),
) > 0.05;
const transientElevations = transientDragStates
.map((state) => state?.elevationM)
.filter(Number.isFinite);
const maxTransientElevationDelta =
transientElevations.length && Number.isFinite(calBeforeDrag?.elevationM)
? Math.max(
...transientElevations.map((height) =>
Math.abs(height - calBeforeDrag.elevationM),
),
)
: Infinity;
const transientSampleDelta =
countersBeforeRelease.sampleHeight - countersBeforeDrag.sampleHeight;
const activeMeshRequestsBeforeRelease = transientDragStates
.map((state) => state?.groundMeshSampleRequestCount)
.filter(Number.isFinite)
.at(-1);
const transientActiveMeshRequestDelta = Number.isFinite(
activeMeshRequestsBeforeRelease,
)
? activeMeshRequestsBeforeRelease -
(calBeforeDrag?.groundMeshSampleRequestCount ?? 0)
: Infinity;
if (eastChanged) {
record(
'east-arrow drag changes the east offset (live, unsaved)',
true,
`offsetEastM ${calBeforeDrag?.calibration?.offsetEastM ?? 0} → ${dragOutcome.eastOffsetM}`,
);
record(
'east-arrow drag keeps mount elevation frozen until mouse-up',
maxTransientElevationDelta < 0.01,
`max transient Δelevation=${maxTransientElevationDelta.toFixed(4)}m over ${transientElevations.length} moves`,
);
record(
'east-arrow transient moves issue zero active-camera mesh-floor requests',
transientActiveMeshRequestDelta === 0,
`active-camera request Δ=${transientActiveMeshRequestDelta}; global sampleHeight Δ=${transientSampleDelta} (other cells may finish asynchronously)`,
);
record(
'east-arrow release resolves the committed floor exactly once',
dragOutcome.groundResolveCount ===
(calBeforeDrag?.groundResolveCount ?? 0) + 1,
`ground resolutions ${calBeforeDrag?.groundResolveCount ?? 0} → ${dragOutcome.groundResolveCount}`,
);
record(
'drag leaves the camera dirty and the store untouched',
dragOutcome.calDirty === true && dragOutcome.stored === false,
`calDirty=${dragOutcome.calDirty} stored=${dragOutcome.stored}`,
);
} else if (eastDrag) {
const missDetail =
dragOutcome.pickable === false
? 'drill-pick cannot see a gizmo under this GL stack'
: dragOutcome.ownsPick === false
? 'the east arrow did not own any tested shaft pick'
: 'LEFT_DOWN did not begin an east-arrow drag despite the ownership precondition';
record(
'east-arrow drag changes the east offset (live, unsaved)',
null,
`${missDetail} — verify on a current headful GPU surface`,
);
record(
'east-arrow drag keeps mount elevation frozen until mouse-up',
null,
'skipped with drag',
);
record(
'east-arrow transient moves issue zero active-camera mesh-floor requests',
null,
'skipped with drag',
);
record(
'east-arrow release resolves the committed floor exactly once',
null,
'skipped with drag',
);
record(
'drag leaves the camera dirty and the store untouched',
null,
'skipped with drag',
);
} else {
const unavailable =
'no on-screen east-arrow shaft found under this GL stack — verify on real GPU';
record(
'east-arrow drag changes the east offset (live, unsaved)',
null,
unavailable,
);
record(
'east-arrow drag keeps mount elevation frozen until mouse-up',
null,
'skipped: drag target unavailable',
);
record(
'east-arrow transient moves issue zero active-camera mesh-floor requests',
null,
'skipped: drag target unavailable',
);
record(
'east-arrow release resolves the committed floor exactly once',
null,
'skipped: drag target unavailable',
);
record(
'drag leaves the camera dirty and the store untouched',
null,
'skipped: drag target unavailable',
);
}
record(
'camera-controller inputs re-enabled after drag',
dragOutcome.inputsEnabled === true,
`enableInputs=${dragOutcome.inputsEnabled}`,
);
// ADJUST off: parts hidden; leave a clean calibration for the next group.
const gizmoOff = await page.evaluate((parts) => {
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
mod.setParams({ calibrationMode: false });
mod.setParams({ calibration: { reset: true } });
const viewer = window.__godsEyeView.viewer;
return parts.map((p) => {
const e = viewer.entities.getById('cctv-gizmo-' + p);
return e ? (e.show ? 'shown' : 'hidden') : 'missing';
});
}, GIZMO_PARTS);
record(
'gizmo parts hide when ADJUST mode turns off',
gizmoOff.every((s) => s === 'hidden'),
gizmoOff.map((s, i) => `${GIZMO_PARTS[i]}=${s}`).join(' '),
);
// -----------------------------------------------------------------------
// Screenshots for human visual review (NOT a pass/fail signal — SwiftShader
// cannot fill plane image materials under headless software GL).
// -----------------------------------------------------------------------
await sleep(1000);
const shot = path.join(SHOTS_DIR, 'cctv-v2-active.png');
await saveReviewShot(page, shot, 'visual review only, not a gate');
// No console errors during the whole exercise.
record(
'no console errors during CCTV v2 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);
});