test(street-level): close the mutation survivors; Esc in rich text

A mutation review left 18 earlier survivors; each now has a test that
fails with the mutant applied:

- the expanded viewer returns into the panel on shrink, close and destroy
  (the fake DOM had no parentNode, so reinsertion was never exercised); the
  fake DOM gains offsetParent and a MutationObserver that honours its
  options, and a <body> child added while expanded goes inert;
- each pose moves the marker, drives FOLLOW and selects a new sequence;
  close() retires an open still mounting or opening; a provider toggled
  while the layer is off activates nothing; the adapter drops late and
  superseded poses;
- share-link defaults: "any date" (0 days), the codec max pinned to the
  filter's MAX_SINCE_DAYS (layerState cannot import it across package
  boundaries, so a test ties the two), Mapillary on when a link omits it;
- Esc that something else handled, or typed in a field, keeps the
  selection; a key problem outranks LOADING on the pill;
- the marker's terrain height (mesh, DEM, clamp then re-place);
- proxy: LRU memory cache and its half-budget rule, Retry-After capped,
  a request after a cancelled fetch starts a fresh one;
- date-line tile ranking with and without focus; the horizon cull idles
  while the camera stands still; the old zoom goes as soon as the new one
  lands; a rate limit holds every refresh inside its wait.

One real bug found and fixed: Esc inside a contenteditable cleared the
selected sequence (selection.js now ignores isContentEditable targets).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-06 16:54:06 +01:00
1 parent 9dde7b4a62
commit 3af0f80cdb
15 files changed
+1198 -30

No files matched your search

