From e8870a4766c802fe6ba235d47006109c1235370b Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Tue, 27 Oct 2026 10:48:26 +0400 Subject: [PATCH] test(migration): establish W2 native oracle and release removed overlay identities Add the sequential W2/completion plan and deterministic earthquake ground-raster, geometry, lifecycle and strict runtime QA. Prove the comparator rejects incomplete evidence and release deleted overlay entries from the collision arbiter. Record native dev/preview acceptance and full checkpoint gates. --- scripts/fixtures/w2/earthquakes.mjs | 47 +++++++++++++++ scripts/format-scope.json | 5 +- src/overlays/worldOverlay.js | 20 +++++-- src/overlays/worldOverlay.test.mjs | 89 +++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 scripts/fixtures/w2/earthquakes.mjs diff --git a/scripts/fixtures/w2/earthquakes.mjs b/scripts/fixtures/w2/earthquakes.mjs new file mode 100644 index 000000000..d7898924e --- /dev/null +++ b/scripts/fixtures/w2/earthquakes.mjs @@ -0,0 +1,47 @@ +// USGS-shaped fixed observations; source parsers remain the production ones. +export const W2_EPOCH_MS = 1792152000000; +export function earthquakeFixture() { + const features = []; + for (const [region, lon, lat] of [ + ['sf', -122.42, 37.77], + ['austin', -97.74, 30.27], + ['alaska', -150.4, 61.0], + ]) { + for (let i = 0; i < 5; i++) { + features.push({ + type: 'Feature', + id: `qa-${region}-${i}`, + geometry: { + type: 'Point', + coordinates: [ + lon + (i - 2) * 0.24, + lat + (i % 2) * 0.3, + [8, 70, 300, 301, 45][i], + ], + }, + properties: { + mag: [2.5, 3, 4, 5, 5.5][i], + place: `W2 ${region} event ${i}`, + time: W2_EPOCH_MS - i * 60000, + }, + }); + } + } + features.push({ + type: 'Feature', + id: 'qa-below', + geometry: { type: 'Point', coordinates: [-122.42, 37.77, 5] }, + properties: { mag: 2.4 }, + }); + features.push({ + type: 'Feature', + id: 'qa-missing-mag', + geometry: { type: 'Point', coordinates: [-122.4, 37.7, 5] }, + properties: { mag: null }, + }); + return { + type: 'FeatureCollection', + metadata: { generated: W2_EPOCH_MS }, + features, + }; +} diff --git a/scripts/format-scope.json b/scripts/format-scope.json index e8179a0c4..5b1a7dfc9 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1179,5 +1179,8 @@ "src/globe/navara/movers/frameCamera.test.mjs", "src/layers/localAdsb/qaDrawn.js", "src/layers/localAdsb/qaDrawn.test.mjs", - "src/tooling/w1RuntimeEvidence.test.mjs" + "src/tooling/w1RuntimeEvidence.test.mjs", + "scripts/qa-w2-layers.mjs", + "scripts/fixtures/w2/earthquakes.mjs", + "src/tooling/w2QaComparison.test.mjs" ] diff --git a/src/overlays/worldOverlay.js b/src/overlays/worldOverlay.js index e040de295..65de0e450 100644 --- a/src/overlays/worldOverlay.js +++ b/src/overlays/worldOverlay.js @@ -951,6 +951,14 @@ export function registerWorldOverlayPaintLane(laneId, painter, options = {}) { }; } +// Removed entries cannot paint an exit fade: their source no longer owns +// them. Release the arbiter identity too, so later layout changes cannot +// wake the idle renderer for an invisible minimum-lifetime/fade state. +function forgetOverlayEntry(entry) { + _records.delete(entry._overlayKey); + _domains.get(entry.collisionGroup)?.arbiter.remove(entry._overlayKey); +} + /** * Replace all entries owned by a source atomically. * @param {string} sourceId @@ -968,7 +976,7 @@ export function setOverlayEntries(sourceId, entries, options = {}) { const next = new Map(); for (const entry of normalized) next.set(entry.id, entry); for (const entry of source.entries.values()) { - if (!next.has(entry.id)) _records.delete(entry._overlayKey); + if (!next.has(entry.id)) forgetOverlayEntry(entry); const replacement = next.get(entry.id); if (replacement && isProtected(entry) !== isProtected(replacement)) getOrCreateDomain(entry.collisionGroup).arbiter.remove(entry._overlayKey); @@ -1005,9 +1013,10 @@ export function removeOverlayEntry(sourceId, entryId) { const source = _sources.get(assertSourceId(sourceId)); if (!source) return false; const id = String(entryId); - const removed = source.entries.delete(id); - if (!removed) return false; - _records.delete(entryKey(source.id, id)); + const entry = source.entries.get(id); + if (!entry) return false; + source.entries.delete(id); + forgetOverlayEntry(entry); rebuildSourceCohorts(source); updateEntryDiagnostics(); invalidateHost(); @@ -1023,8 +1032,7 @@ export function clearOverlaySource(sourceId) { if (_destroyed) return false; const source = _sources.get(assertSourceId(sourceId)); if (!source) return false; - for (const entry of source.entries.values()) - _records.delete(entry._overlayKey); + for (const entry of source.entries.values()) forgetOverlayEntry(entry); source.entries.clear(); source.cohortDomainIds.length = 0; source.cohortLists.length = 0; diff --git a/src/overlays/worldOverlay.test.mjs b/src/overlays/worldOverlay.test.mjs index 34676a655..0a3b6e340 100644 --- a/src/overlays/worldOverlay.test.mjs +++ b/src/overlays/worldOverlay.test.mjs @@ -2378,3 +2378,92 @@ test('the host takes a globe engine and refuses a bare viewer', async () => { assert.equal(engine.frames.postRender.numberOfListeners, 1); env.cleanup(); }); + +// Source deletion is final: invisible candidates must not retain minimum-life +// or fade state which wakes a dormant scene on a later UI/layout invalidation. +for (const [name, remove] of [ + ["clear", () => clearOverlaySource("deleted-static")], + ["remove", () => removeOverlayEntry("deleted-static", "event")], + ["replace-empty", () => setOverlayEntries("deleted-static", [])], +]) { + test(`removed static labels stay dormant after ${name}`, () => { + const env = installMockEnvironment(); + try { + initWorldOverlay(env.engine); + setOverlayEntries( + "deleted-static", + [ + selectedEntry("event", { + selected: false, + protected: false, + variant: "card", + collisionGroup: "deleted-static", + priority: 100, + verticalOnly: true, + }), + ], + { collisionCapacity: 1, maxVisible: 1 }, + ); + env.advanceTime(600); + env.postRender.raise(); + env.advanceTime(600); + env.postRender.raise(); + assert.ok(getWorldOverlayDiagnostics().selectedCount > 0); + remove(); + env.postRender.raise(); + env.viewer.scene.requestRenderCount = 0; + for (let i = 0; i < 16; i++) { + env.advanceTime(250); + for (const observer of env.resizeObservers) observer.callback(); + env.postRender.raise(); + } + assert.equal(getWorldOverlayDiagnostics().paintedCount, 0); + assert.equal( + env.viewer.scene.requestRenderCount, + 0, + "removed source must not retain fades/minimum lifetimes and wake idle", + ); + } finally { + env.cleanup(); + } + }); +} + +test("clearing one static source preserves another source in the same label domain", () => { + const env = installMockEnvironment(); + try { + initWorldOverlay(env.engine); + for (const [source, x] of [ + ["left", -0.5], + ["right", 0.5], + ]) + setOverlayEntries( + source, + [ + selectedEntry("event", { + position: new Cesium.Cartesian3(x, 0, 0), + selected: false, + protected: false, + collisionGroup: "ambient-card", + verticalOnly: true, + }), + ], + { collisionCapacity: 1, maxVisible: 1 }, + ); + env.advanceTime(100); + env.postRender.raise(); + env.advanceTime(600); + env.postRender.raise(); + assert.deepEqual(getWorldOverlayDiagnostics().paintedBySource, { + left: 1, + right: 1, + }); + clearOverlaySource("left"); + env.postRender.raise(); + assert.deepEqual(getWorldOverlayDiagnostics().paintedBySource, { + right: 1, + }); + } finally { + env.cleanup(); + } +});