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:
Sameh Khamis committed 2026-10-01 12:05:50 -07:00
1 parent 9895942633
commit 22c3fe40b0
16 files changed
+1111 -16

No files matched your search

+276
View File
@@ -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',
);
});