mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
Merge main into PR #948
This commit is contained in:
commit
80449af6bd
780 files changed
+62080
-20404
No files matched your search
@@ -98,7 +98,7 @@ test('standalone composition mounts every extracted provider exactly once withou
|
||||
assert.equal(plugins.filter((p) => p.name === factory().name).length, 1);
|
||||
});
|
||||
|
||||
test('terrain middleware chunks missing points and reconstructs repeated/reordered requests from cache', async (t) => {
|
||||
test('terrain middleware bounds missing points and reconstructs repeated/reordered requests from cache', async (t) => {
|
||||
isolate(t);
|
||||
let calls = 0;
|
||||
const sizes = [];
|
||||
@@ -116,25 +116,23 @@ test('terrain middleware chunks missing points and reconstructs repeated/reorder
|
||||
});
|
||||
});
|
||||
const request = install(terrainHeightsProxy());
|
||||
const points = Array.from({ length: 257 }, (_, i) => `${i / 100},1`);
|
||||
const points = Array.from({ length: 64 }, (_, i) => `${i / 100},1`);
|
||||
const res = await request('/?points=' + points.join(';'));
|
||||
assert.equal(res.status, 200);
|
||||
// Tracks UPSTREAM_CHUNK in server/providers/terrain.js (64): 257 points
|
||||
// split into four full chunks and a remainder.
|
||||
assert.deepEqual(sizes, [64, 64, 64, 64, 1]);
|
||||
const reordered = await request('/?points=2.56,1;0,1;2.56,1');
|
||||
assert.deepEqual(sizes, [64]);
|
||||
const reordered = await request('/?points=0.63,1;0,1;0.63,1');
|
||||
assert.deepEqual(json(reordered), {
|
||||
results: [{ ellipsoid: 102.56 }, { ellipsoid: 100 }, { ellipsoid: 102.56 }],
|
||||
results: [{ ellipsoid: 100.63 }, { ellipsoid: 100 }, { ellipsoid: 100.63 }],
|
||||
});
|
||||
// Five chunks for the first batch; the reordered request is fully cached.
|
||||
assert.equal(calls, 5);
|
||||
// One bounded batch; the reordered request is fully cached.
|
||||
assert.equal(calls, 1);
|
||||
assert.equal((await request('/?points=invalid')).status, 400);
|
||||
assert.equal(
|
||||
(await request('/?points=' + Array(2001).fill('0,1').join(';'))).status,
|
||||
500,
|
||||
413,
|
||||
);
|
||||
// Rejected requests add no upstream calls: still the five from the first batch.
|
||||
assert.equal(calls, 5);
|
||||
// Rejected requests add no upstream calls.
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
test('terrain middleware migrates valid legacy disk points without fabricating omitted heights', async (t) => {
|
||||
@@ -160,6 +158,7 @@ test('terrain middleware migrates valid legacy disk points without fabricating o
|
||||
test('traffic middleware preserves keyless mode, caching, stale budget fallback and UTC rollover', async (t) => {
|
||||
isolate(t, { TOMTOM_API_KEY: '', TOMTOM_DAILY_TILE_BUDGET: '1' });
|
||||
let now = Date.UTC(2026, 8, 12, 12);
|
||||
const fetchedAt = String(now);
|
||||
t.mock.method(Date, 'now', () => now);
|
||||
let calls = 0;
|
||||
t.mock.method(globalThis, 'fetch', async (raw) => {
|
||||
@@ -184,10 +183,10 @@ test('traffic middleware preserves keyless mode, caching, stale budget fallback
|
||||
assert.equal(json(await request('/status')).dailyCount, 1);
|
||||
assert.equal(calls, 1);
|
||||
now += 120001;
|
||||
assert.equal(
|
||||
(await request('/flow/8/1/1.pbf')).headers['x-tomtom-cache'],
|
||||
'STALE-BUDGET',
|
||||
);
|
||||
const staleTile = await request('/flow/8/1/1.pbf');
|
||||
assert.equal(staleTile.headers['x-tomtom-cache'], 'STALE-BUDGET');
|
||||
// The stale copy still says when TomTom produced it.
|
||||
assert.equal(staleTile.headers['x-tomtom-fetched-at'], fetchedAt);
|
||||
assert.equal((await request('/flow/8/2/1.pbf')).status, 429);
|
||||
assert.equal(calls, 1);
|
||||
now += 86400000;
|
||||
@@ -388,21 +387,26 @@ test('terrain middleware retains successful chunks around a failure and retries
|
||||
});
|
||||
const request = install(terrainHeightsProxy());
|
||||
const points = Array.from({ length: 130 }, (_, i) => `${i / 100},1`);
|
||||
const url = '/?points=' + points.join(';');
|
||||
assert.equal((await request(url)).status, 502);
|
||||
const urls = [0, 64, 128].map(
|
||||
(start) => '/?points=' + points.slice(start, start + 64).join(';'),
|
||||
);
|
||||
assert.deepEqual(
|
||||
requests.map((p) => p.length),
|
||||
[64, 64, 2],
|
||||
await Promise.all(urls.map(async (url) => (await request(url)).status)),
|
||||
[200, 502, 200],
|
||||
);
|
||||
assert.deepEqual(
|
||||
requests.map((p) => p.length).sort((a, b) => a - b),
|
||||
[2, 64, 64],
|
||||
);
|
||||
fail = false;
|
||||
const recovered = await request(url);
|
||||
assert.equal(recovered.status, 200);
|
||||
const recovered = await Promise.all(urls.map((url) => request(url)));
|
||||
assert.ok(recovered.every((response) => response.status === 200));
|
||||
assert.deepEqual(
|
||||
requests[3],
|
||||
Array.from({ length: 64 }, (_, i) => [(i + 64) / 100, 1]),
|
||||
);
|
||||
assert.deepEqual(
|
||||
json(recovered).results,
|
||||
recovered.flatMap((response) => json(response).results),
|
||||
Array.from({ length: 130 }, (_, i) => ({ ellipsoid: i / 100 + 100 })),
|
||||
);
|
||||
assert.equal(requests.length, 4);
|
||||
|
||||
@@ -156,13 +156,18 @@ test('self package imports cannot evade portable ownership', (t) => {
|
||||
assert.throws(() => checkImportDirections(root), /Source imports rendering/);
|
||||
});
|
||||
|
||||
test('compatibility exceptions are limited to the two existing composition entries', (t) => {
|
||||
test('the compatibility exception is limited to the existing composition entry', (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write('src/ui.js', "import './standalone/catalog.js';");
|
||||
write('src/standalone/catalog.js');
|
||||
write('server/providers/local.js', "import '../standalone/key-setup.js';");
|
||||
write('server/standalone/key-setup.js');
|
||||
assert.doesNotThrow(() => checkImportDirections(root));
|
||||
write('src/ui.js', "import './standalone/catalog.js';");
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/Reusable module imports standalone/,
|
||||
);
|
||||
write('src/ui.js');
|
||||
write('src/ui/new.js', "import '../standalone/catalog.js';");
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
|
||||
@@ -583,3 +583,205 @@ test('track backfill proxy returns 502 on an oversized upstream body and caches
|
||||
assert.equal(res2.statusCode, 502);
|
||||
assert.equal(callCount, 1);
|
||||
});
|
||||
|
||||
test('OpenSky Basic mode no longer sends username and password: it asks anonymously and warns once', async (t) => {
|
||||
environment(t, {
|
||||
OPENSKY_CLIENT_ID: undefined,
|
||||
OPENSKY_CLIENT_SECRET: undefined,
|
||||
OPENSKY_CREDENTIALS_FILE: undefined,
|
||||
OPENSKY_AUTH_MODE: 'basic',
|
||||
OPENSKY_USERNAME: 'fixture-user',
|
||||
OPENSKY_PASSWORD: 'fixture-password',
|
||||
});
|
||||
const warnings = [];
|
||||
t.mock.method(console, 'warn', (message) => warnings.push(message));
|
||||
const authorizations = [];
|
||||
t.mock.method(globalThis, 'fetch', async (url, options) => {
|
||||
if (url.includes('/states/')) {
|
||||
authorizations.push(options.headers.Authorization);
|
||||
return Response.json({ time: Math.floor(Date.now() / 1000), states: [] });
|
||||
}
|
||||
throw Error(`Unexpected URL: ${url}`);
|
||||
});
|
||||
const fresh = await import(
|
||||
`../../server/providers/aircraft/opensky.js?basic=${Math.random()}`
|
||||
);
|
||||
const flights = install(fresh.openSkyProxy());
|
||||
const first = await flights('/api/flights');
|
||||
assert.equal(first.statusCode, 200);
|
||||
assert.equal(first.headers['x-opensky-auth'], 'anon');
|
||||
assert.equal(first.headers['x-opensky-auth-reason'], 'basic_retired');
|
||||
assert.deepEqual(authorizations, [undefined]);
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.match(warnings[0], /retired username\/password sign-in/);
|
||||
assert.ok(!warnings[0].includes('fixture-password'));
|
||||
});
|
||||
|
||||
test('OpenSky auto mode never falls back to Basic, even when OAuth is rejected', async (t) => {
|
||||
environment(t, {
|
||||
OPENSKY_CLIENT_ID: 'fixture-client',
|
||||
OPENSKY_CLIENT_SECRET: 'fixture-secret',
|
||||
OPENSKY_CREDENTIALS_FILE: undefined,
|
||||
OPENSKY_AUTH_MODE: 'auto',
|
||||
OPENSKY_USERNAME: 'fixture-user',
|
||||
OPENSKY_PASSWORD: 'fixture-password',
|
||||
});
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
t.mock.method(console, 'log', () => {});
|
||||
t.mock.method(console, 'error', () => {});
|
||||
const authorizations = [];
|
||||
t.mock.method(globalThis, 'fetch', async (url, options) => {
|
||||
if (url.includes('/token'))
|
||||
return Response.json({ access_token: 'fixture-token', expires_in: 1800 });
|
||||
if (url.includes('/states/')) {
|
||||
authorizations.push(options.headers.Authorization);
|
||||
return new Response('', { status: 401 });
|
||||
}
|
||||
if (url.includes('/lat/')) return regionalFallback();
|
||||
throw Error(`Unexpected URL: ${url}`);
|
||||
});
|
||||
const fresh = await import(
|
||||
`../../server/providers/aircraft/opensky.js?auto=${Math.random()}`
|
||||
);
|
||||
await install(fresh.openSkyProxy())('/api/flights', '?lat=30&lon=-97');
|
||||
assert.deepEqual(authorizations, ['Bearer fixture-token']);
|
||||
});
|
||||
|
||||
test('retired military history refuses without upstream I/O while validating identifiers', async (t) => {
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
assert.fail('retired history must not contact the upstream');
|
||||
});
|
||||
const route = install(providers.trackBackfillProxies());
|
||||
for (const hex of ['abc123', 'abc456']) {
|
||||
const response = await route('/api/military/track', '?hex=' + hex);
|
||||
assert.equal(response.statusCode, 503);
|
||||
assert.equal(JSON.parse(response.body).code, 'history_unavailable');
|
||||
assert.equal(response.headers['retry-after'], '1800');
|
||||
assert.equal(response.headers['cache-control'], 'no-store');
|
||||
}
|
||||
assert.equal(
|
||||
(await route('/api/military/track', '?hex=invalid')).statusCode,
|
||||
400,
|
||||
);
|
||||
});
|
||||
|
||||
test('global aircraft snapshots coalesce cold and expired callers and clear rejected flights', async (t) => {
|
||||
environment(t, { OPENSKY_AUTH_MODE: 'anon' });
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
t.mock.method(console, 'error', () => {});
|
||||
let now = Date.now();
|
||||
t.mock.method(Date, 'now', () => now);
|
||||
for (const kind of ['opensky', 'military']) {
|
||||
const module =
|
||||
kind === 'opensky'
|
||||
? await import(
|
||||
`../../server/providers/aircraft/opensky.js?coalesce=${Math.random()}`
|
||||
)
|
||||
: await import('../../server/providers/aircraft/adsb-lol.js');
|
||||
const request = install(
|
||||
kind === 'opensky' ? module.openSkyProxy() : module.adsbLolProxy(),
|
||||
);
|
||||
const route = kind === 'opensky' ? '/api/flights' : '/api/military';
|
||||
let calls = 0;
|
||||
let release;
|
||||
let blocked;
|
||||
let fail = false;
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
calls++;
|
||||
await blocked;
|
||||
if (fail) throw new Error('fixture connection failed');
|
||||
return Response.json(
|
||||
{ time: now / 1000, states: [], ac: [] },
|
||||
{ headers: { 'x-rate-limit-remaining': '5000' } },
|
||||
);
|
||||
});
|
||||
for (const phase of ['cold', 'expired', 'failed', 'recovered']) {
|
||||
now += 13000;
|
||||
fail = phase === 'failed';
|
||||
blocked = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const before = calls;
|
||||
const pending = Array.from({ length: 5 }, () => request(route));
|
||||
await new Promise(setImmediate);
|
||||
assert.equal(calls - before, 1, `${kind} ${phase}: one refresh starts`);
|
||||
release();
|
||||
const responses = await Promise.all(pending);
|
||||
assert.equal(calls - before, fail && kind === 'opensky' ? 2 : 1);
|
||||
assert.ok(responses.every((res) => res.statusCode === 200));
|
||||
assert.ok(responses.every((res) => res.body === responses[0].body));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('OpenSky 5xx retains dated last-good data, cancels error bodies and cools down before recovery', async (t) => {
|
||||
environment(t, { OPENSKY_AUTH_MODE: 'anon' });
|
||||
let now = Date.now();
|
||||
t.mock.method(Date, 'now', () => now);
|
||||
for (const status of [502, 503, 504]) {
|
||||
const module = await import(
|
||||
`../../server/providers/aircraft/opensky.js?server-error=${status}`
|
||||
);
|
||||
const request = install(module.openSkyProxy());
|
||||
let fail = false;
|
||||
let calls = 0;
|
||||
let cancelled = false;
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
calls++;
|
||||
if (fail)
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
},
|
||||
}),
|
||||
{ status },
|
||||
);
|
||||
return Response.json(
|
||||
{ time: now / 1000, states: [] },
|
||||
{ headers: { 'x-rate-limit-remaining': '5000' } },
|
||||
);
|
||||
});
|
||||
const good = await request('/api/flights');
|
||||
now += 10000;
|
||||
fail = true;
|
||||
const stale = await request('/api/flights');
|
||||
assert.equal(stale.statusCode, 200);
|
||||
assert.equal(stale.body, good.body);
|
||||
assert.equal(stale.headers['x-opensky-cache'], 'STALE');
|
||||
assert.equal(
|
||||
stale.headers['x-opensky-auth-reason'],
|
||||
`upstream_http_${status}_serving_stale`,
|
||||
);
|
||||
assert.equal(stale.headers['x-opensky-stale-seconds'], '10');
|
||||
assert.equal(cancelled, true);
|
||||
const cooling = await request('/api/flights');
|
||||
assert.equal(cooling.body, good.body);
|
||||
assert.equal(
|
||||
cooling.headers['x-opensky-auth-reason'],
|
||||
`upstream_http_${status}_serving_stale`,
|
||||
);
|
||||
assert.equal(calls, 2);
|
||||
now += 16000;
|
||||
fail = false;
|
||||
assert.equal(
|
||||
(await request('/api/flights')).headers['x-opensky-cache'],
|
||||
'MISS',
|
||||
);
|
||||
assert.equal(calls, 3);
|
||||
}
|
||||
const coldModule =
|
||||
await import('../../server/providers/aircraft/opensky.js?cold-server-error');
|
||||
let calls = 0;
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
calls++;
|
||||
return new Response('<html>failure</html>', { status: 503 });
|
||||
});
|
||||
const cold = install(coldModule.openSkyProxy());
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const response = await cold('/api/flights');
|
||||
assert.equal(response.statusCode, 503);
|
||||
assert.match(JSON.parse(response.body).error, /OpenSky/);
|
||||
}
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { promises as fsp } from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import { launchLibraryRecentUrl } from '../data/spaceProviderRequests.js';
|
||||
import {
|
||||
launchLibraryRequestHeaders,
|
||||
rocketLaunchesProxy,
|
||||
} from '../../server/providers/space/launch-library.js';
|
||||
import { keySetupStatus } from '../keySetupCore.mjs';
|
||||
import { initKeySetup } from '../keySetup.js';
|
||||
|
||||
test('LL2 requests from the same UTC minute share a cache key without mutating callers', () => {
|
||||
const first = new Date('2026-10-17T16:20:00.001Z');
|
||||
const last = new Date('2026-10-17T16:20:59.999Z');
|
||||
assert.equal(
|
||||
launchLibraryRecentUrl(first).href,
|
||||
launchLibraryRecentUrl(last).href,
|
||||
);
|
||||
assert.equal(first.toISOString(), '2026-10-17T16:20:00.001Z');
|
||||
assert.equal(last.toISOString(), '2026-10-17T16:20:59.999Z');
|
||||
});
|
||||
|
||||
for (const [date, end] of [
|
||||
['2026-10-17T16:20:00.001Z', '2026-10-17T16:20:00.000Z'],
|
||||
['2026-10-17T16:20:59.999Z', '2026-10-17T16:20:00.000Z'],
|
||||
['2026-10-17T16:21:00.000Z', '2026-10-17T16:21:00.000Z'],
|
||||
['2026-01-01T00:00:00.001Z', '2026-01-01T00:00:00.000Z'],
|
||||
]) {
|
||||
test(`LL2 bounds retain the 30-day UTC window at ${date}`, () => {
|
||||
const url = launchLibraryRecentUrl(new Date(date));
|
||||
assert.equal(url.origin, 'https://ll.thespacedevs.com');
|
||||
assert.equal(url.pathname, '/2.3.0/launches/');
|
||||
assert.equal(url.searchParams.get('net__lte'), end);
|
||||
assert.equal(
|
||||
Date.parse(end) - Date.parse(url.searchParams.get('net__gte')),
|
||||
30 * 86400000,
|
||||
);
|
||||
assert.equal(url.searchParams.get('limit'), '100');
|
||||
assert.equal(url.searchParams.get('mode'), 'detailed');
|
||||
});
|
||||
}
|
||||
|
||||
test('LL2 headers identify the app and preserve optional server-only authorization', () => {
|
||||
const publicHeaders = launchLibraryRequestHeaders('');
|
||||
assert.match(publicHeaders['User-Agent'], /^gods-eye-view.*\/1\.0 /);
|
||||
assert.ok(
|
||||
publicHeaders['User-Agent'].includes(
|
||||
'https://github.com/bilawalsidhu/gods-eye-view',
|
||||
),
|
||||
);
|
||||
assert.equal(publicHeaders.Accept, 'application/json');
|
||||
assert.equal(publicHeaders.Authorization, undefined);
|
||||
const paidHeaders = launchLibraryRequestHeaders(' owned-fixture-token ');
|
||||
assert.equal(paidHeaders.Authorization, 'Token owned-fixture-token');
|
||||
assert.equal(paidHeaders['User-Agent'], publicHeaders['User-Agent']);
|
||||
});
|
||||
|
||||
test('actual setup rendering distinguishes optional paid LL2 from unrelated free keys', async () => {
|
||||
const created = [];
|
||||
const node = () => {
|
||||
const value = {
|
||||
dataset: {},
|
||||
children: [],
|
||||
append(...children) {
|
||||
this.children.push(...children);
|
||||
},
|
||||
querySelector() {
|
||||
return null;
|
||||
},
|
||||
querySelectorAll() {
|
||||
return [];
|
||||
},
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
setAttribute() {},
|
||||
remove() {},
|
||||
};
|
||||
created.push(value);
|
||||
return value;
|
||||
};
|
||||
const rows = node();
|
||||
const root = node();
|
||||
root.querySelector = (selector) =>
|
||||
selector === '[data-key-setup-rows]' ? rows : null;
|
||||
const chip = node();
|
||||
const status = keySetupStatus({});
|
||||
status.keys = status.keys.filter((key) =>
|
||||
['launch-library', 'mapillary'].includes(key.id),
|
||||
);
|
||||
const setup = await initKeySetup({
|
||||
documentRef: {
|
||||
getElementById: (id) => (id === 'key-setup-chip' ? chip : root),
|
||||
createElement: node,
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
},
|
||||
fetchImpl: async () => ({ ok: true, json: async () => status }),
|
||||
});
|
||||
assert.ok(setup);
|
||||
try {
|
||||
const ll2 = rows.children.find(
|
||||
(row) => row.dataset.keyId === 'launch-library',
|
||||
);
|
||||
const free = rows.children.find((row) => row.dataset.keyId === 'mapillary');
|
||||
const tier = (row) =>
|
||||
row.children[0].children.find(
|
||||
(child) => child.className === 'key-setup-tier',
|
||||
);
|
||||
assert.match(tier(ll2).title, /optional paid token/i);
|
||||
assert.match(ll2.children[1].textContent, /leave blank/i);
|
||||
assert.match(ll2.children[1].textContent, /no key/i);
|
||||
assert.equal(tier(free).title, 'Free key — register, paste, done');
|
||||
const serialized = JSON.stringify(status);
|
||||
assert.ok(!serialized.includes('owned-fixture-token'));
|
||||
} finally {
|
||||
setup.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('real LL2 proxy sends cacheable headers and bounds, retaining its memory cache', async (t) => {
|
||||
for (const method of ['readFile', 'stat'])
|
||||
t.mock.method(fsp, method, async () => {
|
||||
throw new Error('owned fixture has no disk cache');
|
||||
});
|
||||
t.mock.method(fsp, 'mkdir', async () => {});
|
||||
t.mock.method(fsp, 'writeFile', async () => {});
|
||||
const calls = [];
|
||||
t.mock.method(globalThis, 'fetch', async (url, options) => {
|
||||
calls.push({ url: new URL(url), options });
|
||||
return new Response(
|
||||
JSON.stringify({ results: [{ id: 'owned-launch-fixture' }] }),
|
||||
);
|
||||
});
|
||||
let handler;
|
||||
rocketLaunchesProxy().configureServer({
|
||||
middlewares: {
|
||||
use(route, value) {
|
||||
assert.equal(route, '/api/launches');
|
||||
handler = value;
|
||||
},
|
||||
},
|
||||
});
|
||||
async function request() {
|
||||
const res = {
|
||||
writeHead(status, headers) {
|
||||
this.status = status;
|
||||
this.headers = headers;
|
||||
},
|
||||
end(body) {
|
||||
this.body = body;
|
||||
},
|
||||
};
|
||||
await handler({ method: 'GET' }, res);
|
||||
return res;
|
||||
}
|
||||
const first = await request();
|
||||
const second = await request();
|
||||
assert.equal(first.status, 200);
|
||||
assert.equal(second.body, first.body);
|
||||
assert.equal(second.headers['X-GEV-Cache'], 'HIT');
|
||||
assert.equal(calls.length, 1);
|
||||
assert.match(calls[0].options.headers['User-Agent'], /gods-eye-view/);
|
||||
assert.match(calls[0].url.searchParams.get('net__lte'), /:00\.000Z$/);
|
||||
assert.match(calls[0].url.searchParams.get('net__gte'), /:00\.000Z$/);
|
||||
});
|
||||
|
||||
for (const preview of [false, true]) {
|
||||
test(`LL2 proxy sends its identifying headers over real owned HTTP in ${preview ? 'preview' : 'development'}`, async (t) => {
|
||||
for (const method of ['readFile', 'stat'])
|
||||
t.mock.method(fsp, method, async () => {
|
||||
throw new Error('no owned cache');
|
||||
});
|
||||
t.mock.method(fsp, 'mkdir', async () => {});
|
||||
t.mock.method(fsp, 'writeFile', async () => {});
|
||||
const received = [];
|
||||
const upstream = http.createServer((req, res) => {
|
||||
received.push({
|
||||
url: new URL(req.url, 'http://fixture.invalid'),
|
||||
headers: req.headers,
|
||||
});
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ results: [{ id: 'owned-wire-fixture' }] }));
|
||||
});
|
||||
await new Promise((resolve) => upstream.listen(0, '127.0.0.1', resolve));
|
||||
const originalFetch = globalThis.fetch;
|
||||
t.mock.method(globalThis, 'fetch', (url, options) => {
|
||||
const parsed = new URL(url);
|
||||
assert.equal(parsed.origin, 'https://ll.thespacedevs.com');
|
||||
return originalFetch(
|
||||
`http://127.0.0.1:${upstream.address().port}${parsed.pathname}${parsed.search}`,
|
||||
options,
|
||||
);
|
||||
});
|
||||
let handler;
|
||||
const plugin = rocketLaunchesProxy();
|
||||
plugin[preview ? 'configurePreviewServer' : 'configureServer']({
|
||||
middlewares: {
|
||||
use(_route, value) {
|
||||
handler = value;
|
||||
},
|
||||
},
|
||||
});
|
||||
const proxy = http.createServer((req, res) => {
|
||||
void handler(req, res);
|
||||
});
|
||||
await new Promise((resolve) => proxy.listen(0, '127.0.0.1', resolve));
|
||||
try {
|
||||
const result = await originalFetch(
|
||||
`http://127.0.0.1:${proxy.address().port}/api/launches`,
|
||||
);
|
||||
assert.equal(result.status, 200);
|
||||
assert.deepEqual(await result.json(), {
|
||||
results: [{ id: 'owned-wire-fixture' }],
|
||||
});
|
||||
assert.equal(received.length, 1);
|
||||
assert.match(
|
||||
received[0].headers['user-agent'],
|
||||
/^gods-eye-view-launch-library-proxy\/1\.0 /,
|
||||
);
|
||||
assert.equal(received[0].headers.authorization, undefined);
|
||||
assert.match(received[0].url.searchParams.get('net__lte'), /:00\.000Z$/);
|
||||
} finally {
|
||||
proxy.closeAllConnections();
|
||||
upstream.closeAllConnections();
|
||||
await Promise.all([
|
||||
new Promise((resolve) => proxy.close(resolve)),
|
||||
new Promise((resolve) => upstream.close(resolve)),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
+659
-1273
File diff suppressed because it is too large.
Load diff
@@ -1,57 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
driftedEdges,
|
||||
growthDelta,
|
||||
handlePoint,
|
||||
MIN_SIZE,
|
||||
pinnedEdges,
|
||||
STORAGE_KEY,
|
||||
} from '../../scripts/qa-panel-resize.mjs';
|
||||
import { RESIZE_DIRECTIONS } from '../ui/panelResize.js';
|
||||
|
||||
test('the harness targets the key and minimums the controls use', () => {
|
||||
assert.equal(STORAGE_KEY, 'godsEyeView.v8.panelPos.cctv-panel');
|
||||
assert.deepEqual(MIN_SIZE, { width: 300, height: 160 });
|
||||
});
|
||||
|
||||
test('pinned edges are exactly the sides a direction does not name', () => {
|
||||
assert.deepEqual(pinnedEdges('n'), ['bottom', 'right', 'left']);
|
||||
assert.deepEqual(pinnedEdges('se'), ['top', 'left']);
|
||||
assert.deepEqual(pinnedEdges('nw'), ['bottom', 'right']);
|
||||
for (const dir of RESIZE_DIRECTIONS)
|
||||
assert.equal(pinnedEdges(dir).length, 4 - dir.length);
|
||||
});
|
||||
|
||||
test('growth deltas push each named edge outward', () => {
|
||||
assert.deepEqual(growthDelta('n', 40), { dx: 0, dy: -40 });
|
||||
assert.deepEqual(growthDelta('e', 40), { dx: 40, dy: 0 });
|
||||
assert.deepEqual(growthDelta('sw', 40), { dx: -40, dy: 40 });
|
||||
assert.deepEqual(growthDelta('se', 40), { dx: 40, dy: 40 });
|
||||
});
|
||||
|
||||
test('handle points land inside the strip or corner for every direction', () => {
|
||||
const rect = { left: 100, top: 200, width: 400, height: 300 };
|
||||
rect.right = rect.left + rect.width;
|
||||
rect.bottom = rect.top + rect.height;
|
||||
assert.deepEqual(handlePoint(rect, 'n'), { x: 300, y: 201 });
|
||||
assert.deepEqual(handlePoint(rect, 'w'), { x: 101, y: 350 });
|
||||
assert.deepEqual(handlePoint(rect, 'se'), { x: 496, y: 496 });
|
||||
assert.deepEqual(handlePoint(rect, 'nw'), { x: 104, y: 204 });
|
||||
for (const dir of RESIZE_DIRECTIONS) {
|
||||
const { x, y } = handlePoint(rect, dir);
|
||||
assert.ok(x >= rect.left && x <= rect.right, `${dir} x inside`);
|
||||
assert.ok(y >= rect.top && y <= rect.bottom, `${dir} y inside`);
|
||||
}
|
||||
});
|
||||
|
||||
test('drift detection reports only pinned edges that moved', () => {
|
||||
const before = { left: 100, top: 100, width: 400, height: 300 };
|
||||
const grewEast = { left: 100, top: 100, width: 440, height: 300 };
|
||||
assert.deepEqual(driftedEdges(before, grewEast, 'e'), []);
|
||||
const slidWest = { left: 60, top: 100, width: 440, height: 300 };
|
||||
assert.deepEqual(driftedEdges(before, slidWest, 'e'), ['left']);
|
||||
assert.deepEqual(driftedEdges(before, slidWest, 'w'), []);
|
||||
const wobble = { left: 101, top: 100, width: 439, height: 300 };
|
||||
assert.deepEqual(driftedEdges(before, wobble, 'e'), []);
|
||||
});
|
||||
@@ -81,6 +81,24 @@ test('nearby labels rank landmarks, deduplicate names/addresses and bound the pr
|
||||
assert.deepEqual(projectNearbyPlaces({}, 0, 0), []);
|
||||
});
|
||||
|
||||
test('nearby place distances take the short way round 180°', () => {
|
||||
const at = (name, longitude) => ({
|
||||
id: name,
|
||||
displayName: { text: name },
|
||||
location: { latitude: -16.8, longitude },
|
||||
types: ['point_of_interest'],
|
||||
});
|
||||
// From Taveuni at 179.98: 2 km east across 180°, 4 km west on this side.
|
||||
const result = projectNearbyPlaces(
|
||||
{ places: [at('West', 179.942), at('East', -179.999)] },
|
||||
-16.8,
|
||||
179.98,
|
||||
);
|
||||
const byName = Object.fromEntries(result.map((p) => [p.name, p.distanceM]));
|
||||
assert.ok(byName.East > 1500 && byName.East < 2500, String(byName.East));
|
||||
assert.ok(byName.West > 3500 && byName.West < 4500, String(byName.West));
|
||||
});
|
||||
|
||||
test('text search preserves bounds, rejects malformed bounds and tolerates absent locations', () => {
|
||||
const viewport = {
|
||||
low: { latitude: 1, longitude: 2 },
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import sharp from 'sharp';
|
||||
import {
|
||||
CUBE_FACES,
|
||||
FACE_FILE_SUFFIX,
|
||||
PLANETS,
|
||||
cubeFaceDirection,
|
||||
icrfToGalactic,
|
||||
} from '../../scripts/build-milky-way.mjs';
|
||||
import { raDecToUnitVector } from '../sky/stars.js';
|
||||
|
||||
const SKY = new URL('../data/local_data/sky/', import.meta.url);
|
||||
// Allowed excess of the painted disc over the surrounding sky, in 8-bit
|
||||
// levels of the encoded face: the blur and dither move values by a few.
|
||||
const TOLERANCE = 4;
|
||||
|
||||
test('painted-out planets leave no disc brighter than the sky around them', async () => {
|
||||
const centres = PLANETS.map((planet) => ({
|
||||
planet,
|
||||
vector: raDecToUnitVector(planet.l, planet.b),
|
||||
disc: [],
|
||||
ring: [],
|
||||
}));
|
||||
for (const face of CUBE_FACES) {
|
||||
const { data, info } = await sharp(
|
||||
new URL(`milkyway_${FACE_FILE_SUFFIX[face]}.webp`, SKY).pathname,
|
||||
)
|
||||
.raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
const size = info.width;
|
||||
for (let j = 0; j < size; j++) {
|
||||
for (let i = 0; i < size; i++) {
|
||||
const d = cubeFaceDirection(face, (i + 0.5) / size, (j + 0.5) / size);
|
||||
const n = Math.hypot(...d);
|
||||
const g = icrfToGalactic(d.map((x) => x / n));
|
||||
for (const centre of centres) {
|
||||
const cos =
|
||||
g[0] * centre.vector[0] +
|
||||
g[1] * centre.vector[1] +
|
||||
g[2] * centre.vector[2];
|
||||
const degrees = (Math.acos(Math.min(1, cos)) * 180) / Math.PI;
|
||||
const k = (j * size + i) * info.channels;
|
||||
const luma = (data[k] + data[k + 1] + data[k + 2]) / 3;
|
||||
if (degrees <= centre.planet.radiusDeg - 0.5) centre.disc.push(luma);
|
||||
else if (
|
||||
degrees >= centre.planet.radiusDeg + 1 &&
|
||||
degrees <= centre.planet.radiusDeg + 3.5
|
||||
)
|
||||
centre.ring.push(luma);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const median = (values) => values.sort((a, b) => a - b)[values.length >> 1];
|
||||
for (const { planet, disc, ring } of centres) {
|
||||
const discMean = disc.reduce((s, v) => s + v, 0) / disc.length;
|
||||
const ringMedian = median(ring);
|
||||
assert.ok(
|
||||
discMean <= ringMedian + TOLERANCE,
|
||||
`l=${planet.l} b=${planet.b}: disc mean ${discMean.toFixed(1)} vs surrounding sky ${ringMedian.toFixed(1)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
buildStarPack,
|
||||
parseBscRecord,
|
||||
} from '../../scripts/build-bright-stars.mjs';
|
||||
import {
|
||||
CUBE_FACES,
|
||||
cubeFaceDirection,
|
||||
directionToPlateCarree,
|
||||
icrfToGalactic,
|
||||
smoothstep,
|
||||
softBlack,
|
||||
} from '../../scripts/build-milky-way.mjs';
|
||||
import { raDecToUnitVector } from '../sky/stars.js';
|
||||
|
||||
// Verbatim CDS V/50 records: HR 1, Sirius (HR 2491) and a nova with no J2000
|
||||
// position. Columns matter, so the padding is part of the fixture.
|
||||
const HR_1 =
|
||||
' 1 BD+44 4550 3 36042 46 000001.1+444022000509.9+451345114.44-16.88 6.70 +0.07 +0.08 A1Vn -0.012-0.018 -018 195 4.2 21.6AC 3';
|
||||
const SIRIUS =
|
||||
'2491 9Alp CMaBD-16 1591 48915151881 257I 5423 064044.6-163444064508.9-164258227.22-08.88-1.46 0.00 -0.05 -0.03 A1Vm -0.553-1.205 +.375-008SBO 13 10.3 11.2AB 4*';
|
||||
const NOVA =
|
||||
' 92 NOVA 1572 B Cas *';
|
||||
|
||||
const close = (actual, expected, epsilon = 1e-9) =>
|
||||
assert.ok(
|
||||
Math.abs(actual - expected) <= epsilon,
|
||||
`${actual} is not within ${epsilon} of ${expected}`,
|
||||
);
|
||||
|
||||
test('BSC5 records parse J2000 position, V magnitude and B−V', () => {
|
||||
const sirius = parseBscRecord(SIRIUS);
|
||||
assert.equal(sirius.hr, 2491);
|
||||
close(sirius.raDeg, (6 + 45 / 60 + 8.9 / 3600) * 15);
|
||||
close(sirius.decDeg, -(16 + 42 / 60 + 58 / 3600));
|
||||
assert.equal(sirius.vmag, -1.46);
|
||||
assert.equal(sirius.bv, 0);
|
||||
const first = parseBscRecord(HR_1);
|
||||
close(first.raDeg, (5 / 60 + 9.9 / 3600) * 15);
|
||||
close(first.decDeg, 45 + 13 / 60 + 45 / 3600);
|
||||
assert.equal(first.vmag, 6.7);
|
||||
assert.equal(first.bv, 0.07);
|
||||
assert.equal(parseBscRecord(NOVA), null);
|
||||
});
|
||||
|
||||
test('the star pack is a flat, rounded column array that counts its drops', () => {
|
||||
const pack = buildStarPack([HR_1, NOVA, SIRIUS, ''].join('\n'), {
|
||||
fetched: '2026-10-19',
|
||||
});
|
||||
assert.deepEqual(
|
||||
pack.stars,
|
||||
[1.291, 45.229, 6.7, 0.07, 101.287, -16.716, -1.46, 0],
|
||||
);
|
||||
assert.equal(pack.meta.count, 2);
|
||||
assert.equal(pack.meta.dropped, 1);
|
||||
assert.equal(pack.meta.fetched, '2026-10-19');
|
||||
});
|
||||
|
||||
test('cube faces use the WebGL major-axis layout', () => {
|
||||
// Each face centre looks down its own axis …
|
||||
const axes = {
|
||||
positiveX: [1, 0, 0],
|
||||
negativeX: [-1, 0, 0],
|
||||
positiveY: [0, 1, 0],
|
||||
negativeY: [0, -1, 0],
|
||||
positiveZ: [0, 0, 1],
|
||||
negativeZ: [0, 0, -1],
|
||||
};
|
||||
for (const face of CUBE_FACES)
|
||||
assert.deepEqual(
|
||||
cubeFaceDirection(face, 0.5, 0.5).map((v) => v + 0),
|
||||
axes[face],
|
||||
);
|
||||
// … and the first texel row and column follow OpenGL ES 3.0 §3.8.10:
|
||||
// on +X, s grows towards −Z and t towards −Y.
|
||||
assert.deepEqual(cubeFaceDirection('positiveX', 0, 0), [1, 1, 1]);
|
||||
assert.deepEqual(cubeFaceDirection('positiveZ', 1, 1), [1, -1, 1]);
|
||||
assert.deepEqual(cubeFaceDirection('negativeY', 0, 1), [-1, -1, -1]);
|
||||
assert.throws(() => cubeFaceDirection('up', 0, 0), RangeError);
|
||||
});
|
||||
|
||||
test('plate carrée is centred on longitude 0, growing to the left, north up', () => {
|
||||
assert.deepEqual(directionToPlateCarree(1, 0, 0), [0.5, 0.5]);
|
||||
const sixHours = directionToPlateCarree(0, 1, 0);
|
||||
close(sixHours[0], 0.25);
|
||||
const eighteenHours = directionToPlateCarree(0, -1, 0);
|
||||
close(eighteenHours[0], 0.75);
|
||||
close(directionToPlateCarree(0, 0, 1)[1], 0);
|
||||
close(directionToPlateCarree(0, 0, -1)[1], 1);
|
||||
const twelveHours = directionToPlateCarree(-1, 0, 0);
|
||||
assert.ok(twelveHours[0] === 0 || twelveHours[0] === 1);
|
||||
});
|
||||
|
||||
test('the galactic rotation puts the Galactic Centre and pole where the IAU does', () => {
|
||||
// Galactic origin l = b = 0 at RA 266.405°, Dec −28.936° (J2000).
|
||||
const centre = icrfToGalactic(raDecToUnitVector(266.40499, -28.93617));
|
||||
close(centre[0], 1, 1e-6);
|
||||
// North galactic pole at RA 192.85948°, Dec +27.12825°.
|
||||
const pole = icrfToGalactic(raDecToUnitVector(192.85948, 27.12825));
|
||||
close(pole[2], 1, 1e-6);
|
||||
// ESO's panorama: the Large Magellanic Cloud (l ≈ 280.5°, b ≈ −32.9°) sits
|
||||
// right of and below the centre.
|
||||
const [u, v] = directionToPlateCarree(
|
||||
...icrfToGalactic(raDecToUnitVector(80.894, -69.756)),
|
||||
);
|
||||
close(u, 0.5 + (360 - 280.47) / 360, 2e-3);
|
||||
close(v, 0.5 + 32.89 / 180, 2e-3);
|
||||
});
|
||||
|
||||
test('the black level eases in instead of cutting the faint sky into contours', () => {
|
||||
assert.equal(softBlack(-0.2), 0);
|
||||
assert.equal(softBlack(0), 0);
|
||||
// Continuous, rising, with no step at the black level.
|
||||
let previous = 0;
|
||||
for (let x = 0.001; x <= 1; x += 0.001) {
|
||||
const y = softBlack(x);
|
||||
assert.ok(y > previous && y - previous < 0.0011, `step at ${x.toFixed(3)}`);
|
||||
previous = y;
|
||||
}
|
||||
// Starts flat, then joins the straight line just below it.
|
||||
assert.ok(softBlack(0.003) < 0.0003);
|
||||
close(softBlack(1), 1 - 0.03, 1e-3);
|
||||
});
|
||||
|
||||
test('the faint-glow blend weight eases between its edges', () => {
|
||||
assert.equal(smoothstep(0.1, 0.2, 0), 0);
|
||||
assert.equal(smoothstep(0.1, 0.2, 0.1), 0);
|
||||
close(smoothstep(0.1, 0.2, 0.15), 0.5, 1e-9);
|
||||
assert.equal(smoothstep(0.1, 0.2, 0.2), 1);
|
||||
assert.equal(smoothstep(0.1, 0.2, 9), 1);
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
launchLibraryRecentUrl,
|
||||
} from 'gods-eye-view/sources/space';
|
||||
import * as compatibility from '../../server/providers/local.js';
|
||||
import { createLaunchSource } from 'gods-eye-view/layers/launches/source';
|
||||
|
||||
function install(plugin, preview = false) {
|
||||
const routes = new Map();
|
||||
@@ -22,17 +23,23 @@ function install(plugin, preview = false) {
|
||||
},
|
||||
},
|
||||
});
|
||||
return async (route, url = '/', method = 'GET') => {
|
||||
return async (route, url = '/', method = 'GET', headers = {}) => {
|
||||
const res = {
|
||||
headersSent: false,
|
||||
headers: {},
|
||||
writeHead(status, headers) {
|
||||
Object.assign(this, { status, headers, headersSent: true });
|
||||
},
|
||||
// The same-site gates answer 403 through statusCode + setHeader.
|
||||
setHeader(name, value) {
|
||||
this.headers[name] = value;
|
||||
},
|
||||
end(body) {
|
||||
this.body = body;
|
||||
this.status ??= this.statusCode;
|
||||
},
|
||||
};
|
||||
await routes.get(route)({ url, method }, res);
|
||||
await routes.get(route)({ url, method, headers, socket: {} }, res);
|
||||
return res;
|
||||
};
|
||||
}
|
||||
@@ -81,8 +88,8 @@ test('portable requests keep fixed origins, encode group data and preserve the 3
|
||||
assert.equal(url.origin, 'https://ll.thespacedevs.com');
|
||||
assert.equal(url.pathname, '/2.3.0/launches/');
|
||||
assert.deepEqual(Object.fromEntries(url.searchParams), {
|
||||
net__gte: '2026-01-30T12:34:56.000Z',
|
||||
net__lte: '2026-03-01T12:34:56.000Z',
|
||||
net__gte: '2026-01-30T12:34:00.000Z',
|
||||
net__lte: '2026-03-01T12:34:00.000Z',
|
||||
limit: '100',
|
||||
mode: 'detailed',
|
||||
});
|
||||
@@ -161,7 +168,11 @@ test('CelesTrak answers 502 to an oversized TLE body without caching it', async
|
||||
assert.ok(bodies[0].pulled <= 17, 'reading stops at the 16 MiB cap');
|
||||
assert.equal(fsp.writeFile.mock.callCount(), 0);
|
||||
assert.equal((await request('/api/celestrak', '/stations')).status, 502);
|
||||
assert.equal(bodies.length, 2, 'nothing was cached in memory');
|
||||
assert.equal(
|
||||
bodies.length,
|
||||
1,
|
||||
'failed bodies are not cached but a cooldown bounds retry work',
|
||||
);
|
||||
});
|
||||
|
||||
for (const preview of [false, true])
|
||||
@@ -190,3 +201,194 @@ for (const preview of [false, true])
|
||||
assert.equal(hit.body, first.body);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
test('space providers honor Retry-After, gate groups, preserve stale dates and recover', async (t) => {
|
||||
isolateDisk(t);
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
let now = Date.now();
|
||||
t.mock.method(Date, 'now', () => now);
|
||||
for (const kind of ['tle', 'launches']) {
|
||||
for (const retryAfter of ['90', new Date(now + 90000).toUTCString()]) {
|
||||
let calls = 0;
|
||||
let fail = false;
|
||||
let cancelled = false;
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
calls++;
|
||||
if (fail)
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: kind === 'tle' ? 403 : 429,
|
||||
headers: { 'Retry-After': retryAfter },
|
||||
},
|
||||
);
|
||||
return new Response(
|
||||
kind === 'tle'
|
||||
? 'ISS\n1 25544U fixture\n2 25544 fixture\n'
|
||||
: '{"results":[]}',
|
||||
);
|
||||
});
|
||||
const request = install(
|
||||
kind === 'tle' ? celestrakProxy() : rocketLaunchesProxy(),
|
||||
);
|
||||
const route = kind === 'tle' ? '/api/celestrak' : '/api/launches';
|
||||
const url = kind === 'tle' ? '/stations' : '/';
|
||||
if (kind === 'tle') {
|
||||
assert.equal(
|
||||
(await request(route, '/not-a-supported-group')).status,
|
||||
400,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
}
|
||||
const first = await request(route, url);
|
||||
now += 7 * 3600000;
|
||||
// Recompute the HTTP-date relative to the clock at the failed refresh.
|
||||
const originalFetch = globalThis.fetch;
|
||||
t.mock.method(globalThis, 'fetch', async (...args) => {
|
||||
const response = await originalFetch(...args);
|
||||
if (fail && !/^\d+$/.test(retryAfter))
|
||||
response.headers.set(
|
||||
'Retry-After',
|
||||
new Date(now + 90000).toUTCString(),
|
||||
);
|
||||
return response;
|
||||
});
|
||||
fail = true;
|
||||
const stale = await request(route, url);
|
||||
assert.equal(stale.status, 200);
|
||||
assert.equal(stale.body, first.body);
|
||||
assert.equal(stale.headers['Retry-After'], '90');
|
||||
assert.ok(
|
||||
stale.headers[kind === 'tle' ? 'X-TLE-Cached-At' : 'X-GEV-Cached-At'],
|
||||
);
|
||||
assert.equal(cancelled, true);
|
||||
await request(route, url);
|
||||
if (kind === 'tle')
|
||||
assert.equal((await request(route, '/visual')).status, 502);
|
||||
assert.equal(calls, 2, 'cooldown applies across TLE groups');
|
||||
now += 91000;
|
||||
fail = false;
|
||||
assert.equal((await request(route, url)).status, 200);
|
||||
assert.equal(calls, 3);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('Space Missions reads the active catalog through the real proxy while unknown groups stay rejected', async (t) => {
|
||||
isolateDisk(t);
|
||||
const tle = 'ISS\n1 25544U fixture\n2 25544 fixture\n';
|
||||
const fetched = [];
|
||||
t.mock.method(globalThis, 'fetch', async (url) => {
|
||||
fetched.push(new URL(url).searchParams.get('GROUP'));
|
||||
return new Response(tle);
|
||||
});
|
||||
const request = install(celestrakProxy());
|
||||
// Route the browser source's real request into the middleware.
|
||||
const source = createLaunchSource({
|
||||
fetchImpl: async (url) => {
|
||||
const prefix = '/api/celestrak';
|
||||
assert.ok(url.startsWith(`${prefix}/`));
|
||||
const res = await request(prefix, url.slice(prefix.length));
|
||||
return new Response(res.body, { status: res.status });
|
||||
},
|
||||
});
|
||||
assert.equal(await source.getActiveTle(), tle);
|
||||
assert.deepEqual(fetched, ['active']);
|
||||
for (const group of ['/not-a-group', '/active-extra', '/../active'])
|
||||
assert.equal((await request('/api/celestrak', group)).status, 400);
|
||||
assert.deepEqual(fetched, ['active'], 'rejected groups never reach upstream');
|
||||
});
|
||||
|
||||
test('CelesTrak admits a same-origin request behind the documented reverse proxy and refuses foreign origins', async (t) => {
|
||||
isolateDisk(t);
|
||||
let calls = 0;
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
calls++;
|
||||
return new Response('ISS\n1 25544U fixture\n2 25544 fixture\n');
|
||||
});
|
||||
const request = install(celestrakProxy());
|
||||
const proxied = {
|
||||
host: 'gev.example.test',
|
||||
'x-forwarded-for': '203.0.113.7',
|
||||
'x-forwarded-host': 'gev.example.test',
|
||||
'x-forwarded-proto': 'https',
|
||||
via: '1.1 proxy',
|
||||
};
|
||||
const app = await request('/api/celestrak', '/stations', 'GET', {
|
||||
...proxied,
|
||||
origin: 'https://gev.example.test',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
});
|
||||
assert.equal(app.status, 200);
|
||||
assert.equal(app.headers['x-tle-cache'], 'MISS');
|
||||
assert.equal(calls, 1);
|
||||
for (const headers of [
|
||||
{ ...proxied, origin: 'https://unrelated.example' },
|
||||
{ ...proxied, origin: 'null' },
|
||||
{ ...proxied, 'sec-fetch-site': 'cross-site' },
|
||||
]) {
|
||||
const refused = await request('/api/celestrak', '/visual', 'GET', headers);
|
||||
assert.equal(refused.status, 403);
|
||||
}
|
||||
assert.equal(calls, 1, 'refused requests do no provider or cache work');
|
||||
});
|
||||
|
||||
for (const longFirst of [true, false])
|
||||
test(`CelesTrak keeps the longest cooldown when concurrent failures finish ${longFirst ? 'long then short' : 'short then long'}`, async (t) => {
|
||||
isolateDisk(t);
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
let now = Date.now();
|
||||
t.mock.method(Date, 'now', () => now);
|
||||
const pending = new Map();
|
||||
let calls = 0;
|
||||
t.mock.method(globalThis, 'fetch', async (url) => {
|
||||
calls++;
|
||||
const group = new URL(url).searchParams.get('GROUP');
|
||||
await new Promise((resolve) => pending.set(group, resolve));
|
||||
return group === 'stations'
|
||||
? new Response('slow down', {
|
||||
status: 429,
|
||||
headers: { 'Retry-After': '1800' },
|
||||
})
|
||||
: new Response('unavailable', { status: 503 });
|
||||
});
|
||||
const request = install(celestrakProxy());
|
||||
const long = request('/api/celestrak', '/stations');
|
||||
const short = request('/api/celestrak', '/visual');
|
||||
while (pending.size < 2) await new Promise(setImmediate);
|
||||
const order = longFirst
|
||||
? [
|
||||
['stations', long],
|
||||
['visual', short],
|
||||
]
|
||||
: [
|
||||
['visual', short],
|
||||
['stations', long],
|
||||
];
|
||||
for (const [group, response] of order) {
|
||||
pending.get(group)();
|
||||
assert.equal((await response).status, 502);
|
||||
}
|
||||
assert.equal(calls, 2);
|
||||
const cooling = await request('/api/celestrak', '/geo');
|
||||
assert.equal(cooling.status, 502);
|
||||
assert.equal(cooling.headers['Retry-After'], '1800');
|
||||
now += 61_000;
|
||||
assert.equal((await request('/api/celestrak', '/geo')).status, 502);
|
||||
assert.equal(
|
||||
calls,
|
||||
2,
|
||||
'the 60 s fallback did not replace the 30 min deadline',
|
||||
);
|
||||
now += 1800_000;
|
||||
t.mock.method(
|
||||
globalThis,
|
||||
'fetch',
|
||||
async () => new Response('ISS\n1 25544U fixture\n2 25544 fixture\n'),
|
||||
);
|
||||
assert.equal((await request('/api/celestrak', '/geo')).status, 200);
|
||||
});
|
||||
@@ -1,21 +1,9 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { inflateSync } from 'node:zlib';
|
||||
import { createApplicationStreetLevel } from '../app/layers/streetLevel.js';
|
||||
import {
|
||||
fixtureTile,
|
||||
EXPECTED_PROVIDERS,
|
||||
isCollapsed,
|
||||
PARK,
|
||||
STRICT_ALLOWED_SKIPS,
|
||||
strictViolations,
|
||||
} from '../../scripts/qa-street-level.mjs';
|
||||
import {
|
||||
answerMapillaryRequest,
|
||||
describeCall,
|
||||
metresApart,
|
||||
PHOTO_LINE,
|
||||
PHOTO_SEQUENCE_ID,
|
||||
photoImages,
|
||||
THUMB_HOST,
|
||||
@@ -40,350 +28,6 @@ import {
|
||||
resizedImageUrl,
|
||||
} from '../layers/streetLevel/providers/kartaview/policy.js';
|
||||
|
||||
// The press helpers wait for paint; Node has no frames, so a tick stands in.
|
||||
globalThis.requestAnimationFrame ??= (callback) =>
|
||||
setTimeout(() => callback(Date.now()), 0);
|
||||
|
||||
test('the harness expects exactly the providers the app registers, in chip order', () => {
|
||||
// Any method a provider asks its source for is a no-op: only ids matter.
|
||||
const source = new Proxy({}, { get: () => () => {} });
|
||||
const layer = createApplicationStreetLevel({
|
||||
surface: null,
|
||||
sources: { mapillary: source },
|
||||
});
|
||||
assert.deepEqual([...EXPECTED_PROVIDERS], [...layer.providerIds]);
|
||||
});
|
||||
|
||||
test('a panel reads as collapsed only with the collapsed class', () => {
|
||||
assert.equal(isCollapsed(['panel-collapsible', 'collapsed']), true);
|
||||
assert.equal(isCollapsed(['panel-collapsible']), false);
|
||||
});
|
||||
|
||||
test('the harness only runs its browser flow when executed directly', () => {
|
||||
const source = fs.readFileSync(
|
||||
new URL('../../scripts/qa-street-level.mjs', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/import\.meta\.url === pathToFileURL\(process\.argv\[1\]\)\.href/,
|
||||
);
|
||||
assert.match(source, /PUPPETEER_EXECUTABLE_PATH/);
|
||||
assert.match(source, /--url/);
|
||||
});
|
||||
|
||||
test('fixture tiles give the hermetic gate something real to filter', async () => {
|
||||
const { decodeCoverageTile } =
|
||||
await import('../layers/streetLevel/providers/mapillary/decode.js');
|
||||
const now = Date.UTC(2026, 9, 1);
|
||||
const street = decodeCoverageTile(fixtureTile(14, 2662, 6286, now), {
|
||||
x: 2662,
|
||||
y: 6286,
|
||||
z: 14,
|
||||
});
|
||||
const grid = street.sequences.filter((s) => s.id !== PHOTO_SEQUENCE_ID);
|
||||
assert.equal(grid.length, 8);
|
||||
const pano = grid.filter((s) => s.isPano).length;
|
||||
assert.ok(pano > 0 && pano < 8, '360° and flat both present');
|
||||
const year = 365 * 86_400_000;
|
||||
const old = grid.filter((s) => now - s.capturedAt > year).length;
|
||||
assert.ok(old > 0 && old < 8, 'recent and older both present');
|
||||
const orbit = decodeCoverageTile(fixtureTile(3, 1, 3, now), {
|
||||
x: 1,
|
||||
y: 3,
|
||||
z: 3,
|
||||
});
|
||||
assert.ok(orbit.overview.length > 0, 'overview points from orbit');
|
||||
assert.equal(fixtureTile(8, 1, 1, now).length, 0, 'nothing in between');
|
||||
});
|
||||
|
||||
/* ── The real status route probe ─────────────────────────────────────── */
|
||||
|
||||
test('the gate requires the server’s real Mapillary status route', async () => {
|
||||
const { assertRealStatusRoute } =
|
||||
await import('../../scripts/qa-street-level.mjs');
|
||||
const asked = [];
|
||||
const answer = (body, init) => async (url) => {
|
||||
asked.push(url);
|
||||
return typeof body === 'string'
|
||||
? new Response(body, init)
|
||||
: Response.json(body, init);
|
||||
};
|
||||
assert.deepEqual(
|
||||
await assertRealStatusRoute(
|
||||
'http://localhost:4173',
|
||||
answer({ configured: false }),
|
||||
),
|
||||
{ configured: false },
|
||||
);
|
||||
assert.deepEqual(asked, ['http://localhost:4173/api/mapillary/status']);
|
||||
for (const [why, fetchImpl] of [
|
||||
[
|
||||
'an unregistered route (the API 404)',
|
||||
answer({ error: 'Unknown API route' }, { status: 404 }),
|
||||
],
|
||||
[
|
||||
'the SPA fallback',
|
||||
answer('<!doctype html><title>GEV</title>', {
|
||||
headers: { 'content-type': 'text/html' },
|
||||
}),
|
||||
],
|
||||
['a malformed status', answer({ configured: 'yes' })],
|
||||
[
|
||||
'an unreachable server',
|
||||
async () => {
|
||||
throw new TypeError('fetch failed');
|
||||
},
|
||||
],
|
||||
])
|
||||
await assert.rejects(
|
||||
assertRealStatusRoute('http://localhost:4173', fetchImpl),
|
||||
/api\/mapillary\/status/,
|
||||
why,
|
||||
);
|
||||
});
|
||||
|
||||
/* ── Header press-and-verify shared with the panel gate ───────────────── */
|
||||
|
||||
/**
|
||||
* A page stand-in that runs page functions in Node against a fake document.
|
||||
* The hit check always sees the header; `presses` says where each pointerdown
|
||||
* lands ('header', 'other', 'moved' or 'none').
|
||||
*/
|
||||
function fakePanelPage({ presses = [], floating = false, stuck = false } = {}) {
|
||||
const state = { floating, downs: 0, last: null, dragging: false, shift: 0 };
|
||||
const listeners = [];
|
||||
const node = (tagName, id, classes, inHeader) => ({
|
||||
tagName,
|
||||
id,
|
||||
classList: classes,
|
||||
closest: (selector) =>
|
||||
inHeader && selector.includes('.panel-header') ? {} : null,
|
||||
});
|
||||
const header = node('DIV', '', ['panel-title'], true);
|
||||
const other = node('SECTION', 'weather-panel', ['panel'], false);
|
||||
const rect = (left, top, width, height) => () => ({
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
const document = {
|
||||
querySelector: () => ({ getBoundingClientRect: rect(120, 210, 100, 20) }),
|
||||
getElementById: () => ({
|
||||
getBoundingClientRect: () => rect(100, 200 + state.shift, 300, 400)(),
|
||||
classList: {
|
||||
contains: (name) => name === 'panel-floating' && state.floating,
|
||||
},
|
||||
}),
|
||||
elementFromPoint: () => header,
|
||||
};
|
||||
const window = {
|
||||
addEventListener: (type, listener) => listeners.push(listener),
|
||||
};
|
||||
const inPage = (fn, ...args) => {
|
||||
const saved = { document: globalThis.document, window: globalThis.window };
|
||||
Object.assign(globalThis, { document, window });
|
||||
try {
|
||||
return fn(...args);
|
||||
} finally {
|
||||
Object.assign(globalThis, saved);
|
||||
}
|
||||
};
|
||||
const evaluate = async (fn, ...args) => inPage(fn, ...args);
|
||||
const press = () => {
|
||||
state.last = presses[state.downs++] ?? 'header';
|
||||
// 'none': the pointerdown never reaches the page.
|
||||
if (state.last === 'none') return;
|
||||
const target = state.last === 'header' ? header : other;
|
||||
// 'moved': the panel's layout shifted just before the press landed.
|
||||
if (state.last === 'moved') state.shift = -116;
|
||||
for (const listener of listeners.splice(0)) inPage(listener, { target });
|
||||
state.shift = 0;
|
||||
};
|
||||
return {
|
||||
state,
|
||||
evaluate,
|
||||
async waitForFunction(fn, options, ...args) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (await evaluate(fn, ...args)) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
}
|
||||
throw Object.assign(new Error('timed out'), { name: 'TimeoutError' });
|
||||
},
|
||||
mouse: {
|
||||
async move() {},
|
||||
async down() {
|
||||
press();
|
||||
state.dragging = true;
|
||||
},
|
||||
async up() {
|
||||
if (state.dragging && state.last === 'header' && !stuck)
|
||||
state.floating = true;
|
||||
state.dragging = false;
|
||||
},
|
||||
async click(x, y, { clickCount }) {
|
||||
if (clickCount === 1) press();
|
||||
else if (state.last === 'header' && !stuck) state.floating = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('a header press is retried only when it provably missed the header', async () => {
|
||||
const { pressMissed } = await import('../../scripts/qa-panelDrag.mjs');
|
||||
assert.equal(pressMissed({ done: true, pressed: null }), false);
|
||||
assert.equal(
|
||||
pressMissed({ done: false, pressed: { inHeader: true } }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
pressMissed({ done: false, pressed: { inHeader: false } }),
|
||||
true,
|
||||
);
|
||||
// No recorded press is a failure, not a miss to retry.
|
||||
assert.equal(pressMissed({ done: false, pressed: null }), false);
|
||||
});
|
||||
|
||||
test('a lift whose press the rail moved off the header is retried, then lifts', async () => {
|
||||
const { liftPanelByHeader, describePress } =
|
||||
await import('../../scripts/qa-panelDrag.mjs');
|
||||
const page = fakePanelPage({ presses: ['other', 'header'] });
|
||||
const notes = [];
|
||||
const attempt = await liftPanelByHeader(page, 'street-level-panel', {
|
||||
dx: -400,
|
||||
dy: 80,
|
||||
log: (line) => notes.push(line),
|
||||
pauseMs: 0,
|
||||
});
|
||||
assert.equal(attempt.done, true);
|
||||
assert.equal(page.state.downs, 2);
|
||||
assert.equal(notes.length, 2, 'a note and a GitHub warning');
|
||||
assert.match(notes[0], /missed the street-level-panel header/);
|
||||
assert.match(notes[0], /section#weather-panel\.panel/, 'names what it hit');
|
||||
assert.match(notes[0], /pre-press hit check passed/);
|
||||
assert.match(notes[0], /stillness (held|timed out)/);
|
||||
assert.match(
|
||||
notes[0],
|
||||
/panel box at hit check 100,200 300x400 -> at press 100,200 300x400/,
|
||||
);
|
||||
assert.match(notes[1], /^::warning title=Panel press retried::retry 1\/2/);
|
||||
assert.match(describePress(attempt), /in header: true, after 1 retry$/);
|
||||
});
|
||||
|
||||
test('a press in the header that does not lift fails at once, naming the target', async () => {
|
||||
const { liftPanelByHeader, describePress } =
|
||||
await import('../../scripts/qa-panelDrag.mjs');
|
||||
const page = fakePanelPage({ stuck: true });
|
||||
const attempt = await liftPanelByHeader(page, 'street-level-panel', {
|
||||
dx: -400,
|
||||
dy: 80,
|
||||
log: () => assert.fail('no retry'),
|
||||
pauseMs: 0,
|
||||
});
|
||||
assert.equal(attempt.done, false);
|
||||
assert.equal(page.state.downs, 1);
|
||||
assert.match(describePress(attempt), /on div\.panel-title, in header: true/);
|
||||
});
|
||||
|
||||
test('presses that keep missing stop after the retries and report the miss', async () => {
|
||||
const { liftPanelByHeader, PRESS_RETRIES } =
|
||||
await import('../../scripts/qa-panelDrag.mjs');
|
||||
const page = fakePanelPage({ presses: ['other', 'other', 'other', 'other'] });
|
||||
const attempt = await liftPanelByHeader(page, 'street-level-panel', {
|
||||
dx: -400,
|
||||
dy: 80,
|
||||
log: () => {},
|
||||
pauseMs: 0,
|
||||
});
|
||||
assert.equal(attempt.done, false);
|
||||
assert.equal(page.state.downs, PRESS_RETRIES + 1);
|
||||
assert.equal(attempt.pressed.inHeader, false);
|
||||
});
|
||||
|
||||
test('a header double-click docks, waiting on the docked state', async () => {
|
||||
const { dockPanelByDoubleClick } =
|
||||
await import('../../scripts/qa-panelDrag.mjs');
|
||||
const page = fakePanelPage({ floating: true, presses: ['other', 'header'] });
|
||||
const attempt = await dockPanelByDoubleClick(page, 'street-level-panel', {
|
||||
log: () => {},
|
||||
pauseMs: 0,
|
||||
});
|
||||
assert.equal(attempt.done, true);
|
||||
assert.equal(page.state.floating, false);
|
||||
assert.equal(page.state.downs, 2, 'the missed first press was retried');
|
||||
});
|
||||
|
||||
test('both panel gates press the header through the shared helper', () => {
|
||||
for (const script of ['qa-street-level.mjs', 'qa-panel-resize.mjs']) {
|
||||
const source = fs.readFileSync(
|
||||
new URL(`../../scripts/${script}`, import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(source, /from '\.\/qa-panelDrag\.mjs'/, script);
|
||||
assert.match(source, /liftPanelByHeader\(page, PANEL_ID/, script);
|
||||
assert.match(source, /dockPanelByDoubleClick\(page, PANEL_ID/, script);
|
||||
}
|
||||
});
|
||||
|
||||
/* ── Hermetic photo flow: the photo line and the Graph fixtures ────────── */
|
||||
|
||||
test('the photo sequence runs through the parked view, clipped to each tile it crosses', async () => {
|
||||
const { decodeCoverageTile } =
|
||||
await import('../layers/streetLevel/providers/mapillary/decode.js');
|
||||
const { lonToTileX, latToTileY } =
|
||||
await import('../layers/streetLevel/tileMath.js');
|
||||
const x = lonToTileX(PHOTO_LINE.lon, 14);
|
||||
const rows = new Set(
|
||||
[PHOTO_LINE.south, PHOTO_LINE.north].map((lat) => latToTileY(lat, 14)),
|
||||
);
|
||||
assert.ok(rows.size > 1, 'the line crosses a tile edge');
|
||||
let south = Infinity;
|
||||
let north = -Infinity;
|
||||
for (const y of rows) {
|
||||
const line = decodeCoverageTile(fixtureTile(14, x, y), {
|
||||
x,
|
||||
y,
|
||||
z: 14,
|
||||
}).sequences.find((s) => s.id === PHOTO_SEQUENCE_ID);
|
||||
assert.ok(line, `tile 14/${x}/${y} carries the photo line`);
|
||||
for (const [lon, lat] of line.parts[0]) {
|
||||
assert.ok(Math.abs(lon - PHOTO_LINE.lon) < 1e-4);
|
||||
south = Math.min(south, lat);
|
||||
north = Math.max(north, lat);
|
||||
}
|
||||
}
|
||||
assert.ok(Math.abs(south - PHOTO_LINE.south) < 1e-4);
|
||||
assert.ok(Math.abs(north - PHOTO_LINE.north) < 1e-4);
|
||||
// Far away there is no photo line, only the grid.
|
||||
const elsewhere = decodeCoverageTile(fixtureTile(14, 100, 100), {
|
||||
x: 100,
|
||||
y: 100,
|
||||
z: 14,
|
||||
});
|
||||
assert.equal(
|
||||
elsewhere.sequences.some((s) => s.id === PHOTO_SEQUENCE_ID),
|
||||
false,
|
||||
);
|
||||
// The parked camera looks down onto the line.
|
||||
assert.equal(PARK.lon, PHOTO_LINE.lon);
|
||||
assert.ok(PARK.lat > PHOTO_LINE.south && PARK.lat < PHOTO_LINE.north);
|
||||
});
|
||||
|
||||
test('the fixture photos: one sequence, 360° and flat, close enough for any nearest lookup on the line', () => {
|
||||
const images = photoImages(Date.UTC(2026, 9, 1));
|
||||
assert.ok(images.length >= 3);
|
||||
assert.ok(images.some((image) => image.isPano));
|
||||
assert.ok(images.some((image) => !image.isPano));
|
||||
for (let i = 1; i < images.length; i++) {
|
||||
const gap = metresApart(images[i - 1], images[i]);
|
||||
// Above the cones' 3 m thinning, and at most 50 m apart: any point on
|
||||
// the line has a photo within the Graph API's 50 m radius.
|
||||
assert.ok(gap > 3 && gap < 50, `gap ${gap}`);
|
||||
}
|
||||
assert.equal(new Set(images.map((image) => image.id)).size, images.length);
|
||||
});
|
||||
|
||||
const graph = (path, params) =>
|
||||
`https://graph.mapillary.com/${path}?${new URLSearchParams({ access_token: 'MLY|0|qa-fixture', ...params })}`;
|
||||
const answer = (url, method = 'GET') => {
|
||||
@@ -394,49 +38,6 @@ const answer = (url, method = 'GET') => {
|
||||
};
|
||||
};
|
||||
|
||||
test('the Graph fixtures answer the provider’s nearest and sequence lookups', () => {
|
||||
const images = photoImages();
|
||||
const middle = images[Math.floor(images.length / 2)];
|
||||
const near = answer(
|
||||
graph('images', {
|
||||
lat: String(middle.lat + 0.0001),
|
||||
lng: String(middle.lon),
|
||||
radius: '50',
|
||||
limit: '8',
|
||||
fields: 'id,geometry,is_pano,captured_at,sequence',
|
||||
}),
|
||||
);
|
||||
assert.equal(near.status, 200);
|
||||
assert.ok(near.json.data.length > 0);
|
||||
assert.ok(near.json.data.some((record) => record.id === middle.id));
|
||||
for (const record of near.json.data) {
|
||||
assert.deepEqual(Object.keys(record).sort(), [
|
||||
'captured_at',
|
||||
'geometry',
|
||||
'id',
|
||||
'is_pano',
|
||||
'sequence',
|
||||
]);
|
||||
assert.equal(record.sequence, PHOTO_SEQUENCE_ID);
|
||||
}
|
||||
const far = answer(
|
||||
graph('images', { lat: '0', lng: '0', radius: '50', fields: 'id' }),
|
||||
);
|
||||
assert.deepEqual(far.json.data, [], 'nothing outside the radius');
|
||||
const sequence = answer(
|
||||
graph('images', {
|
||||
sequence_ids: PHOTO_SEQUENCE_ID,
|
||||
fields: 'id,geometry,compass_angle,captured_at,is_pano',
|
||||
limit: '2000',
|
||||
}),
|
||||
);
|
||||
assert.equal(sequence.json.data.length, images.length);
|
||||
assert.deepEqual(
|
||||
answer(graph('images', { sequence_ids: 'other', fields: 'id' })).json.data,
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
test('the Graph fixtures answer every call MapillaryJS makes to open a photo', () => {
|
||||
const [first, second] = photoImages();
|
||||
const spatial = answer(
|
||||
@@ -531,134 +132,6 @@ test('evidence never carries a Mapillary token', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('--strict accepts only the documented skips', () => {
|
||||
assert.deepEqual(
|
||||
[...STRICT_ALLOWED_SKIPS],
|
||||
['no Google 3D', 'no Google key', 'fixtures only'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
strictViolations([
|
||||
{ reason: 'no Google 3D', label: 'terrain' },
|
||||
{ reason: 'no Google key', label: 'street view' },
|
||||
{ reason: 'fixtures only', label: 'keyless page' },
|
||||
{ reason: 'no Mapillary key', label: 'the keyed steps' },
|
||||
{ reason: 'Google key refused', label: 'street view' },
|
||||
]),
|
||||
[
|
||||
{ reason: 'no Mapillary key', label: 'the keyed steps' },
|
||||
{ reason: 'Google key refused', label: 'street view' },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('a press that records no pointerdown fails at once, without a retry', async () => {
|
||||
const { liftPanelByHeader, describePress } =
|
||||
await import('../../scripts/qa-panelDrag.mjs');
|
||||
const page = fakePanelPage({ presses: ['none', 'header'] });
|
||||
const attempt = await liftPanelByHeader(page, 'street-level-panel', {
|
||||
dx: -400,
|
||||
dy: 80,
|
||||
log: () => assert.fail('no retry'),
|
||||
pauseMs: 0,
|
||||
});
|
||||
assert.equal(attempt.done, false);
|
||||
assert.equal(attempt.pressed, null);
|
||||
assert.equal(page.state.downs, 1);
|
||||
assert.match(describePress(attempt), /no pointerdown reached the page/);
|
||||
});
|
||||
|
||||
test('QA_FAIL_ON_RETRY turns a retry into a failure that says why', async () => {
|
||||
const { liftPanelByHeader, failOnRetryDefault } =
|
||||
await import('../../scripts/qa-panelDrag.mjs');
|
||||
assert.equal(failOnRetryDefault({ QA_FAIL_ON_RETRY: '1' }, []), true);
|
||||
assert.equal(failOnRetryDefault({}, ['--fail-on-retry']), true);
|
||||
assert.equal(failOnRetryDefault({}, []), false);
|
||||
const page = fakePanelPage({ presses: ['other', 'header'] });
|
||||
await assert.rejects(
|
||||
liftPanelByHeader(page, 'street-level-panel', {
|
||||
dx: -400,
|
||||
dy: 80,
|
||||
log: () => {},
|
||||
pauseMs: 0,
|
||||
failOnRetry: true,
|
||||
}),
|
||||
/QA_FAIL_ON_RETRY=1 .*missed the street-level-panel header.*pre-press hit check passed/,
|
||||
);
|
||||
assert.equal(page.state.downs, 1);
|
||||
});
|
||||
|
||||
test('QA_FAIL_ON_RETRY retries, with a warning, a miss the panel moving explains', async () => {
|
||||
const { liftPanelByHeader, panelMoved } =
|
||||
await import('../../scripts/qa-panelDrag.mjs');
|
||||
assert.equal(panelMoved({ left: 0, top: 382 }, { left: 0, top: 266 }), true);
|
||||
assert.equal(panelMoved({ left: 0, top: 382 }, { left: 1, top: 383 }), false);
|
||||
const page = fakePanelPage({ presses: ['moved', 'header'] });
|
||||
const lines = [];
|
||||
const attempt = await liftPanelByHeader(page, 'street-level-panel', {
|
||||
dx: -400,
|
||||
dy: 80,
|
||||
log: (line) => lines.push(line),
|
||||
pauseMs: 0,
|
||||
failOnRetry: true,
|
||||
});
|
||||
assert.equal(attempt.done, true, 'the second press lifted it');
|
||||
assert.equal(page.state.downs, 2);
|
||||
assert.ok(
|
||||
lines.some((line) =>
|
||||
/::warning title=Panel press retried::.*-> at press 100,84/.test(line),
|
||||
),
|
||||
lines.join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
test('both gates save failure evidence and fail on render-loop errors', () => {
|
||||
for (const script of ['qa-street-level.mjs', 'qa-panel-resize.mjs']) {
|
||||
const source = fs.readFileSync(
|
||||
new URL(`../../scripts/${script}`, import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(source, /saveFailureArtifacts\(/, script);
|
||||
assert.match(source, /hookRenderErrors\(/, script);
|
||||
assert.match(source, /readRenderErrors\(/, script);
|
||||
assert.match(source, /--fail-on-retry/, script);
|
||||
}
|
||||
});
|
||||
|
||||
test('CI runs the gates hermetically against a production build, strict, with evidence', () => {
|
||||
const ci = fs.readFileSync(
|
||||
new URL('../../.github/workflows/ci.yml', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const job = ci.slice(ci.indexOf('street-level-browser:'));
|
||||
const gate = job.slice(0, job.indexOf('\n windows-onboarding:'));
|
||||
// The dummy token is baked into the bundle at build time and the preview
|
||||
// server's status route reads it at run time: both steps carry it.
|
||||
assert.match(
|
||||
gate,
|
||||
/MAPILLARY_CLIENT_TOKEN: 'MLY\|0\|qa-fixture'\s+run: npm run build/,
|
||||
);
|
||||
assert.match(
|
||||
gate,
|
||||
/MAPILLARY_CLIENT_TOKEN: 'MLY\|0\|qa-fixture'\s+run: \|\s+npx vite preview --port 4173 --strictPort/,
|
||||
);
|
||||
assert.doesNotMatch(gate, /npx vite --port/);
|
||||
assert.equal(
|
||||
(gate.match(/MAPILLARY_CLIENT_TOKEN: 'MLY\|0\|qa-fixture'/g) || []).length,
|
||||
2,
|
||||
);
|
||||
assert.match(gate, /qa:street-level:fixtures -- --strict/);
|
||||
assert.match(
|
||||
gate,
|
||||
/if: failure\(\)[\s\S]*actions\/upload-artifact@[0-9a-f]{40}/,
|
||||
);
|
||||
assert.match(gate, /path: qa-artifacts\//);
|
||||
assert.match(
|
||||
gate,
|
||||
/QA_FAIL_ON_RETRY: '1'/,
|
||||
'a missed header press fails CI now that the rail settles',
|
||||
);
|
||||
});
|
||||
|
||||
test('KartaView fixtures draw the drive, answer nearest first, and refuse the storage host the API names', () => {
|
||||
const body = (answer) => JSON.parse(answer.body);
|
||||
const lat = (KV_LINE.south + KV_LINE.north) / 2;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/** A reveal must beat the one-second idle rotation refresh, regardless of FPS. */
|
||||
export const ARRIVAL_OBSERVATION_MS = 500;
|
||||
|
||||
/**
|
||||
* Observe the first completed moveEnd frame and the first subsequent reveal.
|
||||
* The deadline starts when moveEnd is forced, before its rotation baseline, so
|
||||
* waiting for either frame cannot let the one-second idle refresh mask a bug.
|
||||
* `read` snapshots visibility and rotation error against the current position.
|
||||
*/
|
||||
export function observeArrivalRotation({
|
||||
scene,
|
||||
startMoveEnd,
|
||||
startReveal,
|
||||
read,
|
||||
now = () => performance.now(),
|
||||
setTimer = setTimeout,
|
||||
clearTimer = clearTimeout,
|
||||
}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startedAt = now();
|
||||
let moveEnd = null;
|
||||
let moveEndMs = null;
|
||||
let revealFrames = 0;
|
||||
let done = false;
|
||||
let remove;
|
||||
let timer;
|
||||
const cleanup = () => {
|
||||
done = true;
|
||||
remove?.();
|
||||
clearTimer(timer);
|
||||
};
|
||||
const finish = (result) => {
|
||||
if (done) return;
|
||||
cleanup();
|
||||
resolve(result);
|
||||
};
|
||||
const expired = () => ({
|
||||
error: `arrival ${moveEnd ? 'reveal' : 'moveEnd'} observation exceeded ${ARRIVAL_OBSERVATION_MS} ms`,
|
||||
elapsedMs: now() - startedAt,
|
||||
moveEndMs,
|
||||
flipFrame: -1,
|
||||
});
|
||||
remove = scene.postRender.addEventListener(() => {
|
||||
if (done) return;
|
||||
try {
|
||||
const elapsedMs = now() - startedAt;
|
||||
if (elapsedMs >= ARRIVAL_OBSERVATION_MS) {
|
||||
finish(expired());
|
||||
return;
|
||||
}
|
||||
const sample = read();
|
||||
if (moveEnd === null) {
|
||||
moveEnd = sample;
|
||||
moveEndMs = elapsedMs;
|
||||
startReveal();
|
||||
} else {
|
||||
revealFrames++;
|
||||
if (sample.shown) {
|
||||
// Keep the FIRST visible result, including a wrong angle. Never
|
||||
// wait for a subsequent tick to repair a failed reveal.
|
||||
finish({
|
||||
dMoveEnd: moveEnd.rotationError,
|
||||
dReveal: sample.rotationError,
|
||||
shown: sample.shown,
|
||||
flipFrame: revealFrames,
|
||||
moveEndMs,
|
||||
revealMs: elapsedMs - moveEndMs,
|
||||
elapsedMs,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
scene.requestRender();
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
timer = setTimer(() => finish(expired()), ARRIVAL_OBSERVATION_MS);
|
||||
try {
|
||||
startMoveEnd();
|
||||
scene.requestRender();
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Test the captured frames without retrying a bad first observation. */
|
||||
export function arrivalRotationPassed(observation) {
|
||||
return (
|
||||
!!observation &&
|
||||
!observation.error &&
|
||||
observation.shown === true &&
|
||||
observation.elapsedMs < ARRIVAL_OBSERVATION_MS &&
|
||||
(observation.dMoveEnd * 180) / Math.PI < 10 &&
|
||||
(observation.dReveal * 180) / Math.PI < 10
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
ARRIVAL_OBSERVATION_MS,
|
||||
arrivalRotationPassed,
|
||||
observeArrivalRotation,
|
||||
} from './trackArrivalObservation.mjs';
|
||||
|
||||
function fixture() {
|
||||
let time = 0;
|
||||
let state = { shown: true, rotationError: 0 };
|
||||
const listeners = new Set();
|
||||
const timers = new Map();
|
||||
let renders = 0;
|
||||
let moveEnds = 0;
|
||||
const observation = observeArrivalRotation({
|
||||
scene: {
|
||||
postRender: {
|
||||
addEventListener(listener) {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
},
|
||||
requestRender() {
|
||||
renders++;
|
||||
},
|
||||
},
|
||||
startMoveEnd() {
|
||||
moveEnds++;
|
||||
state.rotationError = 1.5;
|
||||
},
|
||||
startReveal() {
|
||||
state = { shown: false, rotationError: 1.5 };
|
||||
},
|
||||
read: () => ({ ...state }),
|
||||
now: () => time,
|
||||
setTimer(callback, delay) {
|
||||
const handle = {};
|
||||
timers.set(handle, { callback, at: time + delay });
|
||||
return handle;
|
||||
},
|
||||
clearTimer(handle) {
|
||||
timers.delete(handle);
|
||||
},
|
||||
});
|
||||
return {
|
||||
observation,
|
||||
frame(at, nextState) {
|
||||
time = at;
|
||||
state = { ...state, ...nextState };
|
||||
for (const listener of [...listeners]) listener();
|
||||
},
|
||||
expire(at) {
|
||||
time = at;
|
||||
for (const timer of [...timers.values()])
|
||||
if (timer.at <= at) timer.callback();
|
||||
},
|
||||
get resources() {
|
||||
return {
|
||||
listeners: listeners.size,
|
||||
timers: timers.size,
|
||||
renders,
|
||||
moveEnds,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('80 ms fleet scheduling passes after more than four fast rendered frames', async () => {
|
||||
const f = fixture();
|
||||
f.frame(8, { shown: true, rotationError: 0 });
|
||||
// The forced fleet tick was at 8 ms. A 125 FPS renderer can complete nine
|
||||
// more frames before the next 80 ms fleet interval becomes eligible.
|
||||
for (let at = 16; at < 88; at += 8) f.frame(at);
|
||||
f.frame(88, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.equal(result.flipFrame, 10);
|
||||
assert.equal(result.moveEndMs, 8);
|
||||
assert.equal(result.revealMs, 80);
|
||||
assert.equal(result.elapsedMs, 88);
|
||||
assert.equal(arrivalRotationPassed(result), true);
|
||||
assert.equal(f.resources.listeners, 0);
|
||||
assert.equal(f.resources.timers, 0);
|
||||
assert.equal(f.resources.moveEnds, 1);
|
||||
});
|
||||
|
||||
test('a wrong first visible angle fails even if the next frame corrects it', async () => {
|
||||
const f = fixture();
|
||||
f.frame(8, { shown: true, rotationError: 0 });
|
||||
f.frame(88, { shown: true, rotationError: 1.5 });
|
||||
f.frame(96, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.equal(result.dReveal, 1.5);
|
||||
assert.equal(result.flipFrame, 1);
|
||||
assert.equal(result.elapsedMs, 88);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
});
|
||||
|
||||
test('a wrong first moveEnd angle is retained rather than repaired by reveal', async () => {
|
||||
const f = fixture();
|
||||
f.frame(8, { shown: true, rotationError: 1.5 });
|
||||
f.frame(88, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.equal(result.dMoveEnd, 1.5);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
});
|
||||
|
||||
test('no reveal fails at a deadline before the one-second idle catch-up', async () => {
|
||||
const f = fixture();
|
||||
assert.equal(ARRIVAL_OBSERVATION_MS, 500);
|
||||
f.frame(8, { shown: true, rotationError: 0 });
|
||||
f.frame(480);
|
||||
f.expire(500);
|
||||
const result = await f.observation;
|
||||
assert.match(result.error, /reveal.*500 ms/);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
assert.equal(f.resources.listeners, 0);
|
||||
assert.equal(f.resources.timers, 0);
|
||||
f.frame(1000, { shown: true, rotationError: 0 });
|
||||
assert.equal(result.flipFrame, -1);
|
||||
});
|
||||
|
||||
test('a late visible frame fails even if the timeout callback is delayed', async () => {
|
||||
const f = fixture();
|
||||
f.frame(8, { shown: true, rotationError: 0 });
|
||||
f.frame(501, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.match(result.error, /reveal.*500 ms/);
|
||||
assert.equal(result.elapsedMs, 501);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
});
|
||||
|
||||
test('the deadline includes moveEnd setup rather than restarting when hidden', async () => {
|
||||
const f = fixture();
|
||||
f.frame(400, { shown: true, rotationError: 0 });
|
||||
f.frame(600, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.equal(result.moveEndMs, 400);
|
||||
assert.equal(result.elapsedMs, 600);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
});
|
||||
|
||||
test('missing moveEnd frames fail without waiting for a render callback', async () => {
|
||||
const f = fixture();
|
||||
f.expire(500);
|
||||
const result = await f.observation;
|
||||
assert.match(result.error, /moveEnd.*500 ms/);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
assert.equal(f.resources.listeners, 0);
|
||||
assert.equal(f.resources.timers, 0);
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
trafficFixtureResponse,
|
||||
trafficFixtureTile,
|
||||
} from '../../scripts/traffic-fixtures.mjs';
|
||||
import { decodeOpenFreeMapTile } from '../sources/openFreeMap.js';
|
||||
import { isDrivableFlowSegment } from '../layers/traffic/roadModes.js';
|
||||
import { decodeFlowTile } from '../layers/traffic/flowDecode.js';
|
||||
|
||||
test('traffic fixtures provide aligned current road tiles and partial flow coverage', () => {
|
||||
for (const z of [12, 14]) {
|
||||
const roads = decodeOpenFreeMapTile(
|
||||
trafficFixtureTile(z),
|
||||
z,
|
||||
935,
|
||||
1686,
|
||||
).roads;
|
||||
const flow = decodeFlowTile(trafficFixtureTile(z, true), z, 935, 1686);
|
||||
assert.ok(roads.length > flow.length && flow.length > 0);
|
||||
for (const segment of flow) {
|
||||
assert.equal(segment.trafficLevel, 0.6);
|
||||
assert.equal(isDrivableFlowSegment(segment), true);
|
||||
assert.ok(
|
||||
roads.some(
|
||||
(road) =>
|
||||
JSON.stringify(road.coordinates) === JSON.stringify(segment.coords),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('fixture TileJSON and every road/flow request stay on intercepted sources', () => {
|
||||
const request = (url) => trafficFixtureResponse({ url: () => url });
|
||||
const tileJson = request('https://tiles.openfreemap.org/planet');
|
||||
assert.equal(tileJson.headers['access-control-allow-origin'], '*');
|
||||
const metadata = JSON.parse(tileJson.body);
|
||||
const tileUrl = metadata.tiles[0]
|
||||
.replace('{z}', '14')
|
||||
.replace('{x}', '935')
|
||||
.replace('{y}', '1686');
|
||||
assert.ok(
|
||||
decodeOpenFreeMapTile(request(tileUrl).body, 14, 935, 1686).roads.length,
|
||||
);
|
||||
assert.ok(
|
||||
decodeFlowTile(
|
||||
request('http://localhost/api/tomtom/flow/12/935/1686.pbf').body,
|
||||
12,
|
||||
935,
|
||||
1686,
|
||||
).length,
|
||||
);
|
||||
assert.throws(
|
||||
() => request('https://tiles.openfreemap.org/unexpected'),
|
||||
/Unrecognized/,
|
||||
);
|
||||
assert.equal(request('http://localhost/api/unrelated'), null);
|
||||
});
|
||||
|
||||
test('flat terrain fixture decodes through Cesium and advertises no network children', async () => {
|
||||
const { createServer } = await import('node:http');
|
||||
const { CesiumTerrainProvider } = await import('cesium');
|
||||
const server = createServer((req, res) => {
|
||||
const response = trafficFixtureResponse({
|
||||
url: () => `https://terrain.reearth.land${req.url}`,
|
||||
});
|
||||
res.writeHead(response.status, { 'content-type': response.contentType });
|
||||
res.end(response.body);
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
try {
|
||||
const provider = await CesiumTerrainProvider.fromUrl(
|
||||
`http://127.0.0.1:${server.address().port}`,
|
||||
);
|
||||
for (const x of [0, 1]) {
|
||||
const data = await provider.requestTileGeometry(x, 0, 0);
|
||||
const rect = provider.tilingScheme.tileXYToRectangle(x, 0, 0);
|
||||
assert.equal(
|
||||
data.interpolateHeight(rect, (rect.west + rect.east) / 2, 0),
|
||||
0,
|
||||
);
|
||||
for (const dx of [0, 1])
|
||||
for (const dy of [0, 1]) {
|
||||
assert.equal(data.isChildAvailable(x, 0, x * 2 + dx, dy), false);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
});
|
||||
|
||||
test('fixture interception targets provider URLs and leaves app workers alone', async () => {
|
||||
const { installTrafficRoutes } =
|
||||
await import('../../scripts/traffic-fixtures.mjs');
|
||||
const sent = [];
|
||||
let paused;
|
||||
const session = {
|
||||
on: (_, handler) => {
|
||||
paused = handler;
|
||||
},
|
||||
send: async (name, args) => {
|
||||
sent.push({ name, args });
|
||||
},
|
||||
};
|
||||
await installTrafficRoutes(
|
||||
{ createCDPSession: async () => session, isClosed: () => false },
|
||||
{ origin: 'http://localhost:4173', fixtures: true },
|
||||
);
|
||||
const patterns = sent[0].args.patterns.map((p) => p.urlPattern);
|
||||
assert.deepEqual(patterns, [
|
||||
'http://localhost:4173/api/tomtom/status*',
|
||||
'http://localhost:4173/api/tomtom/flow/*',
|
||||
'https://tiles.openfreemap.org/*',
|
||||
'https://terrain.reearth.land/*',
|
||||
]);
|
||||
await paused({
|
||||
requestId: 'terrain',
|
||||
request: { url: 'https://terrain.reearth.land/layer.json' },
|
||||
});
|
||||
const reply = sent.at(-1);
|
||||
assert.equal(reply.name, 'Fetch.fulfillRequest');
|
||||
assert.equal(
|
||||
JSON.parse(Buffer.from(reply.args.body, 'base64')).format,
|
||||
'heightmap-1.0',
|
||||
);
|
||||
assert.ok(
|
||||
reply.args.responseHeaders.some(
|
||||
(h) => h.name === 'access-control-allow-origin' && h.value === '*',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('keyless interception refuses and records unexpected flow requests', async () => {
|
||||
const { installTrafficRoutes } =
|
||||
await import('../../scripts/traffic-fixtures.mjs');
|
||||
const sent = [],
|
||||
flows = [];
|
||||
let paused;
|
||||
const session = {
|
||||
on: (_, handler) => {
|
||||
paused = handler;
|
||||
},
|
||||
send: async (name, args) => {
|
||||
sent.push({ name, args });
|
||||
},
|
||||
};
|
||||
await installTrafficRoutes(
|
||||
{ createCDPSession: async () => session, isClosed: () => false },
|
||||
{
|
||||
origin: 'http://localhost:4173',
|
||||
keyless: true,
|
||||
onFlow: (url) => flows.push(url),
|
||||
},
|
||||
);
|
||||
await paused({
|
||||
requestId: 'status',
|
||||
request: { url: 'http://localhost:4173/api/tomtom/status' },
|
||||
});
|
||||
assert.deepEqual(JSON.parse(Buffer.from(sent.at(-1).args.body, 'base64')), {
|
||||
hasKey: false,
|
||||
});
|
||||
await paused({
|
||||
requestId: 'flow',
|
||||
request: { url: 'http://localhost:4173/api/tomtom/flow/12/1/1.pbf' },
|
||||
});
|
||||
assert.equal(sent.at(-1).args.responseCode, 503);
|
||||
assert.equal(flows.length, 1);
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { getTransitFeed } from '../data/transitFeeds.js';
|
||||
import {
|
||||
chooseLiveCities,
|
||||
selectStyle,
|
||||
@@ -101,6 +102,18 @@ test('live visual cities follow local operating hours in summer and winter', ()
|
||||
const cities = chooseLiveCities(now);
|
||||
assert.ok(cities.length >= 1);
|
||||
for (const city of cities) {
|
||||
const feed = getTransitFeed(city.feedId);
|
||||
assert.ok(feed, `${city.id} has a registered feed`);
|
||||
const distanceKm =
|
||||
111.32 *
|
||||
Math.hypot(
|
||||
feed.center.lat - city.lat,
|
||||
(feed.center.lon - city.lon) * Math.cos((city.lat * Math.PI) / 180),
|
||||
);
|
||||
assert.ok(
|
||||
distanceKm < feed.loadRadiusKm,
|
||||
`${city.id} is inside its feed's admission region`,
|
||||
);
|
||||
const local = Number(
|
||||
new Intl.DateTimeFormat('en', {
|
||||
timeZone: city.zone,
|
||||
|
||||
+152
-19
@@ -1,13 +1,15 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import {
|
||||
BROWSER_CSP,
|
||||
createBrowserViteConfig,
|
||||
escapeFsDenyPath,
|
||||
} from '../../build/vite.js';
|
||||
import { rm, writeFile } from 'node:fs/promises';
|
||||
import { request } from 'node:http';
|
||||
import path from 'node:path';
|
||||
import { build, createServer, preview } from 'vite';
|
||||
import { BROWSER_CSP, createBrowserViteConfig, escapeFsDenyPath } from '../../build/vite.js';
|
||||
import standaloneConfig, * as compatibility from '../../vite.config.js';
|
||||
import * as providers from '../../server/providers/local.js';
|
||||
import { apiNotFoundPlugin } from '../../server/standalone/api-not-found.js';
|
||||
import { makeFixtureRoot } from './fixtureRoot.mjs';
|
||||
|
||||
test('explicit build inputs preserve browser-only defines, plugin order and loopback protections', () => {
|
||||
const plugin = { name: 'fixture-provider' };
|
||||
@@ -16,24 +18,18 @@ test('explicit build inputs preserve browser-only defines, plugin order and loop
|
||||
googleApiKey: 'browser-fixture',
|
||||
cesiumToken: 'ion-fixture',
|
||||
});
|
||||
// The Host check runs before every other plugin's middleware.
|
||||
assert.equal(config.plugins[0].name, 'host-check');
|
||||
// API response headers, then the Host check, run before every other
|
||||
// plugin's middleware.
|
||||
assert.equal(config.plugins[0].name, 'api-response-headers');
|
||||
assert.equal(config.plugins[0].enforce, 'pre');
|
||||
assert.equal(config.plugins[3], plugin);
|
||||
assert.equal(config.plugins[1].name, 'host-check');
|
||||
assert.equal(config.plugins[1].enforce, 'pre');
|
||||
assert.equal(config.plugins[4], plugin);
|
||||
assert.equal(config.server.host, 'localhost');
|
||||
assert.equal(config.server.port, 4173);
|
||||
assert.deepEqual(config.server.allowedHosts, ['localhost', '127.0.0.1']);
|
||||
assert.ok(config.server.fs.deny.includes('**/ENVIRONMENT'));
|
||||
assert.ok(config.server.fs.deny.includes('.env.*'));
|
||||
assert.ok(config.server.fs.deny.includes('**/.gev-logs/**'));
|
||||
assert.ok(config.server.fs.deny.includes('**/.gev-cache/**'));
|
||||
assert.ok(
|
||||
config.server.fs.deny.some((pattern) => pattern.endsWith('/dist/**')),
|
||||
);
|
||||
assert.equal(
|
||||
escapeFsDenyPath('C:/Users/a/gods-eye-view (1)'),
|
||||
'C:/Users/a/gods-eye-view \\(1\\)',
|
||||
);
|
||||
assert.equal(config.server.headers['X-Frame-Options'], 'DENY');
|
||||
assert.equal(config.server.headers['X-Content-Type-Options'], 'nosniff');
|
||||
const csp = config.server.headers['Content-Security-Policy'];
|
||||
@@ -88,7 +84,7 @@ test('build helper does not discover environment values or construct local provi
|
||||
undefined,
|
||||
);
|
||||
assert.deepEqual(
|
||||
config.plugins.slice(3).map((plugin) => plugin.name),
|
||||
config.plugins.slice(4).map((plugin) => plugin.name),
|
||||
['embed-framing', 'panel-build'],
|
||||
);
|
||||
} finally {
|
||||
@@ -101,8 +97,19 @@ test('root config retains existing named exports and standalone provider order',
|
||||
for (const [name, value] of Object.entries(providers))
|
||||
assert.equal(compatibility[name], value, name);
|
||||
const config = standaloneConfig({ mode: 'test' });
|
||||
const standaloneProviders = config.plugins
|
||||
.slice(4, -5)
|
||||
.map((plugin) => plugin.name);
|
||||
assert.equal(
|
||||
standaloneProviders.filter((name) => name === 'gemini-live-proxy').length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
standaloneProviders.indexOf('gemini-live-proxy'),
|
||||
standaloneProviders.indexOf('openai-realtime-proxy') + 1,
|
||||
);
|
||||
assert.deepEqual(
|
||||
config.plugins.slice(3, -5).map((plugin) => plugin.name),
|
||||
standaloneProviders.filter((name) => name !== 'gemini-live-proxy'),
|
||||
providers.localProviderPlugins().map((plugin) => plugin.name),
|
||||
);
|
||||
assert.equal(config.plugins.at(-6).name, 'gev-key-setup');
|
||||
@@ -259,3 +266,129 @@ test('the CSP admits the event media embeds and the Maps JavaScript API, and no
|
||||
// X posts load in X's own frames.
|
||||
assert.ok(frames.includes('https://platform.twitter.com'));
|
||||
});
|
||||
|
||||
/** A raw GET, so the Host header and an absolute-form target are sent as given. */
|
||||
function rawGet(port, target, headers = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = request(
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
path: target,
|
||||
headers: { host: `localhost:${port}`, ...headers },
|
||||
},
|
||||
(res) => {
|
||||
res.resume();
|
||||
res.on('end', () =>
|
||||
resolve({ status: res.statusCode, headers: res.headers }),
|
||||
);
|
||||
},
|
||||
);
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
test('every API response carries the API policy and nosniff on real dev and preview servers', async (t) => {
|
||||
const policy = "default-src 'none'; sandbox";
|
||||
const root = await makeFixtureRoot('gev-api-headers-');
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
await writeFile(
|
||||
path.join(root, 'index.html'),
|
||||
'<!doctype html><title>Fixture</title><p>x</p>',
|
||||
);
|
||||
const install = (server) => {
|
||||
server.middlewares.use('/api/fixture', (req, res) => {
|
||||
const route = new URL(req.url, 'http://localhost').pathname;
|
||||
if (route === '/ok') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end('{}');
|
||||
} else if (route === '/error') {
|
||||
res.statusCode = 500;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end('{"error":"fixture"}');
|
||||
} else if (route === '/partial') {
|
||||
res.writeHead(206, { 'Content-Range': 'bytes 0-0/2' });
|
||||
res.end('x');
|
||||
} else if (route === '/own-policy') {
|
||||
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
|
||||
res.end('{}');
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Security-Policy': 'default-src *' });
|
||||
res.end('{}');
|
||||
}
|
||||
});
|
||||
};
|
||||
const fixture = {
|
||||
name: 'fixture-provider',
|
||||
configureServer: install,
|
||||
configurePreviewServer: install,
|
||||
};
|
||||
const built = createBrowserViteConfig({ plugins: [fixture] });
|
||||
const base = {
|
||||
root,
|
||||
configFile: false,
|
||||
envFile: false,
|
||||
publicDir: false,
|
||||
logLevel: 'silent',
|
||||
};
|
||||
await build(base);
|
||||
for (const isPreview of [false, true]) {
|
||||
const server = await (isPreview ? preview : createServer)({
|
||||
...base,
|
||||
plugins: [
|
||||
...built.plugins.filter(
|
||||
(plugin) =>
|
||||
plugin === fixture ||
|
||||
['api-response-headers', 'host-check'].includes(plugin.name),
|
||||
),
|
||||
apiNotFoundPlugin(),
|
||||
],
|
||||
server: { host: '127.0.0.1', port: 0, hmr: false },
|
||||
preview: { host: '127.0.0.1', port: 0 },
|
||||
});
|
||||
if (!isPreview) await server.listen();
|
||||
const { port } = server.httpServer.address();
|
||||
const mode = isPreview ? 'preview' : 'dev';
|
||||
try {
|
||||
for (const [target, status, csp = policy, headers] of [
|
||||
['/api/fixture/ok', 200],
|
||||
['/api/fixture/error', 500],
|
||||
['/api/fixture/partial', 206],
|
||||
['/api/nope', 404],
|
||||
['/api', 404],
|
||||
['/api/fixture/ok?x=1', 200],
|
||||
// Spellings the router mounts as the same routes.
|
||||
['/API/fixture/ok', 200],
|
||||
['/Api/fixture/error', 500],
|
||||
['/aPi/nope', 404],
|
||||
['/API', 404],
|
||||
['/api.x', 404],
|
||||
[`http://localhost:${port}/api/fixture/ok`, 200],
|
||||
[`http://localhost:${port}/API/nope`, 404],
|
||||
// The router keeps dot segments in an absolute-form target's path.
|
||||
['http://elsewhere.example/api/../x', 404],
|
||||
['http://elsewhere.example/API/%2e%2e/x', 404],
|
||||
['/api/fixture/ok', 403, policy, { host: 'elsewhere.example' }],
|
||||
// A route's own policy is added to the API policy, never replaces it.
|
||||
['/api/fixture/own-policy', 200, `${policy}, frame-ancestors 'none'`],
|
||||
['/api/fixture/own-policy-head', 200, `${policy}, default-src *`],
|
||||
]) {
|
||||
const res = await rawGet(port, target, headers);
|
||||
const label = `${mode} ${target} ${JSON.stringify(headers || {})}`;
|
||||
assert.equal(res.status, status, label);
|
||||
assert.equal(res.headers['content-security-policy'], csp, label);
|
||||
assert.equal(res.headers['x-content-type-options'], 'nosniff', label);
|
||||
}
|
||||
// The app document and other paths keep the server's own headers.
|
||||
for (const target of ['/', '/apiary', '/src/api/x.js'])
|
||||
assert.notEqual(
|
||||
(await rawGet(port, target)).headers['content-security-policy'],
|
||||
policy,
|
||||
`${mode} ${target}`,
|
||||
);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -234,6 +234,19 @@ test('typed argument checks feed full-call accuracy without changing the legacy
|
||||
const inMeters = gradeCore(altitude, [{ name: 'analyst_query', args: { layers: ['flights'], filters: [{ field: 'altitudeM', op: 'gt', value: 12192 }] } }]);
|
||||
assert.equal(inMeters.fullCallOk, true);
|
||||
assert.equal(where('set_hud', (a) => a.visible === 'off')({ name: 'set_hud', args: { visible: 'off' } }), true);
|
||||
const globe = byPhrase('Keep spinning the Earth slowly to the left');
|
||||
const globeArgs = { motion: 'rotate_globe', direction: 'left', speed: 'slow' };
|
||||
for (const mode of [undefined, 'continuous', 'once', null]) {
|
||||
assert.equal(gradeCore(globe, [{ name: 'move_camera', args: { ...globeArgs, mode } }]).fullCallOk, mode === undefined || mode === 'continuous');
|
||||
}
|
||||
assert.equal(gradeCore(globe, [{ name: 'move_camera', args: { ...globeArgs, direction: 'right' } }]).fullCallOk, false);
|
||||
assert.equal(gradeCore(globe, [{ name: 'move_camera', args: { ...globeArgs, speed: 'normal' } }]).fullCallOk, false);
|
||||
const right = byPhrase('Rotate the globe to the right');
|
||||
assert.equal(gradeCore(right, [{ name: 'move_camera', args: { motion: 'rotate_globe' } }]).fullCallOk, true);
|
||||
for (const direction of ['right', 'left', null]) {
|
||||
assert.equal(gradeCore(right, [{ name: 'move_camera', args: { motion: 'rotate_globe', direction } }]).fullCallOk, direction === 'right');
|
||||
}
|
||||
assert.equal(gradeCore(right, [{ name: 'move_camera', args: { motion: 'rotate', direction: 'right' } }]).fullCallOk, false);
|
||||
});
|
||||
|
||||
test('gradeDialogue scores turn 2 in the context of turn 1', () => {
|
||||
|
||||
Reference in new issue
Block a user