diff --git a/src/tools/queries/aviation.js b/src/tools/queries/aviation.js index f5bc416ba..0bf520d0d 100644 --- a/src/tools/queries/aviation.js +++ b/src/tools/queries/aviation.js @@ -161,13 +161,31 @@ export const findAircraft = defineTool({ icao24: 'id', registration: 'registration', }[key]; - const feeds = [services.aircraft, services.military].filter(Boolean); - const snapshots = await Promise.all( - feeds.map((feed) => feed.getSnapshot({}, { signal })), + // Only the military feed reports registrations. + const feeds = [ + ...(key === 'registration' + ? [] + : [{ name: 'civil', feed: services.aircraft }]), + { name: 'military', feed: services.military }, + ].filter(({ feed }) => feed); + if (!feeds.length) + throw new ToolError( + 'unavailable', + 'The military aircraft feed is not available here', + ); + const settled = await Promise.allSettled( + feeds.map(({ feed }) => feed.getSnapshot({}, { signal })), ); + signal?.throwIfAborted(); + // A search succeeds when any feed answers; missing feeds are named. + const unavailable = feeds + .filter((_, index) => settled[index].status === 'rejected') + .map(({ name }) => name); + if (unavailable.length === feeds.length) throw settled[0].reason; const seen = new Set(); - const rows = snapshots - .flatMap((snapshot) => snapshot.records) + const rows = settled + .filter((result) => result.status === 'fulfilled') + .flatMap((result) => result.value.records) .filter( (record) => String(record[field] || '') @@ -177,10 +195,14 @@ export const findAircraft = defineTool({ .filter((record) => !seen.has(record.id) && seen.add(record.id)) .map((record) => aircraftRow(record)); return { - summary: rows.length - ? `Found ${countNoun(rows.length, 'aircraft', 'aircraft')} with ${key} ${wanted}.` - : `No aircraft with ${key} ${wanted} is currently reported.`, - data: capRows(rows, args.limit), + summary: + (rows.length + ? `Found ${countNoun(rows.length, 'aircraft', 'aircraft')} with ${key} ${wanted}.` + : `No aircraft with ${key} ${wanted} is currently reported.`) + + (unavailable.length + ? ` The ${unavailable.join(' and ')} feed did not answer.` + : ''), + data: { ...capRows(rows, args.limit), unavailable_feeds: unavailable }, }; }, }); diff --git a/src/tools/queries/aviation.test.mjs b/src/tools/queries/aviation.test.mjs index 811a066c4..7594029ce 100644 --- a/src/tools/queries/aviation.test.mjs +++ b/src/tools/queries/aviation.test.mjs @@ -298,3 +298,42 @@ test('live source failures become tool errors with retry guidance', async () => error.message === 'OpenSky rate limited', ); }); + +test('aircraft searches keep the feeds that answered and name the others', async () => { + const down = { + getSnapshot: async () => { + throw new LiveSourceError('unavailable', 'OpenSky HTTP 503'); + }, + }; + const military = feed([ + record('ae1234', 38, -77, { callsign: 'RCH123', registration: '05-5140' }), + ]); + const catalog = composeCatalog({ + tools: coreTools, + services: { aircraft: down, military }, + }); + const byCallsign = await catalog.call('find_aircraft', { + callsign: 'rch123', + }); + assert.equal( + byCallsign.summary, + 'Found 1 aircraft with callsign RCH123. The civil feed did not answer.', + ); + assert.deepEqual(byCallsign.data.unavailable_feeds, ['civil']); + const civil = feed([]); + const byRegistration = await composeCatalog({ + tools: coreTools, + services: { aircraft: civil, military }, + }).call('find_aircraft', { registration: '05-5140' }); + assert.equal(byRegistration.data.rows[0].id, 'ae1234'); + assert.deepEqual(byRegistration.data.unavailable_feeds, []); + assert.equal(civil.calls.length, 0); + await assert.rejects( + composeCatalog({ + tools: coreTools, + services: { aircraft: down, military: down }, + }).call('find_aircraft', { callsign: 'RCH123' }), + (error) => + error.code === 'unavailable' && error.message === 'OpenSky HTTP 503', + ); +});