mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge branch 'main' into feat/bikeshare-norway
This commit is contained in:
260 files changed
+28882
-1788
No files matched your search
@@ -0,0 +1,51 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { composeCatalog, defineTool } from './catalog.js';
|
||||
|
||||
for (const intercept of [false, true]) {
|
||||
test(`cancelled query refuses execution${intercept ? ' after an awaited interceptor' : ''}`, async () => {
|
||||
let calls = 0;
|
||||
const controller = new AbortController();
|
||||
const query = defineTool({
|
||||
name: 'fixture_query',
|
||||
kind: 'query',
|
||||
title: 'Fixture',
|
||||
description: 'Offline fixture',
|
||||
inputSchema: { type: 'object' },
|
||||
run: async () => {
|
||||
calls++;
|
||||
return { summary: 'fixture', data: {} };
|
||||
},
|
||||
});
|
||||
const catalog = composeCatalog({
|
||||
tools: [query],
|
||||
interceptors: intercept
|
||||
? [
|
||||
async (call, next) => {
|
||||
await Promise.resolve();
|
||||
controller.abort();
|
||||
return next(call);
|
||||
},
|
||||
]
|
||||
: [],
|
||||
});
|
||||
if (!intercept) controller.abort();
|
||||
await assert.rejects(
|
||||
catalog.call('fixture_query', {}, { signal: controller.signal }),
|
||||
{ name: 'AbortError' },
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
const fresh = composeCatalog({ tools: [query] });
|
||||
assert.equal(
|
||||
(
|
||||
await fresh.call(
|
||||
'fixture_query',
|
||||
{},
|
||||
{ signal: new AbortController().signal },
|
||||
)
|
||||
).summary,
|
||||
'fixture',
|
||||
);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
}
|
||||
@@ -178,6 +178,7 @@ export function composeCatalog({
|
||||
return chain({ tool, args, signal, ...(parent ? { parent } : {}) });
|
||||
};
|
||||
const execute = async ({ tool, args, signal }) => {
|
||||
signal?.throwIfAborted();
|
||||
const problems = validateValue(tool.inputSchema, args);
|
||||
if (problems.length)
|
||||
throw new ToolError('invalid_arguments', problems.join('; '));
|
||||
|
||||
+11
-1
@@ -15,7 +15,8 @@ export const PANEL_WORKER_PRELUDE_PATH = 'cesium/worker-prelude.js';
|
||||
/**
|
||||
* Sites the app's browser code loads from directly: map imagery, 3D tiles,
|
||||
* terrain, fonts, and Street Level's Mapillary Graph API and image CDN (its
|
||||
* coverage tiles come through the app's server). Everything from the app's
|
||||
* coverage tiles come through the app's server), Google Street View and
|
||||
* KartaView. Everything from the app's
|
||||
* own server comes through the MCP server.
|
||||
*/
|
||||
const PROVIDER_ORIGINS = Object.freeze([
|
||||
@@ -37,8 +38,17 @@ const PROVIDER_ORIGINS = Object.freeze([
|
||||
'https://fonts.googleapis.com',
|
||||
'https://fonts.gstatic.com',
|
||||
'https://graph.mapillary.com',
|
||||
// Google Street View's tiles, controls and contributed photos (the Maps
|
||||
// JavaScript API itself comes from maps.googleapis.com above).
|
||||
'https://streetviewpixels-pa.googleapis.com',
|
||||
'https://maps.gstatic.com',
|
||||
'https://*.ggpht.com',
|
||||
// Mapillary photos come from regional hosts, e.g. scontent-man2-1.xx.fbcdn.net.
|
||||
'https://*.xx.fbcdn.net',
|
||||
// KartaView's API, its image proxy and its photo storage (storage1…storage13).
|
||||
'https://api.kartaview.org',
|
||||
'https://cdn.kartaview.org',
|
||||
'https://*.openstreetcam.org',
|
||||
]);
|
||||
|
||||
export const MCP_APP_MIME_TYPE = 'text/html;profile=mcp-app';
|
||||
|
||||
@@ -169,3 +169,20 @@ test('the package exports the panel resource and its runtime together', async ()
|
||||
assert.equal(panel.PANEL_BASE, '/panel/');
|
||||
assert.match(resource.text, /panelRuntime|function/);
|
||||
});
|
||||
|
||||
test('the panel lets Street Level reach Google Street View from inside the embed', () => {
|
||||
const { csp } = createGlobePanelResource({
|
||||
runtime: panelRuntime,
|
||||
panelKey: 'test-key',
|
||||
})._meta.ui;
|
||||
// The API script and lookups, panorama tiles, control images, contributed photos.
|
||||
for (const url of [
|
||||
'https://maps.googleapis.com/maps/api/js?key=k&v=weekly',
|
||||
'https://streetviewpixels-pa.googleapis.com/v1/tile?panoid=p&x=0&y=0&zoom=1',
|
||||
'https://maps.gstatic.com/mapfiles/api-3/images/cb_scout5_hdpi.png',
|
||||
'https://lh3.ggpht.com/p/photo.jpg',
|
||||
]) {
|
||||
assert.ok(allows(csp.connectDomains, url), `connect-src allows ${url}`);
|
||||
assert.ok(allows(csp.resourceDomains, url), `img-src allows ${url}`);
|
||||
}
|
||||
});
|
||||
@@ -106,6 +106,34 @@ test('bike share joins station information with live status inside the area', as
|
||||
assert.equal(result.data.rows[1].capacity, 10);
|
||||
});
|
||||
|
||||
test('bike share returns localized names without losing stations with malformed names', async () => {
|
||||
const informationUrl = 'https://gbfs.example/austin/station_information.json';
|
||||
const information = structuredClone(documents[informationUrl]);
|
||||
information.version = '3.0';
|
||||
information.data.stations[0].name = [
|
||||
{ language: 'hu', text: 'Magyar név' },
|
||||
{ language: 'en', text: 'Congress & 2nd' },
|
||||
];
|
||||
information.data.stations[2].name = [null, { language: 42, text: [] }];
|
||||
const source = bikeshare([system('austin', 'Austin, TX', 30.27, -97.74)]);
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
bikeshare: {
|
||||
...source,
|
||||
getStations: (url) =>
|
||||
url === informationUrl ? information : source.getStations(url),
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await catalog.call('get_bike_share', { area: downtown });
|
||||
assert.equal(result.data.total, 2);
|
||||
assert.equal(result.data.rows[0].name, 'Congress & 2nd');
|
||||
assert.equal(result.data.rows[0].bikes_available, 4);
|
||||
assert.equal(result.data.rows[1].name, null);
|
||||
assert.doesNotMatch(result.summary, /unavailable/);
|
||||
});
|
||||
|
||||
test('bike share reports uncovered areas and failed systems honestly', async () => {
|
||||
const none = composeCatalog({
|
||||
tools: coreTools,
|
||||
|
||||
@@ -30,6 +30,27 @@ test('TLE text parses into named entries with catalog numbers', () => {
|
||||
assert.equal(tleCatalogNumber('1 xx'), null);
|
||||
});
|
||||
|
||||
for (const missingLine of [1, 2]) {
|
||||
test(`TLE parsing resumes after a set missing line ${missingLine}`, () => {
|
||||
const sets = Array.from({ length: 8 }, (_, i) => {
|
||||
const lines = TLE.trim().split('\n').slice(0, 3);
|
||||
lines[0] = `SAT ${i}`;
|
||||
return lines;
|
||||
});
|
||||
sets[1].splice(missingLine, 1);
|
||||
const entries = parseTleText(sets.flat().join('\r\n'));
|
||||
assert.deepEqual(
|
||||
entries.map((entry) => entry.name),
|
||||
['SAT 0', 'SAT 2', 'SAT 3', 'SAT 4', 'SAT 5', 'SAT 6', 'SAT 7'],
|
||||
);
|
||||
assert.ok(
|
||||
entries.every(
|
||||
(entry) => entry.line1.startsWith('1 ') && entry.line2.startsWith('2 '),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('the next pass defaults to the ISS and reports times, peak and direction', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
|
||||
@@ -24,7 +24,9 @@ export function capRows(rows, limit = DEFAULT_LIMIT) {
|
||||
|
||||
/** Format an epoch millisecond time as ISO 8601, or null. */
|
||||
export function isoTime(ms) {
|
||||
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
||||
if (!Number.isFinite(ms)) return null;
|
||||
const date = new Date(ms);
|
||||
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
||||
}
|
||||
|
||||
/** "1 earthquake", "3 earthquakes". */
|
||||
|
||||
+14
-4
@@ -106,10 +106,20 @@ export function validateValue(schema, value, path = 'arguments') {
|
||||
fail('must be a string');
|
||||
break;
|
||||
}
|
||||
if (schema.minLength != null && value.length < schema.minLength)
|
||||
fail(`must be at least ${schema.minLength} characters`);
|
||||
if (schema.maxLength != null && value.length > schema.maxLength)
|
||||
fail(`must be at most ${schema.maxLength} characters`);
|
||||
if (schema.minLength != null || schema.maxLength != null) {
|
||||
// Code points, as JSON Schema counts. Counting stops once both bounds
|
||||
// are decided, so a huge argument costs no more than its limit.
|
||||
const limit = Math.max(
|
||||
schema.minLength ?? 0,
|
||||
schema.maxLength != null ? schema.maxLength + 1 : 0,
|
||||
);
|
||||
let length = 0;
|
||||
for (const _ of value) if (++length >= limit) break;
|
||||
if (schema.minLength != null && length < schema.minLength)
|
||||
fail(`must be at least ${schema.minLength} characters`);
|
||||
if (schema.maxLength != null && length > schema.maxLength)
|
||||
fail(`must be at most ${schema.maxLength} characters`);
|
||||
}
|
||||
if (
|
||||
schema.pattern !== undefined &&
|
||||
!new RegExp(schema.pattern, 'u').test(value)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { validateValue } from './schema.js';
|
||||
test('tool string schemas count Unicode characters rather than UTF-16 units', () => {
|
||||
const value = '𠮷野家😀';
|
||||
assert.deepEqual(validateValue({ type: 'string', maxLength: 4 }, value), []);
|
||||
assert.equal(
|
||||
validateValue({ type: 'string', minLength: 5 }, value).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
validateValue({ type: 'string', maxLength: 3 }, value).length,
|
||||
1,
|
||||
);
|
||||
assert.deepEqual(
|
||||
validateValue({ type: 'string', minLength: 4, maxLength: 4 }, 'abcd'),
|
||||
[],
|
||||
);
|
||||
assert.equal(validateValue({ type: 'string', minLength: 2 }, '𠮷').length, 1);
|
||||
});
|
||||
|
||||
test('tool string bounds hold at their edges and on huge arguments', () => {
|
||||
const string = (bounds) => ({ type: 'string', ...bounds });
|
||||
assert.deepEqual(validateValue(string({ maxLength: 3 }), '😀😀😀'), []);
|
||||
assert.equal(validateValue(string({ maxLength: 3 }), '😀😀😀😀').length, 1);
|
||||
assert.deepEqual(validateValue(string({ minLength: 3 }), '😀😀😀'), []);
|
||||
assert.deepEqual(validateValue(string({}), 'x'.repeat(1e6)), []);
|
||||
// Counting all of it takes roughly 0.7 s; stopping at the limit takes none.
|
||||
const huge = 'x'.repeat(1e8);
|
||||
const started = performance.now();
|
||||
assert.equal(validateValue(string({ maxLength: 60 }), huge).length, 1);
|
||||
assert.ok(performance.now() - started < 200, 'stops counting past the limit');
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { isoTime } from './results.js';
|
||||
import { getBikeShare } from './queries/mobility.js';
|
||||
test('out-of-range finite timestamps are unknown rather than throwing', async () => {
|
||||
for (const value of [1e30, -1e30, NaN, Infinity, null])
|
||||
assert.equal(isoTime(value), null);
|
||||
assert.equal(isoTime(0), '1970-01-01T00:00:00.000Z');
|
||||
assert.equal(isoTime(8640000000000000), '+275760-09-13T00:00:00.000Z');
|
||||
assert.equal(isoTime(-8640000000000000), '-271821-04-20T00:00:00.000Z');
|
||||
for (const value of [8640000000000001, -8640000000000001, -Infinity])
|
||||
assert.equal(isoTime(value), null);
|
||||
assert.equal(isoTime(-1), '1969-12-31T23:59:59.999Z');
|
||||
const station = {
|
||||
station_id: 's1',
|
||||
name: 'Fixture',
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
capacity: 4,
|
||||
};
|
||||
const result = await getBikeShare.run(
|
||||
{ area: { bbox: [-1, -1, 1, 1] } },
|
||||
{
|
||||
services: {
|
||||
bikeshare: {
|
||||
systems: [
|
||||
{
|
||||
id: 'fixture',
|
||||
city: 'Fixture',
|
||||
centerLat: 0,
|
||||
centerLon: 0,
|
||||
loadRadiusKm: 30,
|
||||
stationInformationUrl: 'information',
|
||||
stationStatusUrl: 'status',
|
||||
},
|
||||
],
|
||||
async getStations(url) {
|
||||
return {
|
||||
data: {
|
||||
stations:
|
||||
url === 'information'
|
||||
? [station]
|
||||
: [
|
||||
{
|
||||
station_id: 's1',
|
||||
last_reported: 1e30,
|
||||
num_bikes_available: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.equal(result.data.returned, 1);
|
||||
assert.equal(result.data.rows[0].last_reported, null);
|
||||
});
|
||||
Reference in new issue
Block a user