From ce985768bc0f788c3c4099e9de428e4fbb2871c3 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Fri, 16 Oct 2026 15:06:15 +0400 Subject: [PATCH] test(qa): wheel floor against the floor's own ground; polite tile cache The Navara wheel check measures clearance against engine.groundFloor's ground (threshold 29 m: the floor keeps 30 m), so tiles streaming under a still camera cannot flake it. The tile cache identifies itself with the repository URL, keeps at most two misses in flight and merges identical ones. --- scripts/qa/tileCache.mjs | 52 ++++++++++++++++++++++++++++++++-- src/tooling/tileCache.test.mjs | 44 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/scripts/qa/tileCache.mjs b/scripts/qa/tileCache.mjs index b345e96e5..ec1aba3ff 100644 --- a/scripts/qa/tileCache.mjs +++ b/scripts/qa/tileCache.mjs @@ -16,6 +16,11 @@ export const CACHED_TILE_HOSTS = Object.freeze([ 'terrain.reearth.land', ]); const RETRIES = 4; +/** Who is asking, with a contact URL (the tile hosts' usage policies). */ +export const QA_USER_AGENT = + 'gods-eye-view-qa/1 (headless tests with a disk cache; https://github.com/bilawalsidhu/gods-eye-view)'; +/** Fetches to the tile hosts in flight at once. */ +export const MAX_MISSES_IN_FLIGHT = 2; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); /** @@ -59,7 +64,7 @@ export async function fetchTile( for (let attempt = 0; ; attempt++) { const response = await fetchImpl(url, { headers: { - 'User-Agent': 'gods-eye-view-qa (headless test, cached)', + 'User-Agent': QA_USER_AGENT, ...(accept ? { Accept: accept } : {}), }, }); @@ -73,6 +78,48 @@ export async function fetchTile( } } +/** + * Loads missing tiles politely: at most MAX_MISSES_IN_FLIGHT fetches at + * once (the rest queue), and one fetch for identical misses in flight (same + * URL and Accept). `load(url, accept)` resolves to fetchTile's result. + */ +export function createMissLoader({ + fetchImpl = fetch, + wait = sleep, + maxInFlight = MAX_MISSES_IN_FLIGHT, +} = {}) { + const pending = new Map(); + const queue = []; + let running = 0; + const next = () => { + while (running < maxInFlight && queue.length) { + const job = queue.shift(); + running++; + fetchTile(job.url, { accept: job.accept, fetchImpl, wait }) + .then(job.resolve, job.reject) + .finally(() => { + running--; + pending.delete(job.key); + next(); + }); + } + }; + return { + load(url, accept) { + const key = `${url}\n${accept ?? ''}`; + let promise = pending.get(key); + if (!promise) { + promise = new Promise((resolve, reject) => + queue.push({ key, url, accept, resolve, reject }), + ); + pending.set(key, promise); + next(); + } + return promise; + }, + }; +} + /** * Serves the page's requests to CACHED_TILE_HOSTS from the cache (filling * it on a miss). Returns the counts. @@ -84,6 +131,7 @@ export async function installTileCache( fs.mkdirSync(dir, { recursive: true }); const stats = { hits: 0, fetched: 0, cancelled: 0, failed: 0, errors: {} }; const client = await page.createCDPSession(); + const misses = createMissLoader(); client.on('Fetch.requestPaused', async ({ requestId, request }) => { const accept = Object.entries(request.headers ?? {}).find( ([name]) => name.toLowerCase() === 'accept', @@ -101,7 +149,7 @@ export async function installTileCache( }; stats.hits++; } else { - const tile = await fetchTile(request.url, { accept }); + const tile = await misses.load(request.url, accept); // Only successes are cached; a 404 (no tile there) is passed on. if (tile.status === 200) { fs.writeFileSync(file, tile.body); diff --git a/src/tooling/tileCache.test.mjs b/src/tooling/tileCache.test.mjs index 19ffcb879..5674f06b1 100644 --- a/src/tooling/tileCache.test.mjs +++ b/src/tooling/tileCache.test.mjs @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { backoffMs, + createMissLoader, fetchTile, tileCacheFile, } from '../../scripts/qa/tileCache.mjs'; @@ -69,3 +70,46 @@ test('a tile fetch retries 429s, stops after four retries, keeps Accept', async assert.equal(limited.status, 429); assert.equal(calls, 5); }); + +test('misses: at most two fetches in flight, identical ones merged, a contact UA', async () => { + let inFlight = 0; + let most = 0; + const calls = []; + const releases = []; + const loader = createMissLoader({ + fetchImpl: (url, init) => { + calls.push([url, init.headers['User-Agent']]); + inFlight++; + most = Math.max(most, inFlight); + return new Promise((resolve) => + releases.push(() => { + inFlight--; + resolve(response(200, { 'content-type': 'image/png' })); + }), + ); + }, + wait: async () => {}, + }); + const loads = [ + loader.load('a', null), + loader.load('a', null), + loader.load('b', null), + loader.load('c', null), + loader.load('a', 'image/webp'), + ]; + for (let round = 0; round < 10 && releases.length; round++) { + await new Promise((resolve) => setTimeout(resolve, 0)); + releases.splice(0).forEach((release) => release()); + await new Promise((resolve) => setTimeout(resolve, 0)); + } + const tiles = await Promise.all(loads); + assert.ok(tiles.every((tile) => tile.status === 200)); + assert.equal(tiles[0], tiles[1], 'one fetch for two identical misses'); + assert.equal(calls.length, 4, 'a, b, c and a with another Accept'); + assert.ok(most <= 2, `${most} fetches in flight`); + assert.ok( + calls.every(([, agent]) => + agent.includes('https://github.com/bilawalsidhu/gods-eye-view'), + ), + ); +});