mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat: add aviation and satellite tools
Add aircraft_in_area, find_aircraft, get_aircraft_track and get_aircraft_info over the live aircraft sources, with an optional military feed, and next_satellite_pass and satellites_overhead over the satellite catalog and the existing pass predictor. Source failures from the live contract now become tool errors with the matching code, so a rate limit reaches clients as retry_later with a wait time. Tool schemas may use pattern.
This commit is contained in:
1 parent
9895942633
commit
22c3fe40b0
16 files changed
+1111
-16
No files matched your search
@@ -0,0 +1,276 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { composeCatalog, coreTools } from '../index.js';
|
||||
import { LiveSourceError } from '../../sources/live/contract.js';
|
||||
|
||||
const record = (id, latitude, longitude, extra = {}) => ({
|
||||
id,
|
||||
reference: id,
|
||||
latitude,
|
||||
longitude,
|
||||
callsign: null,
|
||||
originCountry: null,
|
||||
positionTimeMs: Date.UTC(2026, 0, 1),
|
||||
contactTimeMs: Date.UTC(2026, 0, 1, 0, 0, 5),
|
||||
baroAltitudeM: 10000,
|
||||
ellipsoidAltitudeM: null,
|
||||
onGround: false,
|
||||
speedMps: 230.04,
|
||||
courseDeg: 90.4,
|
||||
verticalRateMps: 0,
|
||||
category: null,
|
||||
typeCode: null,
|
||||
registration: null,
|
||||
operator: null,
|
||||
...extra,
|
||||
});
|
||||
const snapshot = (records, extra = {}) => ({
|
||||
records,
|
||||
complete: true,
|
||||
source: 'Test feed',
|
||||
coverage: 'test coverage',
|
||||
observedAtMs: Date.UTC(2026, 0, 1),
|
||||
freshness: 'current',
|
||||
...extra,
|
||||
});
|
||||
const feed = (records, { track = [], enrichment = {}, ...extra } = {}) => {
|
||||
const calls = [];
|
||||
return {
|
||||
calls,
|
||||
async getSnapshot(query) {
|
||||
calls.push(query);
|
||||
return snapshot(records, extra);
|
||||
},
|
||||
async getTrack(reference) {
|
||||
calls.push(reference);
|
||||
return { records: track, complete: false };
|
||||
},
|
||||
async getEnrichment({ kind, id }) {
|
||||
calls.push(`${kind}:${id}`);
|
||||
return enrichment[`${kind}:${id}`] ?? { found: false };
|
||||
},
|
||||
};
|
||||
};
|
||||
const civil = [
|
||||
record('aaa111', 37.7, -122.4, {
|
||||
callsign: 'UAL123',
|
||||
originCountry: 'United States',
|
||||
}),
|
||||
record('bbb222', 37.58, -122.28, { onGround: true, baroAltitudeM: 0 }),
|
||||
record('ccc333', 51.5, -0.1, { callsign: 'BAW1' }),
|
||||
];
|
||||
const military = [
|
||||
record('ae0001', 37.6, -122.3, {
|
||||
callsign: 'RCH123',
|
||||
registration: '07-7175',
|
||||
typeCode: 'C17',
|
||||
}),
|
||||
];
|
||||
const bay = { lat: 37.6, lon: -122.3, radius_km: 60 };
|
||||
|
||||
test('aircraft in an area are filtered, nearest first, with source details', async () => {
|
||||
const aircraft = feed(civil);
|
||||
const catalog = composeCatalog({ tools: coreTools, services: { aircraft } });
|
||||
const result = await catalog.call('aircraft_in_area', { area: bay });
|
||||
assert.equal(result.summary, '2 aircraft in 60 km around 37.600, -122.300.');
|
||||
assert.deepEqual(
|
||||
result.data.rows.map((row) => row.id),
|
||||
['bbb222', 'aaa111'],
|
||||
);
|
||||
assert.deepEqual(aircraft.calls[0], { latitude: 37.6, longitude: -122.3 });
|
||||
assert.deepEqual(result.data.rows[1], {
|
||||
id: 'aaa111',
|
||||
callsign: 'UAL123',
|
||||
lat: 37.7,
|
||||
lon: -122.4,
|
||||
altitude_m: 10000,
|
||||
on_ground: false,
|
||||
speed_mps: 230,
|
||||
course_deg: 90,
|
||||
vertical_rate_mps: 0,
|
||||
type_code: null,
|
||||
registration: null,
|
||||
operator: null,
|
||||
origin_country: 'United States',
|
||||
last_contact: '2026-01-01T00:00:05.000Z',
|
||||
distance_km: 14.2,
|
||||
});
|
||||
assert.equal(result.data.source, 'Test feed');
|
||||
assert.equal(result.data.observed_at, '2026-01-01T00:00:00.000Z');
|
||||
|
||||
const airborne = await catalog.call('aircraft_in_area', {
|
||||
area: bay,
|
||||
airborne_only: true,
|
||||
});
|
||||
assert.deepEqual(
|
||||
airborne.data.rows.map((row) => row.id),
|
||||
['aaa111'],
|
||||
);
|
||||
const low = await catalog.call('aircraft_in_area', {
|
||||
area: bay,
|
||||
max_altitude_m: 100,
|
||||
});
|
||||
assert.deepEqual(
|
||||
low.data.rows.map((row) => row.id),
|
||||
['bbb222'],
|
||||
);
|
||||
});
|
||||
|
||||
test('the military feed is used on request and reported when missing', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
aircraft: feed(civil),
|
||||
military: feed(military, { freshness: 'stale' }),
|
||||
},
|
||||
});
|
||||
const result = await catalog.call('aircraft_in_area', {
|
||||
area: bay,
|
||||
military: true,
|
||||
});
|
||||
assert.equal(
|
||||
result.summary,
|
||||
'1 military aircraft in 60 km around 37.600, -122.300 (data may be stale).',
|
||||
);
|
||||
assert.equal(result.data.rows[0].registration, '07-7175');
|
||||
const civilOnly = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { aircraft: feed(civil) },
|
||||
});
|
||||
await assert.rejects(
|
||||
civilOnly.call('aircraft_in_area', { area: bay, military: true }),
|
||||
(error) => error.code === 'unsupported',
|
||||
);
|
||||
});
|
||||
|
||||
test('aircraft are found by exactly one identifier across feeds', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { aircraft: feed(civil), military: feed(military) },
|
||||
});
|
||||
const byCallsign = await catalog.call('find_aircraft', {
|
||||
callsign: 'ual123',
|
||||
});
|
||||
assert.equal(byCallsign.summary, 'Found 1 aircraft with callsign UAL123.');
|
||||
assert.deepEqual(
|
||||
byCallsign.data.rows.map((row) => row.id),
|
||||
['aaa111'],
|
||||
);
|
||||
assert.equal(byCallsign.data.rows[0].distance_km, undefined);
|
||||
const byRegistration = await catalog.call('find_aircraft', {
|
||||
registration: '07-7175',
|
||||
});
|
||||
assert.deepEqual(
|
||||
byRegistration.data.rows.map((row) => row.id),
|
||||
['ae0001'],
|
||||
);
|
||||
const none = await catalog.call('find_aircraft', { icao24: 'ABCDEF' });
|
||||
assert.equal(
|
||||
none.summary,
|
||||
'No aircraft with icao24 ABCDEF is currently reported.',
|
||||
);
|
||||
await assert.rejects(catalog.call('find_aircraft', {}), /exactly one/);
|
||||
await assert.rejects(
|
||||
catalog.call('find_aircraft', { callsign: 'UAL123', icao24: 'aaa111' }),
|
||||
/exactly one/,
|
||||
);
|
||||
await assert.rejects(
|
||||
catalog.call('find_aircraft', { icao24: 'xyz' }),
|
||||
/must match/,
|
||||
);
|
||||
});
|
||||
|
||||
test('tracks are ordered and thinned to 200 points', async () => {
|
||||
const start = Date.UTC(2026, 0, 1);
|
||||
const track = Array.from({ length: 450 }, (_, index) => ({
|
||||
latitude: 37 + index / 1000,
|
||||
longitude: -122,
|
||||
observedAtMs: start + (449 - index) * 1000,
|
||||
baroAltitudeM: 1000,
|
||||
onGround: false,
|
||||
}));
|
||||
const aircraft = feed(civil, { track });
|
||||
const catalog = composeCatalog({ tools: coreTools, services: { aircraft } });
|
||||
const result = await catalog.call('get_aircraft_track', { icao24: 'AAA111' });
|
||||
assert.equal(aircraft.calls[0], 'aaa111');
|
||||
assert.equal(
|
||||
result.summary,
|
||||
'450 positions for aaa111 from 2026-01-01T00:00:00.000Z to 2026-01-01T00:07:29.000Z.',
|
||||
);
|
||||
assert.deepEqual(
|
||||
{
|
||||
total: result.data.total,
|
||||
returned: result.data.returned,
|
||||
thinned: result.data.thinned,
|
||||
},
|
||||
{ total: 450, returned: 200, thinned: true },
|
||||
);
|
||||
assert.equal(result.data.points[0].time, '2026-01-01T00:00:00.000Z');
|
||||
assert.equal(result.data.points.at(-1).time, '2026-01-01T00:07:29.000Z');
|
||||
const empty = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { aircraft: feed(civil) },
|
||||
});
|
||||
assert.equal(
|
||||
(await empty.call('get_aircraft_track', { icao24: 'aaa111' })).summary,
|
||||
'No recent track is available for aaa111.',
|
||||
);
|
||||
});
|
||||
|
||||
test('aircraft info combines type and route lookups', async () => {
|
||||
const aircraft = feed(civil, {
|
||||
enrichment: {
|
||||
'type:aaa111': {
|
||||
found: true,
|
||||
typeCode: 'B738',
|
||||
typeName: 'Boeing 737-800',
|
||||
registration: 'N12345',
|
||||
},
|
||||
'route:UAL123': {
|
||||
found: true,
|
||||
airline: 'United Airlines',
|
||||
origin: { code: 'SFO', name: 'San Francisco', lat: 37.6, lon: -122.4 },
|
||||
destination: { code: 'EWR', name: 'Newark', lat: 40.7, lon: -74.2 },
|
||||
},
|
||||
},
|
||||
});
|
||||
const catalog = composeCatalog({ tools: coreTools, services: { aircraft } });
|
||||
const both = await catalog.call('get_aircraft_info', {
|
||||
icao24: 'AAA111',
|
||||
callsign: 'ual123',
|
||||
});
|
||||
assert.equal(
|
||||
both.summary,
|
||||
'Aaa111 is Boeing 737-800 (N12345); UAL123 flies SFO to EWR (United Airlines).',
|
||||
);
|
||||
assert.equal(both.data.route.destination.name, 'Newark');
|
||||
const unknown = await catalog.call('get_aircraft_info', { callsign: 'XYZ9' });
|
||||
assert.equal(unknown.summary, 'No route is known for XYZ9.');
|
||||
assert.deepEqual(unknown.data, { aircraft: null, route: null });
|
||||
await assert.rejects(
|
||||
catalog.call('get_aircraft_info', {}),
|
||||
/icao24, callsign or both/,
|
||||
);
|
||||
});
|
||||
|
||||
test('live source failures become tool errors with retry guidance', async () => {
|
||||
const limited = {
|
||||
getSnapshot: async () => {
|
||||
throw new LiveSourceError('limited', 'OpenSky rate limited', {
|
||||
retryAfterMs: 45000,
|
||||
});
|
||||
},
|
||||
};
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { aircraft: limited },
|
||||
});
|
||||
await assert.rejects(
|
||||
catalog.call('aircraft_in_area', { area: bay }),
|
||||
(error) =>
|
||||
error.name === 'ToolError' &&
|
||||
error.code === 'retry_later' &&
|
||||
error.retryAfterSeconds === 45 &&
|
||||
error.message === 'OpenSky rate limited',
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user