mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix(tools): keep results when a timestamp is outside Date's range (#1045)
isoTime returned toISOString() for any finite number, which throws past ±8.64e15 ms, so one bad record (e.g. a bike-share last_reported) failed the whole tool answer. Such timestamps are now null. The voice analyst's result rows had the same conversion and get the same guard. Co-authored-by: Sameh Khamis <[email protected]>
This commit is contained in:
1 parent
84bfba8d39
commit
fd5a1ef856
6 files changed
+89
-3
No files matched your search
@@ -36,6 +36,8 @@
|
||||
|
||||
- Director timeline seeks cross the dateline along the short longitude arc.
|
||||
|
||||
- Analyst queries preserve useful rows when an upstream timestamp is outside the supported date range.
|
||||
|
||||
- Fire detections with missing or impossible positions are skipped while genuine zero coordinates remain valid.
|
||||
|
||||
- Frequently reused terrain batches retain their cached heights ahead of unused cells.
|
||||
|
||||
@@ -75,6 +75,8 @@ Windows/Claude Desktop Cowork confirmation remains outstanding (#927).
|
||||
|
||||
Tool argument string lengths follow JSON Schema Unicode code-point counts, so supplementary characters count once rather than as two UTF-16 units.
|
||||
|
||||
Query timestamps outside the representable Date range are reported as null, preserving the rest of the result row.
|
||||
|
||||
`gods-eye-view/tools` defines queries that answer questions from the app's data,
|
||||
and `gods-eye-view/tools/mcp` exposes a composed catalog over the Model Context
|
||||
Protocol. `npm run mcp` serves Core's tools over stdio to a local MCP client,
|
||||
|
||||
@@ -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". */
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -4808,8 +4808,9 @@ function compactAnalystItem(r) {
|
||||
for (const [key, spec] of Object.entries(fields)) {
|
||||
if (!present(r[key]) || key in compact) continue;
|
||||
if (spec.type === 'time') {
|
||||
const ms = Number(r[key]);
|
||||
if (Number.isFinite(ms)) compact.time = new Date(ms).toISOString();
|
||||
// A finite value can still be past Date's range, where toISOString throws.
|
||||
const time = new Date(Number(r[key]));
|
||||
if (Number.isFinite(time.getTime())) compact.time = time.toISOString();
|
||||
} else compact[key] = r[key];
|
||||
}
|
||||
if (present(r.ageHours)) compact.ageHours = r.ageHours;
|
||||
|
||||
@@ -3459,6 +3459,26 @@ test('analyst_query refusals carry their code and the allowed values', async ()
|
||||
assert.match(ok.items[0].time, /^\d{4}-\d\d-\d\dT/);
|
||||
});
|
||||
|
||||
test('analyst_query keeps its answer when one record has an out-of-range time', async () => {
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
const now = Date.now();
|
||||
const earthquakes = {
|
||||
getStats: () => ({ count: 2, lastUpdate: now }),
|
||||
getAnalystRecords: () => [
|
||||
{ id: 'q', lat: 0, lon: 0, magnitude: 5, timeMs: now },
|
||||
{ id: 'r', lat: 1, lon: 1, magnitude: 4, timeMs: 8.64e15 + 1 },
|
||||
],
|
||||
};
|
||||
const { runner } = manifestRunner({ earthquakes }, { enabled: ['earthquakes'] });
|
||||
const result = await runner('analyst_query', { layers: ['earthquakes'], scope: { kind: 'anywhere' } });
|
||||
assert.equal(result.ok, true);
|
||||
const q = result.items.find((item) => item.id === 'q');
|
||||
assert.match(q.time, /^\d{4}-\d\d-\d\dT/);
|
||||
const r = result.items.find((item) => item.id === 'r');
|
||||
assert.ok(r, 'the record is still listed');
|
||||
assert.equal(r.time, undefined, 'an unrepresentable time is omitted');
|
||||
});
|
||||
|
||||
test('spoken layer names from the manifest reach the newer layers', async () => {
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
const stub = () => ({ getStats: () => ({ count: 0 }) });
|
||||
|
||||
Reference in new issue
Block a user