+200
View File
@@ -13,7 +13,9 @@ import * as tiles from '../../server/providers/mapillary/tiles.js';
import {
TILE_DISK_FILE_OVERHEAD_BYTES,
TILE_MAX_BYTES,
TILE_MEMORY_BUDGET_BYTES,
TILE_MEMORY_ENTRY_OVERHEAD_BYTES,
TILE_RATE_LIMIT_MAX_HOLD_MS,
TILE_ROUTE_MAX_PER_MIN,
TILE_UPSTREAM_CONCURRENCY,
} from '../../server/providers/mapillary/constants.js';
@@ -259,6 +261,45 @@ test('a 429 passes its Retry-After on and holds misses until it is over (review
);
});
for (const [why, retryAfter] of [
['seconds', () => '999999'],
['an HTTP date', () => new Date(Date.now() + 365 * 24 * HOUR).toUTCString()],
])
test(`a far-off Retry-After (${why}) holds misses for at most TILE_RATE_LIMIT_MAX_HOLD_MS`, async (t) => {
const maxSec = TILE_RATE_LIMIT_MAX_HOLD_MS / 1000;
await withUpstream(
() =>
new Response('{}', {
status: 429,
headers: { 'Retry-After': retryAfter() },
}),
async ({ calls, call }) => {
const first = await call('/api/mapillary/tiles', '/coverage/14/5/5');
assert.equal(first.statusCode, 429);
assert.equal(first.headers['retry-after'], String(maxSec));
assert.equal(json(first).retryAfter, maxSec);
const now = Date.now();
t.mock.method(
Date,
'now',
() => now + TILE_RATE_LIMIT_MAX_HOLD_MS - 5000,
);
const held = await call('/api/mapillary/tiles', '/coverage/14/6/6');
assert.equal(held.statusCode, 429);
assert.ok(
json(held).retryAfter <= 5,
'the hold counts down from the cap',
);
assert.equal(calls.length, 1, 'held: Mapillary is not asked again');
Date.now.mock.mockImplementation(
() => now + TILE_RATE_LIMIT_MAX_HOLD_MS + 1000,
);
await call('/api/mapillary/tiles', '/coverage/14/6/6');
assert.equal(calls.length, 2, 'asked again once the capped hold ends');
},
);
});
test('normalizeTileAddress enforces layer names, zoom ranges and tile bounds', () => {
assert.equal(
normalizeTileAddress({ layer: 'coverage', z: 3, x: 1, y: 2 }).key,
@@ -601,6 +642,45 @@ test('the upstream tile fetch is cancelled once every waiter has left', async ()
});
});
test('a request arriving before a cancelled fetch has wound down starts a fresh one', async () => {
const tile = { layer: 'coverage', z: 13, x: 130, y: 100 };
await withDeferredUpstream([tile], async ({ body, settle }) => {
// A real fetch rejects some time after its abort; until then the
// cancelled flight is still registered for its tile.
const upstream = [];
globalThis.fetch = (url, { signal } = {}) =>
new Promise((resolve, reject) => {
const call = { aborted: false };
call.release = () => resolve(new Response(body, { status: 200 }));
call.windDown = () => reject(signal.reason);
signal?.addEventListener('abort', () => (call.aborted = true), {
once: true,
});
upstream.push(call);
});
const first = new AbortController();
const a = fetchTile(tile, { signal: first.signal });
await waitFor(() => upstream.length === 1, 'the upstream fetch');
first.abort();
await assert.rejects(a, { name: 'AbortError' });
assert.equal(upstream[0].aborted, true, 'the last waiter cancelled it');
// Joining the cancelled flight would end in its AbortError (a 502).
const b = fetchTile(tile);
await waitFor(() => upstream.length === 2, 'a fresh upstream fetch');
// The cancelled flight winding down must not unregister the fresh one.
upstream[0].windDown();
await settle();
const c = fetchTile(tile);
await settle();
assert.equal(upstream.length, 2, 'a third request joins the fresh fetch');
upstream[1].release();
const fresh = await b;
assert.equal(fresh.source, 'upstream');
assert.deepEqual(listTileLayers(fresh.bytes), ['sequence']);
assert.equal((await c).source, 'inflight');
});
});
// ── Route: success path, refusals, gate and limiter (review gekh) ──
/** Every header value and the body, as text, for a token-leak check. */
@@ -1012,6 +1092,126 @@ test('empty tiles are charged against the memory budget (review gekh)', async ()
);
});
/** A one-layer tile of exactly `size` bytes (a big opaque payload). */
function tileOfSize(size) {
const probe = tile([{ name: 'sequence', payload: Buffer.alloc(size) }]);
const bytes = tile([
{ name: 'sequence', payload: Buffer.alloc(2 * size - probe.length) },
]);
assert.equal(bytes.length, size);
return bytes;
}
/**
* Serve each address's big tile from a scripted upstream, with disk writes
* off: a tile memory dropped can only come back from Mapillary.
*/
async function withBigTiles(t, bodies, run) {
for (const address of bodies.keys())
await fsp.rm(tileFile(address), { force: true });
t.mock.method(fsp, 'writeFile', async () => {
throw new Error('disk writes are off in this test');
});
t.mock.method(console, 'warn', () => {});
const byPath = new Map(
[...bodies].map(([{ z, x, y }, body]) => [`/2/${z}/${x}/${y}?`, body]),
);
await withUpstream(
(_n, url) => {
const [, body] = [...byPath].find(([key]) => url.includes(key));
return new Response(body, { status: 200 });
},
({ calls }) => {
const fetched = (address) =>
calls.filter((url) =>
url.includes(`/2/${address.z}/${address.x}/${address.y}?`),
).length;
return run({ fetched });
},
);
}
test('the memory cache evicts the least recently used tile, not the oldest fetched', async (t) => {
const MB = 1024 * 1024;
// Three fit in the budget, a fourth does not.
const size = 30 * MB;
const cost = size + TILE_MEMORY_ENTRY_OVERHEAD_BYTES;
assert.ok(3 * cost <= TILE_MEMORY_BUDGET_BYTES);
assert.ok(4 * cost > TILE_MEMORY_BUDGET_BYTES);
const body = tileOfSize(size);
const [a, b, c, d] = [20, 21, 22, 23].map((x) => ({
layer: 'coverage',
z: 14,
x,
y: 20,
}));
await withBigTiles(
t,
new Map([a, b, c, d].map((address) => [address, body])),
async ({ fetched }) => {
for (const address of [a, b, c])
assert.equal((await fetchTile(address)).source, 'upstream');
assert.deepEqual(tiles._tileMemoryForTest(), {
entries: 3,
bytes: 3 * cost,
});
// A is used again, so B is now the least recently used.
assert.equal((await fetchTile(a)).source, 'memory');
assert.equal((await fetchTile(d)).source, 'upstream');
assert.deepEqual(tiles._tileMemoryForTest(), {
entries: 3,
bytes: 3 * cost,
});
for (const address of [a, c, d])
assert.equal(
(await fetchTile(address)).source,
'memory',
`${address.x} kept`,
);
// B was evicted; asking for it again goes back to Mapillary.
assert.equal((await fetchTile(b)).source, 'upstream');
assert.equal(fetched(b), 2);
assert.equal(fetched(a), 1);
},
);
});
test('a tile costing more than half the memory budget is served but not kept', async (t) => {
const half = TILE_MEMORY_BUDGET_BYTES / 2;
// At exactly half the budget a tile is kept; one byte more and it is not.
const fits = tileOfSize(half - TILE_MEMORY_ENTRY_OVERHEAD_BYTES);
const tooBig = tileOfSize(half - TILE_MEMORY_ENTRY_OVERHEAD_BYTES + 1);
assert.ok(tooBig.length <= TILE_MAX_BYTES, 'within the upstream size cap');
const small = { layer: 'coverage', z: 14, x: 24, y: 20 };
const kept = { layer: 'coverage', z: 14, x: 25, y: 20 };
const big = { layer: 'coverage', z: 14, x: 26, y: 20 };
await withBigTiles(
t,
new Map([
[small, tile([{ name: 'sequence' }])],
[kept, fits],
[big, tooBig],
]),
async ({ fetched }) => {
await fetchTile(small);
const before = tiles._tileMemoryForTest();
const served = await fetchTile(big);
assert.equal(served.source, 'upstream');
assert.equal(served.bytes.length, tooBig.length, 'served in full');
assert.deepEqual(
tiles._tileMemoryForTest(),
before,
'nothing evicted to make room',
);
assert.equal((await fetchTile(small)).source, 'memory');
assert.equal((await fetchTile(big)).source, 'upstream', 'not kept');
assert.equal(fetched(big), 2);
assert.equal((await fetchTile(kept)).source, 'upstream');
assert.equal((await fetchTile(kept)).source, 'memory', 'half fits');
},
);
});
test('empty tile files count toward the disk cap (review gekh)', async () => {
await withCacheDir(async (dir) => {
const files = [3, 2, 1].map((age) => ({