import { createStandalonePlaceSearch } from '../standalone/placeSearch.js'; // Voice-annotation resilience contract tests — pure logic, no network, no browser. // // Locks the 2026-07-21 field-test fixes: // 1. A TRANSIENT (undefined) deferred-outline resolution is retried with backoff // (the /api/overpass proxy caches the late completion, so a retry is nearly // free) instead of silently leaving the mark a point forever. A DEFINITIVE // miss (null) is never retried — honest point beats hammering Overpass. // 2. targetKey normalization strips trailing locality qualifiers so "California" // and "California, United States" dedupe while their outlines are pending // (identity is GEOMETRY once resolved; targetKey is only the pending stand-in). // // Run with: npm test (node --test) import { test } from 'node:test'; import * as Cesium from 'cesium'; import assert from 'node:assert/strict'; import { createAnnotationEngine, resolveOutlineWithRetry, normalizeTargetKey, } from './annotationEngine.js'; import { resolveAnnotationTarget } from './annotationResolver.js'; import { getRenderGovernorDiagnostics, installRenderGovernor, _resetRenderGovernorForTest, } from '../renderGovernor.js'; // A resolver that replays a scripted sequence of outcomes (undefined = transient, // null = definitive miss, object = footprint) and counts its invocations. function scriptedResolver(outcomes) { const calls = { count: 0 }; const resolve = async () => { const i = Math.min(calls.count, outcomes.length - 1); calls.count += 1; const out = outcomes[i]; if (out instanceof Error) throw out; return out; }; return { resolve, calls }; } // Instant fake wait that records the requested backoff delays. function fakeWait() { const delays = []; const waitFn = async (ms) => { delays.push(ms); }; return { waitFn, delays }; } const FP = { ring: [[0, 0], [0, 1], [1, 1], [0, 0]], footprintKind: 'area' }; function installAnimationFrameStubs(t) { const originalRequestAnimationFrame = globalThis.requestAnimationFrame; const originalCancelAnimationFrame = globalThis.cancelAnimationFrame; globalThis.requestAnimationFrame = () => 1; globalThis.cancelAnimationFrame = () => {}; t.after(() => { if (originalRequestAnimationFrame === undefined) delete globalThis.requestAnimationFrame; else globalThis.requestAnimationFrame = originalRequestAnimationFrame; if (originalCancelAnimationFrame === undefined) delete globalThis.cancelAnimationFrame; else globalThis.cancelAnimationFrame = originalCancelAnimationFrame; }); } function fakeRenderer() { const calls = { add: 0, update: 0, remove: 0 }; return { calls, renderer: { add() { calls.add += 1; }, update() { calls.update += 1; }, remove() { calls.remove += 1; }, sync() {}, }, }; } async function flushMicrotasks(rounds = 20) { for (let i = 0; i < rounds; i += 1) await Promise.resolve(); } function httpFailure(status, retryAfter = null) { return { ok: false, status, headers: { get: (name) => (name.toLowerCase() === 'retry-after' ? retryAfter : null) }, }; } test('retry: first-try footprint returns immediately, no retry, no waiting', async () => { const { resolve, calls } = scriptedResolver([FP]); const { waitFn, delays } = fakeWait(); const fp = await resolveOutlineWithRetry(resolve, { delaysMs: [8000, 25000], waitFn }); assert.equal(fp, FP); assert.equal(calls.count, 1); assert.deepEqual(delays, []); }); test('retry: a DEFINITIVE miss (null) is never retried — the honest point stands', async () => { const { resolve, calls } = scriptedResolver([null, FP]); const { waitFn, delays } = fakeWait(); const fp = await resolveOutlineWithRetry(resolve, { delaysMs: [8000, 25000], waitFn }); assert.equal(fp, null); assert.equal(calls.count, 1); assert.deepEqual(delays, []); }); test('retry: a TRANSIENT miss (undefined) re-runs after the first backoff and can succeed', async () => { const { resolve, calls } = scriptedResolver([undefined, FP]); const { waitFn, delays } = fakeWait(); const fp = await resolveOutlineWithRetry(resolve, { delaysMs: [8000, 25000], waitFn }); assert.equal(fp, FP); assert.equal(calls.count, 2); assert.deepEqual(delays, [8000]); // waited once, succeeded on attempt 2 }); test('retry: a transient retry can still conclude with a definitive miss (no third run)', async () => { const { resolve, calls } = scriptedResolver([undefined, null]); const { waitFn, delays } = fakeWait(); const fp = await resolveOutlineWithRetry(resolve, { delaysMs: [8000, 25000], waitFn }); assert.equal(fp, null); assert.equal(calls.count, 2); assert.deepEqual(delays, [8000]); }); test('retry: persistent transients exhaust the backoff schedule and give up as transient', async () => { const { resolve, calls } = scriptedResolver([undefined, undefined, undefined, FP]); const { waitFn, delays } = fakeWait(); const fp = await resolveOutlineWithRetry(resolve, { delaysMs: [8000, 25000], waitFn }); assert.equal(fp, undefined); // never found within budget — mark honestly stays a point assert.equal(calls.count, 3); // initial + one per backoff entry assert.deepEqual(delays, [8000, 25000]); }); test('retry: a stale board (clear/supersede during backoff) stops retrying immediately', async () => { const { resolve, calls } = scriptedResolver([undefined, FP]); const { waitFn, delays } = fakeWait(); let stale = false; const fp = await resolveOutlineWithRetry(resolve, { delaysMs: [8000, 25000], waitFn: async (ms) => { await waitFn(ms); stale = true; }, // goes stale mid-wait isStale: () => stale, }); assert.equal(fp, undefined); assert.equal(calls.count, 1); // never re-ran against a superseded board assert.deepEqual(delays, [8000]); }); test('retry: a thrown resolver is a definitive miss (no retry), matching the old catch→point path', async () => { const { resolve, calls } = scriptedResolver([new Error('boom'), FP]); const { waitFn, delays } = fakeWait(); const fp = await resolveOutlineWithRetry(resolve, { delaysMs: [8000, 25000], waitFn }); assert.equal(fp, null); assert.equal(calls.count, 1); assert.deepEqual(delays, []); }); test('outline queue: an 8-spec batch keeps at most two FIFO upgrades in flight', async (t) => { installAnimationFrameStubs(t); const { renderer, calls } = fakeRenderer(); const started = []; const releases = new Map(); let inFlight = 0; let maxInFlight = 0; const resolveTarget = async ({ target }) => { const index = Number(target.slice('queue-'.length)); return { lon: index, lat: index, height: 0, label: target, source: 'fake', ring: null, resolveOutline: () => new Promise((resolve) => { started.push(target); inFlight += 1; maxInFlight = Math.max(maxInFlight, inFlight); releases.set(target, () => { inFlight -= 1; resolve({ ring: [[index, index], [index + 0.1, index], [index, index + 0.1], [index, index]], footprintKind: 'area', lat: index, lon: index, height: 0, }); }); }), }; }; const engine = createAnnotationEngine({ viewer: {}, renderer, resolveTarget }); let completed = 0; const allCompleted = new Promise((resolve) => engine.onOutlineEvent(() => { completed += 1; if (completed === 8) resolve(); })); const result = await engine.annotate(Array.from({ length: 8 }, (_, i) => ({ type: 'area', target: `queue-${i}`, footprint: true, }))); assert.equal(result.drawn, 8); assert.equal(calls.add, 8, 'all point renders land before queued outlines finish'); assert.deepEqual(started, ['queue-0', 'queue-1']); for (let next = 2; next < 8; next += 1) { releases.get(`queue-${next - 2}`)(); await flushMicrotasks(); assert.equal(started[next], `queue-${next}`, 'queued upgrades start in ask order'); } releases.get('queue-6')(); releases.get('queue-7')(); await allCompleted; assert.equal(maxInFlight, 2); assert.equal(calls.update, 8); }); test('outline queue: clear drops queued-but-unstarted upgrades without a later fetch', async (t) => { let engine; // Destroy while the browser/timer stubs are still installed. t.after(() => engine?.destroy()); installAnimationFrameStubs(t); t.mock.timers.enable({ apis: ['setTimeout'] }); t.after(() => t.mock.timers.reset()); const { renderer } = fakeRenderer(); renderer.destroy = () => {}; let fetchesStarted = 0; const releases = []; const resolveTarget = async ({ target, signal }) => ({ lon: Number(target.slice('clear-'.length)), lat: 10, height: 0, label: target, source: 'fake', ring: null, resolveOutline: () => new Promise((resolve) => { fetchesStarted += 1; const finish = () => resolve(undefined); releases.push(finish); if (signal.aborted) finish(); else signal.addEventListener('abort', finish, { once: true }); }), }); engine = createAnnotationEngine({ viewer: {}, renderer, resolveTarget, }); await engine.annotate( Array.from({ length: 8 }, (_, i) => ({ type: 'area', target: `clear-${i}`, footprint: true, })), ); assert.equal(fetchesStarted, 2); // Both workers enter their real backoff while the remaining six stay queued. releases[0](); releases[1](); await flushMicrotasks(); engine.clear(); await flushMicrotasks(); assert.equal(engine.count(), 0); assert.equal( fetchesStarted, 2, 'the six queued upgrades were dropped on clear', ); await engine.annotate( [8, 9].map((i) => ({ type: 'area', target: `clear-${i}`, footprint: true, })), ); await flushMicrotasks(); assert.equal( fetchesStarted, 4, 'cleared backoff timers release both workers without advancing time', ); }); test('retry: HTTP 429 Retry-After 5s gets one ladder-spaced retry; a second 429 stops', async (t) => { const originalWindow = globalThis.window; const originalFetch = globalThis.fetch; t.mock.timers.enable({ apis: ['setTimeout', 'Date'], now: 10_000 }); globalThis.window = { setTimeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout }; const requestTimes = []; globalThis.fetch = async (url) => { // The one-time capability probe is not an outline request. if (String(url).endsWith('/status')) return httpFailure(404); requestTimes.push(Date.now()); return httpFailure(429, '5'); }; t.after(() => { t.mock.timers.reset(); if (originalWindow === undefined) delete globalThis.window; else globalThis.window = originalWindow; globalThis.fetch = originalFetch; }); const resolved = await resolveAnnotationTarget({ viewer: {}, target: 'PT2 rate-limit fixture', latitude: 12.345, longitude: 67.89, footprint: true, deferFootprint: true, entityKind: 'district', }); let settled = false; const waits = []; const pending = resolveOutlineWithRetry(resolved.resolveOutline, { waitFn: (ms) => { waits.push(ms); return new Promise((resolve) => setTimeout(resolve, ms)); }, }) .finally(() => { settled = true; }); while (waits.length === 0) await flushMicrotasks(); assert.equal(requestTimes.length, 1); assert.deepEqual(waits, [8000], 'the 8s ladder floor is longer than Retry-After: 5'); t.mock.timers.tick(7999); await flushMicrotasks(); assert.equal(requestTimes.length, 1); t.mock.timers.tick(1); await flushMicrotasks(); // Mutation hygiene: if 429 is accidentally collapsed into ordinary transient, // drain its forbidden second ladder timer before making the named assertions. if (waits.length > 1) { t.mock.timers.tick(waits[1]); await flushMicrotasks(); } assert.equal(requestTimes.length, 2); assert.ok(requestTimes[1] - requestTimes[0] >= 5000, 'Retry-After is a hard minimum'); assert.equal(settled, true, 'a second 429 must not schedule the 25s ladder replay'); assert.equal(await pending, undefined); }); test('retry: a plain HTTP 500 still exhausts the existing 8s/25s transient ladder', async (t) => { const originalWindow = globalThis.window; const originalFetch = globalThis.fetch; t.mock.timers.enable({ apis: ['setTimeout', 'Date'], now: 100_000 }); globalThis.window = { setTimeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout }; const requestTimes = []; globalThis.fetch = async (url) => { if (String(url).endsWith('/status')) return Response.json({ configured: true }); requestTimes.push(Date.now()); return httpFailure(500); }; t.after(() => { t.mock.timers.reset(); if (originalWindow === undefined) delete globalThis.window; else globalThis.window = originalWindow; globalThis.fetch = originalFetch; }); const resolved = await resolveAnnotationTarget({ viewer: {}, target: 'PT2 transient fixture', latitude: -12.345, longitude: -67.89, footprint: true, deferFootprint: true, entityKind: 'district', }); let settled = false; const waits = []; const pending = resolveOutlineWithRetry(resolved.resolveOutline, { waitFn: (ms) => { waits.push(ms); return new Promise((resolve) => setTimeout(resolve, ms)); }, }) .finally(() => { settled = true; }); while (waits.length === 0) await flushMicrotasks(); assert.equal(requestTimes.length, 1); assert.deepEqual(waits, [8000]); t.mock.timers.tick(8000); await flushMicrotasks(); assert.equal(requestTimes.length, 2); assert.equal(settled, false); assert.deepEqual(waits, [8000, 25_000]); t.mock.timers.tick(24_999); await flushMicrotasks(); assert.equal(requestTimes.length, 2); t.mock.timers.tick(1); await flushMicrotasks(); assert.equal(requestTimes.length, 3); assert.deepEqual(requestTimes.map((at) => at - requestTimes[0]), [0, 8000, 33_000]); assert.equal(settled, true); assert.equal(await pending, undefined); }); test('targetKey: trailing locality qualifiers are stripped so state names dedupe', () => { assert.equal(normalizeTargetKey('California'), 'california'); assert.equal(normalizeTargetKey('California, United States'), 'california'); assert.equal( normalizeTargetKey('California'), normalizeTargetKey('California, United States'), ); }); test('targetKey: lowercases, trims, and keeps comma-free names intact', () => { assert.equal(normalizeTargetKey(' Texas State Capitol '), 'texas state capitol'); assert.equal(normalizeTargetKey('Lady Bird Lake'), 'lady bird lake'); }); test('targetKey: multi-qualifier names keep only the leading place name', () => { assert.equal(normalizeTargetKey('Sixth Street, Austin, TX'), 'sixth street'); }); test('targetKey: empty / absent targets stay null (coord and pixel specs never pending-collapse)', () => { assert.equal(normalizeTargetKey(''), null); assert.equal(normalizeTargetKey(' '), null); assert.equal(normalizeTargetKey(null), null); assert.equal(normalizeTargetKey(undefined), null); }); test('outline upgrade updates the rendered element in place without remove/add', { timeout: 5000 }, async (t) => { const originalRequestAnimationFrame = globalThis.requestAnimationFrame; const originalCancelAnimationFrame = globalThis.cancelAnimationFrame; const originalWindow = globalThis.window; const originalFetch = globalThis.fetch; globalThis.requestAnimationFrame = () => 1; globalThis.cancelAnimationFrame = () => {}; globalThis.window = { __GOOGLE_MAPS_API_KEY__: 'unit-test-key', setTimeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout, }; let overpassCall = 0; globalThis.fetch = async (url) => { if (String(url).startsWith('/api/google/geocode')) { return { json: async () => ({ status: 'OK', results: [{ formatted_address: 'FB-3 Engine Texas Fixture', types: ['administrative_area_level_1', 'political'], address_components: [{ long_name: 'FB-3 Engine Texas Fixture', types: ['administrative_area_level_1', 'political'], }], geometry: { location: { lat: 31, lng: -99 } }, }], }) }; } assert.equal(String(url), '/api/overpass'); overpassCall += 1; if (overpassCall === 1) { return { ok: true, json: async () => ({ elements: [{ type: 'area', id: 54321, tags: { name: 'FB-3 Engine Texas Fixture', admin_level: '4' }, }] }), }; } return { ok: true, json: async () => ({ elements: [{ type: 'relation', geometry: [ { lon: -106, lat: 25 }, { lon: -93, lat: 25 }, { lon: -93, lat: 36 }, { lon: -106, lat: 36 }, { lon: -106, lat: 25 }, ], }] }), }; }; t.after(() => { if (originalRequestAnimationFrame === undefined) delete globalThis.requestAnimationFrame; else globalThis.requestAnimationFrame = originalRequestAnimationFrame; if (originalCancelAnimationFrame === undefined) delete globalThis.cancelAnimationFrame; else globalThis.cancelAnimationFrame = originalCancelAnimationFrame; if (originalWindow === undefined) delete globalThis.window; else globalThis.window = originalWindow; globalThis.fetch = originalFetch; }); const calls = { add: 0, update: 0, remove: 0 }; const elements = new Map(); let originalElement = null; const renderer = { add(anno) { calls.add += 1; originalElement = { id: anno.id }; elements.set(anno.id, originalElement); }, update(anno) { calls.update += 1; assert.equal(elements.get(anno.id), originalElement); }, remove() { calls.remove += 1; }, sync() {}, }; const viewer = {}; const engine = createAnnotationEngine({ viewer, renderer, placeSearch: createStandalonePlaceSearch({ resolveApiKey: () => 'unit-test-key' }) }); const upgraded = new Promise((resolve) => engine.onOutlineEvent(resolve)); const result = await engine.annotate([{ type: 'area', target: 'FB-3 Engine Texas Fixture', label: 'Texas', footprint: true, }]); await upgraded; assert.equal(result.drawn, 1); assert.deepEqual(calls, { add: 1, update: 1, remove: 0 }); assert.equal(elements.get(result.ids[0]), originalElement, 'rendered element identity survives'); assert.deepEqual(engine.list()[0].anchor, { lon: -100.8, lat: 29.4, height: 0 }); assert.equal(engine.list()[0].ring.length, 5, 'the existing data-level centroid/ring snap remains'); }); // ── Renderer-throw rollback: no phantom mark, no permanent governor hold ────── // // The engine holds continuous render for exactly as long as a mark is live // (annotations.size > 0). A renderer/WebGL throw that leaves an entry in the // map therefore leaks a hold that NOTHING releases short of an explicit clear — // the idle governor is defeated for the rest of the session. The fresh path has // always guarded this; the duplicate-REPLACEMENT path did the renderer swap // outside the guard. (perf rebase 2026-08-17) function throwingRendererHarness() { const calls = { add: 0, remove: 0 }; let failNextAdd = false; return { calls, failAddOnce() { failNextAdd = true; }, renderer: { add() { calls.add += 1; if (failNextAdd) { failNextAdd = false; throw new Error('WebGL context lost'); } }, update() {}, remove() { calls.remove += 1; }, sync() {}, }, }; } const FIXED_POINT = { lon: -97.7431, lat: 30.2672, height: 0 }; // Building installation is a renderer-owned async interval after location resolution. function controlledBuildingRenderer() { const { renderer, calls } = fakeRenderer(); const pending = []; const live = new Set(); const enqueue = (anno, options = {}) => new Promise((resolve, reject) => { let settled = false; const finish = (status = 'installed', error) => { if (settled) return; settled = true; options.signal?.removeEventListener('abort', abort); if (status === 'installed' && (options.isCurrent?.() ?? true)) live.add(anno.id); if (error) reject(error); else resolve({ status, timing: { workMs: 1, maxSliceMs: 1, lastSliceMs: 1 }, }); }; const abort = () => finish('cancelled'); options.signal?.addEventListener('abort', abort, { once: true }); pending.push({ anno, options, finish }); }); renderer.add = (anno, options) => { calls.add += 1; if (anno.footprintKind === 'building' && anno.ring) return enqueue(anno, options); live.add(anno.id); }; renderer.update = (anno, options) => { calls.update += 1; if (anno.footprintKind === 'building' && anno.ring) return enqueue(anno, options); }; renderer.remove = (anno) => { calls.remove += 1; live.delete(anno.id); pending .filter((item) => item.anno === anno) .forEach((item) => item.finish('cancelled')); }; renderer.destroy = () => pending.forEach((item) => item.finish('cancelled')); return { renderer, calls, pending, live }; } const BUILDING_FP = { ...FIXED_POINT, ring: [ [-97.744, 30.267], [-97.743, 30.267], [-97.743, 30.268], [-97.744, 30.267], ], footprintKind: 'building', buildingHeight: 20, }; test('fresh async building batch reserves slots together and releases failed/cleared installs', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const h = controlledBuildingRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer: h.renderer, resolveTarget: async ({ target }) => ({ ...BUILDING_FP, lon: target === 'second' ? -97.7 : BUILDING_FP.lon, }), }); const result = engine.annotate([ { type: 'area', target: 'first', label: 'First' }, { type: 'area', target: 'second', label: 'Second' }, ]); await flushMicrotasks(); assert.equal( h.pending.length, 2, 'both installs enqueue without serial awaits', ); assert.equal(engine.count(), 2, 'pending marks reserve the cap'); h.pending[0].finish('failed', new Error('async WebGL failure')); h.pending[1].finish('timeout'); const completed = await result; assert.equal(completed.drawn, 1); assert.equal(completed.failed, 1); assert.equal(engine.count(), 1); assert.equal(engine.list()[0].outlineUnavailable, true); assert.equal(engine.list()[0].ring, null); assert.equal(engine.list()[0].pendingOutline, false); engine.clear(); assert.equal(engine.count(), 0); assert.ok(!getRenderGovernorDiagnostics().holds.includes('annotations')); const cancelled = engine.annotate([{ type: 'area', target: 'third' }]); await flushMicrotasks(); const task = h.pending.at(-1); engine.clear(); assert.equal(task.options.signal.aborted, true); task.finish(); assert.equal((await cancelled).aborted, true); assert.equal(engine.count(), 0); assert.equal(h.live.size, 0); }); test('outline workers retain completion ownership and clear cancels resolved-but-uninstalled geometry', async (t) => { installAnimationFrameStubs(t); const h = controlledBuildingRenderer(); const started = []; const engine = createAnnotationEngine({ viewer: {}, renderer: h.renderer, resolveTarget: async ({ target }) => ({ ...FIXED_POINT, lon: -97 + Number(target), resolveOutline: async () => { started.push(target); return { ...BUILDING_FP, lon: -97 + Number(target) }; }, }), }); const events = []; engine.onOutlineEvent((event) => events.push(event)); const result = await engine.annotate( ['0', '1', '2'].map((target) => ({ type: 'area', target })), ); assert.equal(result.drawn, 3); await flushMicrotasks(); assert.equal(h.pending.length, 2); assert.deepEqual(started, ['0', '1']); assert.equal(events.length, 0, 'resolver finish is not installation finish'); assert.ok(engine.list().every((anno) => anno.pendingOutline)); h.pending[0].finish(); await flushMicrotasks(); assert.equal(events.length, 1); assert.equal(events[0].status, 'resolved'); assert.deepEqual(started, ['0', '1', '2']); engine.clear(); await flushMicrotasks(); h.pending.forEach((task) => { assert.equal(task.options.signal.aborted, true); task.finish(); }); await flushMicrotasks(); assert.equal(events.length, 1, 'no late outcome on cleared marks'); assert.equal(engine.count(), 0); assert.equal(h.live.size, 0); }); test('building recaption timeout retains old route and a newer replacement owns the swap', async (t) => { installAnimationFrameStubs(t); const h = controlledBuildingRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }), }); const initial = engine.annotate([ { type: 'area', target: 'Capitol', label: 'Old' }, ]); await flushMicrotasks(); h.pending[0].finish(); await initial; const old = engine.list()[0]; const refresh = (label) => engine.annotate([{ type: 'area', target: 'Capitol', label }]); const timed = refresh('Timed'); await flushMicrotasks(); assert.equal(engine.list()[0], old); assert.ok(h.live.has(old.id)); h.pending.at(-1).finish('timeout'); assert.equal((await timed).failed, 1); assert.equal(engine.list()[0], old); assert.equal(old.label, 'Old'); assert.ok(h.live.has(old.id)); const losing = refresh('Losing'); await flushMicrotasks(); const loser = h.pending.at(-1); const winning = refresh('Winning'); await flushMicrotasks(); const winner = h.pending.at(-1); assert.notEqual(loser.anno, winner.anno); assert.equal(loser.options.isCurrent(), false); winner.finish(); loser.finish(); assert.equal((await losing).failed, 1); assert.equal((await winning).drawn, 1); assert.equal(engine.list()[0].label, 'Winning'); assert.equal(h.live.size, 1); assert.ok(!h.live.has(old.id)); const late = refresh('After clear'); await flushMicrotasks(); const lateTask = h.pending.at(-1); engine.clear(); lateTask.finish(); assert.equal((await late).aborted, true); assert.equal(engine.count(), 0); assert.equal(h.live.size, 0); }); test('replacement cleanup failure still removes the old route and retains failed cleanup ownership', async (t) => { installAnimationFrameStubs(t); const h = controlledBuildingRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }), }); const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]); await flushMicrotasks(); h.pending[0].finish(); await initial; const old = engine.list()[0]; const refresh = engine.annotate([{ type: 'area', target: 'Capitol', label: 'New' }]); await flushMicrotasks(); const replacementTask = h.pending.at(-1); const replacement = replacementTask.anno; const originalRemove = h.renderer.remove; const removals = []; let fail = true; h.renderer.remove = (anno) => { removals.push(anno); if (anno === replacement && fail) throw new Error('replacement cleanup failed'); originalRemove(anno); }; assert.throws(() => engine.clear(), /replacement cleanup failed/); assert.ok(removals.includes(old), 'old removal is attempted even when replacement removal throws'); assert.equal(replacementTask.options.isCurrent(), false, 'cleanup failure cannot retain install authority'); assert.equal(engine.list()[0], old, 'failed cleanup remains reachable'); await flushMicrotasks(); fail = false; const attempts = removals.filter((anno) => anno === replacement).length; engine.clear(); assert.ok( removals.filter((anno) => anno === replacement).length > attempts, 'the failed replacement route is retried on the next clear', ); await refresh; assert.equal(engine.count(), 0); assert.equal(h.live.size, 0); replacementTask.finish(); await flushMicrotasks(); assert.equal(h.live.size, 0, 'a revoked install cannot commit after cleanup'); }); test('identical refresh revokes an older building replacement even if cancellation cleanup fails', async (t) => { installAnimationFrameStubs(t); const h = controlledBuildingRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }), }); const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]); await flushMicrotasks(); h.pending[0].finish(); await initial; const old = engine.list()[0]; const pending = engine.annotate([{ type: 'area', target: 'Capitol', label: 'New' }]); await flushMicrotasks(); const task = h.pending.at(-1); const remove = h.renderer.remove; let fail = true; h.renderer.remove = (anno) => { if (anno === task.anno && fail) throw new Error('cancel cleanup failed'); remove(anno); }; const identical = await engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]); assert.equal(identical.results[0].id, old.id); assert.equal(identical.results[0].cleanupPending, true); assert.equal(task.options.isCurrent(), false, 'newest identical ask revokes install authority immediately'); task.finish(); assert.equal((await pending).failed, 1); assert.equal(engine.list()[0], old); assert.equal(old.label, 'Old'); fail = false; engine.clear(); assert.equal(engine.count(), 0); }); test('successful replacement survives failed old-route retirement and keeps cleanup addressable', async (t) => { installAnimationFrameStubs(t); const h = controlledBuildingRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }), }); const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]); await flushMicrotasks(); h.pending[0].finish(); await initial; const old = engine.list()[0]; const remove = h.renderer.remove; const removals = []; let fail = true; h.renderer.remove = (anno) => { removals.push(anno); if (anno === old && fail) throw new Error('retirement failed'); remove(anno); }; const refresh = engine.annotate([{ type: 'area', target: 'Capitol', label: 'New' }]); await flushMicrotasks(); h.pending.at(-1).finish(); const completed = await refresh; const current = engine.list()[0]; assert.equal(completed.drawn, 1); assert.equal(completed.results[0].cleanupPending, true); assert.equal(current.label, 'New'); assert.ok(h.live.has(current.id), 'valid installed replacement is not rolled back by old teardown failure'); assert.equal(h.pending.filter((task) => task.options.isCurrent()).length, 0, 'no install is left pending'); fail = false; const attempts = removals.filter((anno) => anno === old).length; engine.clear(); assert.ok( removals.filter((anno) => anno === old).length > attempts, 'the old route that failed to retire is retried on clear', ); assert.equal(engine.count(), 0); assert.equal(h.live.size, 0); }); test('pending fresh predecessor failure cannot erase a newer building intent', async (t) => { installAnimationFrameStubs(t); const frames = []; globalThis.requestAnimationFrame = (callback) => { frames.push(callback); return frames.length; }; const h = controlledBuildingRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }), }); const first = engine.annotate([{ type: 'area', target: 'Capitol', label: 'First' }]); await flushMicrotasks(); const predecessor = h.pending[0]; assert.ok(frames.length > 0, 'fresh pending point starts its existing fade tick before batch settlement'); const remove = h.renderer.remove; h.renderer.remove = (anno) => { if (anno !== predecessor.anno) remove(anno); }; const second = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Second' }]); await flushMicrotasks(); const successor = h.pending.at(-1); assert.notEqual(successor.anno, predecessor.anno); predecessor.finish('failed', new Error('late old install failure')); await flushMicrotasks(); assert.equal(engine.list()[0], successor.anno); assert.equal(successor.options.isCurrent(), true); successor.finish(); assert.equal((await first).failed, 1); assert.equal((await second).drawn, 1); assert.equal(engine.list()[0].label, 'Second'); engine.clear(); }); test('duplicate pending building results reflect final failure rather than a dead id', async (t) => { installAnimationFrameStubs(t); const h = controlledBuildingRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }), }); const spec = { type: 'area', target: 'Capitol', label: 'Same' }; const batch = engine.annotate([spec, spec]); await flushMicrotasks(); assert.equal(h.pending.length, 1); h.pending[0].finish('failed', new Error('failed shared install')); const result = await batch; assert.equal(result.drawn, 0); assert.equal(result.failed, 2); assert.deepEqual(result.ids, []); assert.equal(engine.count(), 0); }); test('separate duplicate callers observe the original building install outcome', async (t) => { installAnimationFrameStubs(t); for (const outcome of ['installed', 'failed', 'timeout', 'clear']) { const h = controlledBuildingRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }), }); const spec = { type: 'area', target: 'Capitol', label: 'Same' }; const first = engine.annotate([spec]); await flushMicrotasks(); let reported = false; const second = engine.annotate([spec]).then((result) => { reported = true; return result; }); await flushMicrotasks(); assert.equal(h.pending.length, 1); assert.equal( reported, false, 'the observer cannot report before owner settlement', ); assert.equal(h.pending[0].options.signal.aborted, false); assert.equal(h.pending[0].options.isCurrent(), true); if (outcome === 'clear') engine.clear(); else h.pending[0].finish( outcome, outcome === 'failed' ? new Error('owner install failed') : undefined, ); const results = await Promise.all([first, second]); if (outcome === 'failed' || outcome === 'clear') { for (const result of results) { assert.equal(result.drawn, 0); assert.deepEqual(result.ids, []); } assert.equal(engine.count(), 0); } else { assert.equal(results[0].drawn, 1); assert.equal(results[1].drawn, 1); assert.deepEqual(results[1].ids, results[0].ids); assert.equal(results[1].results[0].duplicate, true); if (outcome === 'timeout') { for (const result of results) assert.equal(result.results[0].outlineUnavailable, true); assert.equal(engine.list()[0].ring, null); } } engine.clear(); } }); test('a duplicate joins the physical deferred install but not another outline lookup', async (t) => { installAnimationFrameStubs(t); const h = controlledBuildingRenderer(); let lookups = 0; const engine = createAnnotationEngine({ viewer: {}, renderer: h.renderer, resolveTarget: async () => ({ ...FIXED_POINT, resolveOutline: async () => { lookups++; return { ...BUILDING_FP }; }, }), }); const spec = { type: 'area', target: 'Capitol', label: 'Same' }; const initial = await engine.annotate([spec]); await flushMicrotasks(); assert.equal(initial.drawn, 1, 'the progressive point already exists'); assert.equal(h.pending.length, 1); let reported = false; const duplicate = engine.annotate([spec]).then((result) => { reported = true; return result; }); await flushMicrotasks(); assert.equal(reported, false); assert.equal(h.pending.length, 1); assert.equal(lookups, 1); h.pending[0].finish('failed', new Error('deferred owner install failed')); const result = await duplicate; assert.equal(result.drawn, 1, 'the owner restored an honest point'); assert.deepEqual(result.ids, initial.ids); assert.equal(result.results[0].outlineUnavailable, true); assert.equal(engine.list()[0].ring, null); assert.equal(lookups, 1); assert.equal(h.pending.length, 1); engine.clear(); }); test('duplicate-replacement: a renderer throw leaves no phantom mark and no leaked hold', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const { renderer, failAddOnce } = throwingRendererHarness(); const engine = createAnnotationEngine({ viewer: {}, renderer, resolveTarget: async () => ({ ...FIXED_POINT }), }); const first = await engine.annotate([{ type: 'point', target: 'Texas Capitol', label: 'Capitol' }]); assert.equal(first.drawn, 1); assert.equal(engine.list().length, 1); assert.ok( getRenderGovernorDiagnostics().holds.includes('annotations'), 'a live mark must hold continuous render', ); // Same place, same caption, NEW colour → the duplicate-REPLACEMENT branch, // which deletes the dup and inserts the replacement before touching the // renderer. (A bare point dedupes on LABEL — two different captions at one // spot are deliberately two marks — so a recolour is the way into this // branch for a point; see findDuplicate.) failAddOnce(); const second = await engine.annotate([ { type: 'point', target: 'Texas Capitol', label: 'Capitol', color: 'amber' }, ]); assert.equal(second.drawn, 0, 'the failed swap is reported as a failure, not a draw'); assert.equal(second.failed, 1); assert.equal(engine.list().length, 0, 'no phantom annotation survives the failed swap'); assert.ok( !getRenderGovernorDiagnostics().holds.includes('annotations'), 'the annotations hold must be released — a leak defeats the idle governor permanently', ); }); test('fresh path: a renderer throw is rolled back the same way', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const { renderer, failAddOnce } = throwingRendererHarness(); const engine = createAnnotationEngine({ viewer: {}, renderer, resolveTarget: async () => ({ ...FIXED_POINT }), }); failAddOnce(); const result = await engine.annotate([{ type: 'point', target: 'Texas Capitol', label: 'Capitol' }]); assert.equal(result.drawn, 0); assert.equal(engine.list().length, 0, 'no phantom annotation from a failed first add'); assert.ok( !getRenderGovernorDiagnostics().holds.includes('annotations'), 'a never-rendered mark must not hold continuous render', ); }); // ── Rollback must also unwind PARTIAL renderer state (review round 2) ────────── // // The harness above throws on the FIRST statement of add(), so a rollback that // only deletes the engine's map entry looked complete. The real renderers build // a mark in stages — the hybrid adds world geometry before it records the route; // the screen renderer inserts its SVG group before its last projection pass — so // a throw part-way leaves live content the map rollback cannot reach. The next // annotate of the same geometry then stacks a fresh mark over that orphan. /** A renderer that puts the mark on the board BEFORE the step that fails. */ function partialStateRendererHarness() { const live = new Map(); // stands in for world entities / SVG groups let failNextAdd = false; return { live, failAddOnce() { failNextAdd = true; }, renderer: { add(anno) { live.set(anno.id, anno); // partial renderer state exists NOW if (failNextAdd) { failNextAdd = false; throw new Error('WebGL context lost'); } }, update() {}, remove(anno) { live.delete(anno.id); }, sync() {}, }, }; } test('fresh path: a throw AFTER partial renderer state leaves nothing on the board', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const { renderer, failAddOnce, live } = partialStateRendererHarness(); const engine = createAnnotationEngine({ viewer: {}, renderer, resolveTarget: async () => ({ ...FIXED_POINT }), }); failAddOnce(); const failed = await engine.annotate([{ type: 'point', target: 'Texas Capitol', label: 'Capitol' }]); assert.equal(failed.drawn, 0); assert.equal(engine.list().length, 0, 'no phantom annotation'); assert.equal(live.size, 0, 'the rollback must release what the renderer had already created'); assert.ok( !getRenderGovernorDiagnostics().holds.includes('annotations'), 'a never-rendered mark must not hold continuous render', ); // The real cost of an orphan: re-annotating the same place stacks a second // mark on top of the one nothing owns. const retry = await engine.annotate([{ type: 'point', target: 'Texas Capitol', label: 'Capitol' }]); assert.equal(retry.drawn, 1); assert.equal(live.size, 1, 'a re-annotate must draw ONE mark, not stack over an orphan'); }); test('duplicate-replacement: a throw AFTER partial renderer state leaves nothing on the board', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const { renderer, failAddOnce, live } = partialStateRendererHarness(); const engine = createAnnotationEngine({ viewer: {}, renderer, resolveTarget: async () => ({ ...FIXED_POINT }), }); const first = await engine.annotate([{ type: 'point', target: 'Texas Capitol', label: 'Capitol' }]); assert.equal(first.drawn, 1); assert.equal(live.size, 1); // Recolour → the duplicate-REPLACEMENT swap: remove(dup) lands, add(anno) // creates its state and then throws. failAddOnce(); const swap = await engine.annotate([ { type: 'point', target: 'Texas Capitol', label: 'Capitol', color: 'amber' }, ]); assert.equal(swap.drawn, 0); assert.equal(engine.list().length, 0, 'no phantom annotation survives the failed swap'); assert.equal(live.size, 0, 'and no half-swapped mark survives on the board'); assert.ok( !getRenderGovernorDiagnostics().holds.includes('annotations'), 'the annotations hold must be released', ); const retry = await engine.annotate([ { type: 'point', target: 'Texas Capitol', label: 'Capitol', color: 'amber' }, ]); assert.equal(retry.drawn, 1); assert.equal(live.size, 1, 'the recoloured mark redraws once, with no orphan underneath'); }); test('destroy releases the renderer and prevents subsequent annotation work', async (t) => { installAnimationFrameStubs(t); const { renderer, calls } = fakeRenderer(); let destroyed = 0; renderer.destroy = () => { destroyed++; }; const engine = createAnnotationEngine({ viewer: {}, renderer }); const drawn = await engine.annotate([{ type: 'pin', longitude: -97.74, latitude: 30.27 }]); assert.equal(drawn.drawn, 1); engine.destroy(); const additions = calls.add; const result = await engine.annotate([{ type: 'pin', target: 'Must not resolve' }]); assert.equal(result.error, 'destroyed'); assert.equal(calls.add, additions); assert.equal(engine.count(), 0); engine.destroy(); assert.equal(destroyed, 1); assert.ok(!getRenderGovernorDiagnostics().holds.includes('annotations')); }); test('a late annotation resolver cannot redraw after destruction', async (t) => { installAnimationFrameStubs(t); const { renderer, calls } = fakeRenderer(); renderer.destroy = () => {}; let release; let signal; const engine = createAnnotationEngine({ viewer: {}, renderer, resolveTarget: (options) => { signal = options.signal; return new Promise((resolve) => { release = resolve; }); }, }); const pending = engine.annotate([{ type: 'pin', target: 'Pending place' }]); engine.destroy(); assert.equal(signal.aborted, true); release({ lon: -97.74, lat: 30.27 }); await pending; assert.equal(calls.add, 0); assert.equal(engine.count(), 0); }); test('only an explicit navigation request permits resolving distant annotation targets', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const { renderer } = throwingRendererHarness(); renderer.destroy = () => {}; const received = []; const engine = createAnnotationEngine({ viewer: {}, renderer, resolveTarget: async (options) => { received.push(options); return null; }, }); await engine.annotate([{ type: 'point', target: 'Remote landmark' }]); await engine.annotate([{ type: 'point', target: 'Remote landmark' }], { flyTo: true }); assert.deepEqual(received.map(options => options.allowDistant), [false, true]); engine.destroy(); }); test('recaption inherits outline credit from borrowed geometry rather than its new anchor', async (t) => { installAnimationFrameStubs(t); const { renderer } = fakeRenderer(); let first = true; const ring = [[-99,31],[-98.99,31],[-98.99,31.01],[-99,31]]; const engine = createAnnotationEngine({ viewer: {}, renderer, resolveTarget: async () => { if (first) { first = false; return { ...FIXED_POINT, ring, footprintKind: 'area', outlineCredit: 'tiles' }; } return { ...FIXED_POINT, source: 'osm-local', resolveOutline: async () => null }; }, }); await engine.annotate([{ type: 'area', target: 'Library', label: 'Old' }]); await engine.annotate([{ type: 'area', target: 'Library', label: 'New' }]); await flushMicrotasks(); assert.equal(engine.list()[0].outlineCredit, 'tiles'); assert.equal(engine.list()[0].ring, ring); engine.clear(); }); // Draw's list works one hand-drawn mark at a time: hide, rename, recolour, // frame and remove it, and hear about every change to the board. test('hand-drawn marks: one mark can be hidden, renamed, recoloured, framed and removed', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const { renderer, calls } = fakeRenderer(); const flights = []; const engine = createAnnotationEngine({ viewer: { camera: { heading: 0, flyToBoundingSphere: (sphere) => flights.push(sphere) } }, renderer, }); let changes = 0; const unsubscribe = engine.onChange(() => { changes += 1; }); const drawn = await engine.annotate([ { type: 'route', manual: true, path: [[-97.75, 30.26], [-97.73, 30.27]], label: '' }, { type: 'pin', manual: true, latitude: 30.27, longitude: -97.74, label: 'Camp', color: 'amber' }, ], { persist: true }); assert.equal(drawn.drawn, 2); assert.equal(changes, 1, 'one annotate, one change'); const [line, pin] = engine.list(); assert.equal(line.manual, true); assert.equal(line.name, null); assert.match(line.label, /km|m$/, 'an unnamed line is labelled by its length'); assert.equal(pin.name, 'Camp'); assert.equal(engine.setHidden(pin.id, true), true); assert.equal(pin.hidden, true); assert.equal(changes, 2); const addsBefore = calls.add; assert.equal(engine.restyle(line.id, { name: 'Ridge walk', color: 'green' }), true); assert.equal(line.name, 'Ridge walk'); assert.match(line.label, /^Ridge walk — /, 'a renamed line keeps its length'); assert.equal(line.color, 'green'); assert.equal(calls.add, addsBefore + 1, 'the new look is drawn again'); assert.equal(engine.restyle(line.id, { color: 'not-a-colour' }), true); assert.equal(line.color, 'green', 'an unknown colour is ignored'); assert.equal(engine.frame(line.id), true); assert.equal(flights.length, 1); assert.equal(engine.frame('anno-missing'), false); assert.equal(engine.remove(pin.id), true); assert.deepEqual(engine.list().map(({ id }) => id), [line.id]); assert.equal(engine.remove(pin.id), false); engine.clear(); assert.equal(engine.list().length, 0); const seen = changes; unsubscribe(); await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true }); assert.equal(changes, seen, 'an unsubscribed listener hears nothing'); }); // The list trusts these calls to change the globe, not just the list: each // is checked by its effect (renderer calls, alpha, camera, notifications). test('hand-drawn marks: remove, hide, clear, expiry and framing have their effect', async (t) => { const frames = []; const originalRequestAnimationFrame = globalThis.requestAnimationFrame; const originalCancelAnimationFrame = globalThis.cancelAnimationFrame; globalThis.requestAnimationFrame = (callback) => frames.push(callback); globalThis.cancelAnimationFrame = () => {}; let now = 1_000_000; t.mock.method(performance, 'now', () => now); _resetRenderGovernorForTest(); t.after(() => { globalThis.requestAnimationFrame = originalRequestAnimationFrame; globalThis.cancelAnimationFrame = originalCancelAnimationFrame; _resetRenderGovernorForTest(); }); const runFrames = () => { for (let i = 0; i < 5 && frames.length; i += 1) frames.shift()(); }; const { renderer, calls } = fakeRenderer(); const flights = []; const engine = createAnnotationEngine({ viewer: { camera: { heading: 0, flyToBoundingSphere: (sphere) => flights.push(sphere), }, }, renderer, }); let changes = 0; engine.onChange(() => { changes += 1; }); await engine.annotate( [ { type: 'pin', manual: true, latitude: 30.27, longitude: -97.74, label: 'Camp', }, { type: 'pin', manual: true, latitude: 30.3, longitude: -97.7, label: 'Base', }, ], { persist: true }, ); now += 1000; runFrames(); const [camp, base] = engine.list(); assert.equal(camp.alpha, 1); engine.setHidden(camp.id, true); runFrames(); assert.equal(camp.alpha, 0, 'a hidden mark is drawn at alpha 0'); assert.ok(engine.list().includes(camp), 'and stays on the board'); engine.setHidden(camp.id, false); runFrames(); assert.equal(camp.alpha, 1, 'showing it again is immediate, with no fade-in'); const removes = calls.remove; let seen = changes; engine.remove(base.id); assert.equal(calls.remove, removes + 1, 'remove takes it off the globe'); assert.equal(changes, seen + 1, 'and tells the list'); seen = changes; engine.clear(); assert.equal(changes, seen + 1, 'a clear tells the list'); // A voice mark with a TTL: its expiry must reach the list too. await engine.annotate( [{ type: 'pin', latitude: 1, longitude: 1, label: 'Brief' }], { persist: false, ttlMs: 2000, }, ); seen = changes; now += 60_000; runFrames(); assert.equal(engine.list().length, 0, 'the mark expired'); assert.equal(changes, seen + 1, 'expiry tells the list'); // A line across the antimeridian is framed there, not on the far side. await engine.annotate( [ { type: 'route', manual: true, path: [ [179.9, -17], [-179.9, -17.1], ], label: '', }, ], { persist: true }, ); const [line] = engine.list(); engine.frame(line.id); const centre = Cesium.Cartographic.fromCartesian(flights.at(-1).center); assert.ok( Math.abs(Math.abs(Cesium.Math.toDegrees(centre.longitude)) - 180) < 0.5, `framed at ${Cesium.Math.toDegrees(centre.longitude)}°, not near 180°`, ); }); test('hand-drawn marks: a voice mark or a hidden mark is not taken as their duplicate', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const { renderer } = fakeRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer }); await engine.annotate( [ { type: 'pin', manual: true, latitude: 30.27, longitude: -97.74, label: 'Camp', }, ], { persist: true }, ); const [drawn] = engine.list(); // The agent marks the same spot with the same label, briefly. await engine.annotate( [{ type: 'pin', latitude: 30.27, longitude: -97.74, label: 'Camp' }], { persist: false, }, ); assert.equal(engine.list().length, 2, 'the voice mark is its own mark'); assert.equal(drawn.ttlMs, null, 'the drawn pin keeps persisting'); assert.equal(drawn.manual, true); engine.setHidden(drawn.id, true); const again = await engine.annotate( [ { type: 'pin', manual: true, latitude: 30.27, longitude: -97.74, label: 'Camp', }, ], { persist: true }, ); assert.equal(again.drawn, 1); const visible = engine.list().filter((mark) => mark.manual && !mark.hidden); assert.equal( visible.length, 1, 'a pin dropped on a hidden one is a new, visible pin', ); }); test('hand-drawn marks: only a manual mark can be restyled', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const { renderer } = fakeRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer, resolveTarget: async () => ({ ...FIXED_POINT }), }); await engine.annotate([{ type: 'point', target: 'Texas Capitol', label: 'Capitol' }]); const [voiceMark] = engine.list(); assert.equal(voiceMark.manual, false); assert.equal(engine.restyle(voiceMark.id, { name: 'Renamed' }), false); assert.equal(voiceMark.label, 'Capitol'); }); test('a drawn line that rounds up to the next unit is labelled in it, not as 1000 m or 10.0 km', async (t) => { installAnimationFrameStubs(t); const labels = []; const renderer = { add(anno) { labels.push(anno.label); }, update() {}, remove() {}, sync() {}, destroy() {} }; const engine = createAnnotationEngine({ viewer: {}, renderer }); const metresPerDegree = (6371.0088 * 1000 * Math.PI) / 180; for (const metres of [994, 996, 9940, 9960]) { await engine.annotate([ { type: 'route', manual: true, path: [[0, 0], [0, metres / metresPerDegree]] }, ]); } assert.deepEqual(labels, ['990 m', '1.0 km', '9.9 km', '10 km']); engine.destroy(); }); test('hand-drawn marks: two unnamed pins close together are two pins, whatever their colours', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const { renderer } = fakeRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer }); const pin = (lon, color) => ({ type: 'pin', manual: true, latitude: 30.27, longitude: lon, label: '', color }); await engine.annotate([pin(-97.74, 'primary')], { persist: true }); await engine.annotate([pin(-97.7402, 'primary')], { persist: true }); await engine.annotate([pin(-97.7404, 'red')], { persist: true }); assert.deepEqual( engine.list().map(({ color }) => color), ['primary', 'primary', 'red'], 'no drawn pin is dropped or replaced by the next', ); }); test('hand-drawn marks: hiding a mark before its first frame takes it off at once', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); let syncs = 0; const renderer = { add() {}, update() {}, remove() {}, sync() { syncs += 1; }, destroy() {} }; const engine = createAnnotationEngine({ viewer: {}, renderer }); await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true }); const [mark] = engine.list(); const before = syncs; engine.setHidden(mark.id, true); assert.ok(syncs > before, 'the renderer hears about it now'); assert.ok( !getRenderGovernorDiagnostics().holds.includes('annotations'), 'and the render hold goes with the last visible mark', ); }); test('hand-drawn marks: with every mark hidden the scene idles, and showing one wakes it', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); let renders = 0; installRenderGovernor({ scene: { requestRenderMode: true, requestRender: () => (renders += 1) } }); const { renderer } = fakeRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer }); await engine.annotate([ { type: 'pin', manual: true, latitude: 1, longitude: 1 }, { type: 'pin', manual: true, latitude: 2, longitude: 2 }, ], { persist: true }); const [first, second] = engine.list(); const held = () => getRenderGovernorDiagnostics().holds.includes('annotations'); assert.ok(held(), 'visible marks keep the scene drawing'); engine.setHidden(first.id, true); assert.ok(held(), 'one visible mark is enough'); const before = renders; engine.setHidden(second.id, true); assert.equal(held(), false, 'every mark hidden: the scene idles'); assert.ok(renders > before, 'after one last frame that takes them off'); engine.setHidden(second.id, false); assert.ok(held(), 'showing one wakes it again'); }); test('hand-drawn marks: a restyle the renderer cannot draw takes the mark off and says so', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const { renderer, failAddOnce } = throwingRendererHarness(); let syncs = 0; renderer.sync = () => (syncs += 1); const engine = createAnnotationEngine({ viewer: {}, renderer }); await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true }); const [mark] = engine.list(); let changes = 0; engine.onChange(() => (changes += 1)); const before = syncs; failAddOnce(); assert.throws(() => engine.restyle(mark.id, { color: 'red' }), /WebGL context lost/); assert.equal(engine.list().length, 0, 'no ghost row is left behind'); assert.equal(changes, 1, 'the list hears about it'); assert.ok(syncs > before, 'and the renderer is told'); }); test('hand-drawn marks: a mark shown again right after it was drawn appears at once, not fading in', async (t) => { _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const queued = []; const raf = globalThis.requestAnimationFrame; const caf = globalThis.cancelAnimationFrame; globalThis.requestAnimationFrame = (callback) => queued.push(callback); globalThis.cancelAnimationFrame = () => {}; t.after(() => { globalThis.requestAnimationFrame = raf; globalThis.cancelAnimationFrame = caf; }); const tick = () => queued.splice(0).forEach((callback) => callback()); const { renderer } = fakeRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer }); await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true }); const [mark] = engine.list(); engine.setHidden(mark.id, true); tick(); assert.equal(mark.alpha, 0); engine.setHidden(mark.id, false); tick(); assert.equal(mark.alpha, 1, 'no fade-in after a hide'); }); test('removing one mark whose cleanup fails keeps it addressable for a retry', async (t) => { installAnimationFrameStubs(t); _resetRenderGovernorForTest(); t.after(() => _resetRenderGovernorForTest()); const { renderer, calls } = fakeRenderer(); let failRemove = true; renderer.remove = () => { calls.remove += 1; if (failRemove) throw new Error('renderer remove failed'); }; const engine = createAnnotationEngine({ viewer: { camera: { heading: 0 } }, renderer }); await engine.annotate([ { type: 'pin', manual: true, latitude: 30.27, longitude: -97.74, label: 'Camp' }, ], { persist: true }); const [pin] = engine.list(); assert.throws(() => engine.remove(pin.id), /renderer remove failed/); assert.deepEqual(engine.list().map(({ id }) => id), [pin.id], 'the mark is not orphaned'); assert.equal(pin.cleanupPending, true); failRemove = false; assert.equal(engine.remove(pin.id), true); assert.deepEqual(engine.list(), []); assert.equal(pin.cleanupPending, undefined); }); test('a building install in the batch does not re-judge marks that never installed', async (t) => { installAnimationFrameStubs(t); const h = controlledBuildingRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer: h.renderer, resolveTarget: async ({ target }) => target === 'Tower' ? { ...BUILDING_FP, lon: -97.7 } : { ...FIXED_POINT }, }); // The recolour replaces the first point within the same batch; without the // building that point's result stays ok, so the building must not change it. const batch = engine.annotate([ { type: 'point', target: 'Capitol', label: 'Capitol' }, { type: 'area', target: 'Tower', label: 'Tower' }, { type: 'point', target: 'Capitol', label: 'Capitol', color: 'amber' }, ]); await flushMicrotasks(); h.pending[0].finish(); const completed = await batch; assert.deepEqual(completed.results.map((result) => result.ok), [true, true, true]); assert.equal(completed.drawn, 3); }); test('plain recaption keeps the old mark addressable when its removal throws', async (t) => { installAnimationFrameStubs(t); const { renderer } = fakeRenderer(); const engine = createAnnotationEngine({ viewer: {}, renderer, resolveTarget: async () => ({ ...FIXED_POINT }), }); await engine.annotate([{ type: 'point', target: 'Capitol', label: 'Capitol' }]); const old = engine.list()[0]; const removals = []; let fail = true; renderer.remove = (anno) => { removals.push(anno.id); if (anno.id === old.id && fail) throw new Error('old route removal failed'); }; const swap = await engine.annotate([ { type: 'point', target: 'Capitol', label: 'Capitol', color: 'amber' }, ]); assert.equal(swap.drawn, 0); assert.deepEqual(engine.list().map((anno) => anno.id), [old.id], 'failed cleanup remains reachable'); assert.equal(engine.list()[0].cleanupPending, true); fail = false; const attempts = removals.filter((id) => id === old.id).length; engine.clear(); assert.ok( removals.filter((id) => id === old.id).length > attempts, 'the old route is retried on the next clear', ); assert.equal(engine.count(), 0); });