mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
From gekh's review of #768, each bug with a test that fails before: - P2: a failed sequence load clears the previous sequence's cones, resets the selection and can be retried; a second click while loading starts no second lookup. sequences.js had no tests: sequences.test.mjs now covers failure and retry, a superseded late answer, the cache and clearSelection. - P2: closing a photo stops MapillaryJS sequence playback, which kept the hidden viewer stepping and downloading images. - openNearest without the panel reports it instead of loading forever, and stops if the layer (or its provider) went off during the lookup; the mesh sampler's misses are bounded and forgotten with its samples; the sequence count is the union of ids across tiles; a provider withdraws only its own error, so a sequence error cannot mask KEY REJECTED; hover stays on (throttled) under an orbiting camera and re-picks when it stops; "since N days" follows the clock in a long-open tab. - A prewarm waits for one in flight and then builds if it gave up. - New coverage for the layer core (credits, KEY REJECTED vs REQUIRED, switching a provider off), the refresh wiring of the tilted-view fix and the draped-to-cast swap completion. - Cleanup: loading is derived from pending; freshStreet(); tile ranking computes distances once and shares one path for the date line; shared metresBetween / whenIdle / redrawTile; one NEAREST_RADIUS_M and one source method list; unused contract fields, exports, getImage and the decoder's image branch removed; PROVIDER_COLORS lists only Mapillary. Co-Authored-By: Claude Opus 5.5 <[email protected]>
149 lines
5.1 KiB
JavaScript
149 lines
5.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createMapillarySource, MapillarySourceError } from './source.js';
|
|
|
|
function fakeFetch(handler) {
|
|
const calls = [];
|
|
const fetchImpl = async (url, init = {}) => {
|
|
calls.push({ url: String(url), init });
|
|
return handler(String(url), init);
|
|
};
|
|
return { calls, fetchImpl };
|
|
}
|
|
|
|
const jsonResponse = (payload, status = 200) =>
|
|
new Response(JSON.stringify(payload), {
|
|
status,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
|
|
test('tiles go through the local proxy and never carry the token', async () => {
|
|
const { calls, fetchImpl } = fakeFetch(
|
|
() => new Response(new Uint8Array([1, 2, 3]), { status: 200 }),
|
|
);
|
|
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
|
|
const bytes = await source.getTile('coverage', 14, 2662, 6285);
|
|
assert.deepEqual([...bytes], [1, 2, 3]);
|
|
assert.equal(calls[0].url, '/api/mapillary/tiles/coverage/14/2662/6285');
|
|
assert.doesNotMatch(calls[0].url, /MLY/);
|
|
});
|
|
|
|
test('an empty tile (204) is an empty byte array, not an error', async () => {
|
|
const { fetchImpl } = fakeFetch(() => new Response(null, { status: 204 }));
|
|
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
|
|
assert.equal((await source.getTile('coverage', 14, 1, 1)).length, 0);
|
|
});
|
|
|
|
test('a missing key on the proxy surfaces as keyRequired', async () => {
|
|
const { fetchImpl } = fakeFetch(() =>
|
|
jsonResponse({ error: 'no_key', keyRequired: true }, 503),
|
|
);
|
|
const source = createMapillarySource({ token: '', fetchImpl });
|
|
assert.equal(source.hasToken(), false);
|
|
await assert.rejects(
|
|
() => source.getTile('coverage', 14, 1, 1),
|
|
(error) =>
|
|
error instanceof MapillarySourceError &&
|
|
error.status === 503 &&
|
|
error.keyRequired === true,
|
|
);
|
|
});
|
|
|
|
test('graph lookups call graph.mapillary.com with the client token and requested fields', async () => {
|
|
const { calls, fetchImpl } = fakeFetch(() =>
|
|
jsonResponse({ data: [{ id: '1', geometry: { coordinates: [1, 2] } }] }),
|
|
);
|
|
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
|
|
const images = await source.getSequenceImages('seq-1');
|
|
assert.equal(images.length, 1);
|
|
const url = new URL(calls[0].url);
|
|
assert.equal(url.host, 'graph.mapillary.com');
|
|
assert.equal(url.pathname, '/images');
|
|
assert.equal(url.searchParams.get('access_token'), 'MLY|1|abc');
|
|
assert.equal(url.searchParams.get('sequence_ids'), 'seq-1');
|
|
assert.match(url.searchParams.get('fields'), /geometry/);
|
|
|
|
const nearest = await source.nearestImages({
|
|
lat: 38.5,
|
|
lon: -121.5,
|
|
radius: 500,
|
|
limit: 500,
|
|
});
|
|
assert.equal(nearest.length, 1);
|
|
const nearestUrl = new URL(calls[1].url);
|
|
assert.equal(
|
|
nearestUrl.searchParams.get('radius'),
|
|
'50',
|
|
'radius is clamped to the API maximum',
|
|
);
|
|
assert.equal(nearestUrl.searchParams.get('limit'), '100');
|
|
});
|
|
|
|
test('graph calls without a token fail fast as keyRequired without a request', async () => {
|
|
const { calls, fetchImpl } = fakeFetch(() => jsonResponse({}));
|
|
const source = createMapillarySource({ token: '', fetchImpl });
|
|
await assert.rejects(
|
|
() => source.getSequenceImages('123'),
|
|
(error) =>
|
|
error instanceof MapillarySourceError && error.keyRequired === true,
|
|
);
|
|
assert.equal(calls.length, 0);
|
|
});
|
|
|
|
test('graph API errors carry the upstream message and status', async () => {
|
|
const { fetchImpl } = fakeFetch(() =>
|
|
jsonResponse({ error: { message: 'Invalid OAuth 2.0 Access Token' } }, 400),
|
|
);
|
|
const source = createMapillarySource({ token: 'MLY|1|abc', fetchImpl });
|
|
await assert.rejects(
|
|
() => source.getSequenceImages('999'),
|
|
(error) =>
|
|
error instanceof MapillarySourceError &&
|
|
error.status === 400 &&
|
|
/Invalid OAuth/.test(error.message),
|
|
);
|
|
});
|
|
|
|
test('the source exposes only imagery lookups', () => {
|
|
const source = createMapillarySource({ token: 'MLY|1|abc' });
|
|
for (const gone of ['queryFeatures', 'plan', 'geocode', 'spriteUrl'])
|
|
assert.equal(source[gone], undefined, gone);
|
|
});
|
|
|
|
test('a rejected key and a rate limit are told apart from other tile errors (review IC8 P2)', async () => {
|
|
const rejectedSource = createMapillarySource({
|
|
token: 'MLY|1|abc',
|
|
fetchImpl: fakeFetch(() =>
|
|
jsonResponse(
|
|
{ error: 'Mapillary rejected the access token', keyRejected: true },
|
|
403,
|
|
),
|
|
).fetchImpl,
|
|
});
|
|
await assert.rejects(
|
|
() => rejectedSource.getTile('coverage', 14, 1, 1),
|
|
(error) =>
|
|
error.keyRejected === true &&
|
|
error.keyRequired === false &&
|
|
error.retryAfterSec === null,
|
|
);
|
|
const limitedSource = createMapillarySource({
|
|
token: 'MLY|1|abc',
|
|
fetchImpl: fakeFetch(
|
|
() =>
|
|
new Response(
|
|
JSON.stringify({ error: 'rate limited', retryAfter: 45 }),
|
|
{
|
|
status: 429,
|
|
headers: { 'retry-after': '30' },
|
|
},
|
|
),
|
|
).fetchImpl,
|
|
});
|
|
await assert.rejects(
|
|
() => limitedSource.getTile('coverage', 14, 1, 1),
|
|
(error) => error.retryAfterSec === 30 && error.keyRejected === false,
|
|
'the Retry-After header wins over the body',
|
|
);
|
|
});
|