diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index a7ed66091..08471c14b 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4464,6 +4464,11 @@ silently demoting every later lookup for the session. Tarktee owns both XML requests with one 30-second deadline and shared abort; a failed header or body read cancels its sibling, including a response arriving after cleanup. + Both Tarktee XML catalogs refuse redirects and have a 4 MiB per-document + limit, enforced against declared length and streamed bytes. A rejected fetch, + unsuccessful response, or body-read failure releases both available response + bodies; read failures also cancel an active sibling reader. Cameras sharing + an image number are deduplicated before the Tarktee pack cap is applied. - **CCTV v3 UX — viewshed + calibration gizmo** (built 2026-07-05 and field validated 2026-07-21): the COVERAGE toggle is a tri-state cycle `OFF → ON → VIEWSHED`; viewshed mode renders each visible camera's frustum diff --git a/scripts/qa-cctv-v2.mjs b/scripts/qa-cctv-v2.mjs index 8b69ac83d..400549508 100644 --- a/scripts/qa-cctv-v2.mjs +++ b/scripts/qa-cctv-v2.mjs @@ -11,8 +11,10 @@ * 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 (>=1 real sample — the - * ceiling alone would pass trivially at Δ=0 if the snap never finished), + * 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 @@ -658,13 +660,68 @@ async function main() { // (existence, not just ceiling). The wait below tolerates the update-tick // latency of the completion pass (update interval is 10s). // ----------------------------------------------------------------------- - // Globe terrain uses the cached DEM/prior and intentionally never calls - // scene.sampleHeight. Only the photoreal regime owns a one-shot mesh snap. - // Keep the mesh completion floor strict when that regime is active. - const photorealRegime = await page.evaluate( - () => window.__godsEyeView.viewer.scene.globe?.show === false, + const globeMode = await page.evaluate( + () => window.__godsEyeView.viewer.scene.globe?.show !== false, ); - if (photorealRegime) { + 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; @@ -683,13 +740,6 @@ async function main() { ? `total real samples since enable=${cFloor.sampleHeight - c0.sampleHeight}` : 'tilesLoaded never observed within 45s — floor not assessable this run', ); - } else { - const cFloor = await readCounters(); - record( - 'terrain-globe uses its DEM/prior without photoreal mesh sampling', - cFloor.sampleHeight === c0.sampleHeight, - `mesh samples since enable=${cFloor.sampleHeight - c0.sampleHeight}`, - ); } // Let the completion pass finish before the steady-state window: require diff --git a/src/data/cctvEstonia.test.mjs b/src/data/cctvEstonia.test.mjs index 7ded2cc66..945cf189f 100644 --- a/src/data/cctvEstonia.test.mjs +++ b/src/data/cctvEstonia.test.mjs @@ -192,6 +192,48 @@ test('Tarktee loader refuses a redirect and cancels both response bodies', async assert.equal(cancelled.images, true, 'sibling 200 body is also cancelled'); }); +test('Tarktee loader cancels fulfilled bodies when either fetch rejects', async (t) => { + for (const failedUrl of [TARKTEE_LOCATIONS_URL, TARKTEE_IMAGES_URL]) { + for (const delayedSibling of [false, true]) { + await t.test( + `${failedUrl}, delayed sibling: ${delayedSibling}`, + async (t) => { + quiet(t); + let cancelled = false; + const response = new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode('')); + }, + cancel() { + cancelled = true; + }, + }), + ); + t.mock.method(globalThis, 'fetch', async (url) => { + if (String(url) === failedUrl) throw new TypeError('fetch failed'); + if (delayedSibling) await new Promise(setImmediate); + return response; + }); + + try { + assert.deepEqual(await loadTarkteeSourcesFromDatex(), []); + // An aborted mock transport may still deliver its queued response. + if (delayedSibling) await new Promise(setImmediate); + assert.equal( + cancelled, + true, + 'fulfilled sibling body is cancelled', + ); + } finally { + await response.body.cancel(); + } + }, + ); + } + } +}); + test('Tarktee loader cancels the sibling body when one feed fails or overflows the byte cap', async (t) => { quiet(t); let siblingCancelled = false;