mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix: tighten tool answers found in local testing
Unknown aircraft tracks read as empty instead of a server error, aircraft info names the aircraft and flight, the HUD caption sends the app's caption context, and satellite pass, satellites overhead and nearby places take a place name or point like the other tools.
This commit is contained in:
1 parent
1a03e7483d
commit
ec1adbdc19
9 files changed
+119
-64
No files matched your search
@@ -200,9 +200,13 @@ export const getAircraftTrack = defineTool({
|
||||
requires: ['aircraft'],
|
||||
async run(args, { services, signal }) {
|
||||
const icao24 = args.icao24.toLowerCase();
|
||||
const track = await feedFor(services, args.military).getTrack(icao24, {
|
||||
signal,
|
||||
});
|
||||
// A track server answers 404 for an aircraft it has no positions for.
|
||||
const track = await feedFor(services, args.military)
|
||||
.getTrack(icao24, { signal })
|
||||
.catch((error) => {
|
||||
if (error?.status === 404) return { records: [] };
|
||||
throw error;
|
||||
});
|
||||
const points = [...track.records].sort(
|
||||
(a, b) => a.observedAtMs - b.observedAtMs,
|
||||
);
|
||||
@@ -284,13 +288,13 @@ export const getAircraftInfo = defineTool({
|
||||
if (icao24)
|
||||
parts.push(
|
||||
aircraft
|
||||
? `${icao24} is ${aircraft.type_name || aircraft.type_code || 'an aircraft of unknown type'}${aircraft.registration ? ` (${aircraft.registration})` : ''}`
|
||||
? `aircraft ${icao24} is ${aircraft.type_name || aircraft.type_code || 'an aircraft of unknown type'}${aircraft.registration ? ` (${aircraft.registration})` : ''}`
|
||||
: `no type is known for ${icao24}`,
|
||||
);
|
||||
if (callsign)
|
||||
parts.push(
|
||||
flight
|
||||
? `${callsign} flies ${flight.origin?.code || flight.origin?.name} to ${flight.destination?.code || flight.destination?.name}${flight.airline ? ` (${flight.airline})` : ''}`
|
||||
? `flight ${callsign} flies ${flight.origin?.code || flight.origin?.name} to ${flight.destination?.code || flight.destination?.name}${flight.airline ? ` (${flight.airline})` : ''}`
|
||||
: `no route is known for ${callsign}`,
|
||||
);
|
||||
const summary = parts.join('; ');
|
||||
|
||||
@@ -215,6 +215,30 @@ test('tracks are ordered and thinned to 200 points', async () => {
|
||||
(await empty.call('get_aircraft_track', { icao24: 'aaa111' })).summary,
|
||||
'No recent track is available for aaa111.',
|
||||
);
|
||||
const failing = (status) => ({
|
||||
getSnapshot: async () => snapshot([]),
|
||||
getTrack: async () => {
|
||||
throw new LiveSourceError('unavailable', `OpenSky HTTP ${status}`, {
|
||||
status,
|
||||
});
|
||||
},
|
||||
});
|
||||
const unknown = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { aircraft: failing(404) },
|
||||
});
|
||||
assert.equal(
|
||||
(await unknown.call('get_aircraft_track', { icao24: 'abcdef' })).summary,
|
||||
'No recent track is available for abcdef.',
|
||||
);
|
||||
const down = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { aircraft: failing(502) },
|
||||
});
|
||||
await assert.rejects(
|
||||
down.call('get_aircraft_track', { icao24: 'abcdef' }),
|
||||
(error) => error.code === 'unavailable',
|
||||
);
|
||||
});
|
||||
|
||||
test('aircraft info combines type and route lookups', async () => {
|
||||
@@ -241,7 +265,7 @@ test('aircraft info combines type and route lookups', async () => {
|
||||
});
|
||||
assert.equal(
|
||||
both.summary,
|
||||
'Aaa111 is Boeing 737-800 (N12345); UAL123 flies SFO to EWR (United Airlines).',
|
||||
'Aircraft aaa111 is Boeing 737-800 (N12345); flight 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' });
|
||||
|
||||
@@ -25,6 +25,7 @@ const AWARENESS_RADIUS_KM = 250;
|
||||
const SECTIONS = [
|
||||
{
|
||||
key: 'weather',
|
||||
label: 'Weather',
|
||||
tool: getWeather,
|
||||
args: (area, center) => ({
|
||||
location: { lat: center.lat, lon: center.lon },
|
||||
@@ -32,26 +33,31 @@ const SECTIONS = [
|
||||
},
|
||||
{
|
||||
key: 'earthquakes',
|
||||
label: 'Earthquakes',
|
||||
tool: getEarthquakes,
|
||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||
},
|
||||
{
|
||||
key: 'fires',
|
||||
label: 'Active fires',
|
||||
tool: getActiveFires,
|
||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||
},
|
||||
{
|
||||
key: 'aircraft',
|
||||
label: 'Aircraft',
|
||||
tool: aircraftInArea,
|
||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||
},
|
||||
{
|
||||
key: 'vessels',
|
||||
label: 'Ships',
|
||||
tool: vesselsInArea,
|
||||
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
|
||||
},
|
||||
{
|
||||
key: 'cyclones',
|
||||
label: 'Tropical cyclones',
|
||||
tool: getCyclones,
|
||||
args: (area) => ({ area: area.argument }),
|
||||
},
|
||||
@@ -181,9 +187,30 @@ export const getHudCaption = defineTool({
|
||||
},
|
||||
requires: ['weather', 'summary'],
|
||||
async run(args, context) {
|
||||
const { area, center, lines } = await buildBrief(args, context);
|
||||
const { area, center, brief, lines } = await buildBrief(args, context);
|
||||
// The app's caption context: place labels and the layers that answered,
|
||||
// with their feed state.
|
||||
const answered = SECTIONS.filter(
|
||||
({ key }) => brief[key] && !brief[key].unavailable,
|
||||
);
|
||||
const layers = answered.map(({ key, label }) => ({
|
||||
id: key,
|
||||
name: label,
|
||||
feedState: 'nominal',
|
||||
}));
|
||||
const response = await context.services.summary.summarize(
|
||||
{ location: area.label, center, observations: lines },
|
||||
{
|
||||
placeLabels: [area.label],
|
||||
location: area.label,
|
||||
center,
|
||||
observations: lines,
|
||||
enabledLayerLabels: layers.map((layer) => layer.name),
|
||||
enabledLayers: layers,
|
||||
feedProvenance: {
|
||||
overall: layers.length ? 'nominal' : null,
|
||||
layers,
|
||||
},
|
||||
},
|
||||
{ signal: context.signal },
|
||||
);
|
||||
const caption = response?.data?.summary;
|
||||
|
||||
@@ -438,6 +438,9 @@ test('the HUD caption summarizes the brief through the summary service', async (
|
||||
);
|
||||
assert.equal(result.summary, 'Overcast skies over Austin');
|
||||
assert.equal(sent[0].location, 'Austin, Texas');
|
||||
assert.deepEqual(sent[0].placeLabels, ['Austin, Texas']);
|
||||
assert.deepEqual(sent[0].enabledLayerLabels, ['Weather']);
|
||||
assert.equal(sent[0].feedProvenance.overall, 'nominal');
|
||||
assert.match(sent[0].observations[0], /^Weather at /);
|
||||
const failing = {
|
||||
summarize: async () => ({
|
||||
|
||||
@@ -82,25 +82,25 @@ export const placesNearby = defineTool({
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
lat: { type: 'number', minimum: -90, maximum: 90 },
|
||||
lon: { type: 'number', minimum: -180, maximum: 180 },
|
||||
location: POINT_SCHEMA,
|
||||
radius_m: { type: 'number', minimum: 10, maximum: 5000 },
|
||||
limit: LIMIT_SCHEMA,
|
||||
},
|
||||
required: ['lat', 'lon'],
|
||||
required: ['location'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
requires: ['placeSearch'],
|
||||
async run(args, { services, signal }) {
|
||||
const point = await resolvePoint(args.location, { services, signal });
|
||||
const radiusM = Math.round(args.radius_m ?? 250);
|
||||
const result = await services.placeSearch.nearby(
|
||||
{ latitude: args.lat, longitude: args.lon, radiusM },
|
||||
{ latitude: point.lat, longitude: point.lon, radiusM },
|
||||
{ signal },
|
||||
);
|
||||
if (!result.configured) throw new ToolError('unavailable', NOT_CONFIGURED);
|
||||
const rows = result.places.map(placeRow);
|
||||
return {
|
||||
summary: `${countNoun(rows.length, 'place')} within ${radiusM} m.`,
|
||||
summary: `${countNoun(rows.length, 'place')} within ${radiusM} m of ${point.label}.`,
|
||||
data: capRows(rows, args.limit),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -93,11 +93,10 @@ test('nearby places use the given radius', async () => {
|
||||
services: { placeSearch: search },
|
||||
});
|
||||
const result = await catalog.call('places_nearby', {
|
||||
lat: 30.27,
|
||||
lon: -97.74,
|
||||
location: { lat: 30.27, lon: -97.74 },
|
||||
radius_m: 400,
|
||||
});
|
||||
assert.equal(result.summary, '1 place within 400 m.');
|
||||
assert.equal(result.summary, '1 place within 400 m of 30.2700, -97.7400.');
|
||||
assert.deepEqual(search.calls[0], {
|
||||
latitude: 30.27,
|
||||
longitude: -97.74,
|
||||
|
||||
+26
-26
@@ -6,6 +6,7 @@ import {
|
||||
lookAnglesAt,
|
||||
} from '../../data/satellitePass.js';
|
||||
import { parseTleText, tleCatalogNumber } from '../../sources/tle.js';
|
||||
import { POINT_SCHEMA, resolvePoint } from '../area.js';
|
||||
import { defineTool, ToolError } from '../catalog.js';
|
||||
import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js';
|
||||
|
||||
@@ -83,20 +84,6 @@ const COMPASS = [
|
||||
'NW',
|
||||
'NNW',
|
||||
];
|
||||
const OBSERVER = {
|
||||
lat: {
|
||||
type: 'number',
|
||||
minimum: -90,
|
||||
maximum: 90,
|
||||
description: 'Observer latitude.',
|
||||
},
|
||||
lon: {
|
||||
type: 'number',
|
||||
minimum: -180,
|
||||
maximum: 180,
|
||||
description: 'Observer longitude.',
|
||||
},
|
||||
};
|
||||
const GROUP_SCHEMA = {
|
||||
type: 'string',
|
||||
enum: GROUPS,
|
||||
@@ -154,7 +141,7 @@ export const nextSatellitePass = defineTool({
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...OBSERVER,
|
||||
location: POINT_SCHEMA,
|
||||
satellite: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
@@ -170,11 +157,12 @@ export const nextSatellitePass = defineTool({
|
||||
},
|
||||
hours: { type: 'number', minimum: 1, maximum: 72 },
|
||||
},
|
||||
required: ['lat', 'lon'],
|
||||
required: ['location'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
requires: ['satellites'],
|
||||
async run(args, { services, signal }) {
|
||||
const point = await resolvePoint(args.location, { services, signal });
|
||||
const group = args.group ?? 'stations';
|
||||
const entries = await readCatalog(services, group, signal);
|
||||
const satellite = matchSatellite(entries, args.satellite);
|
||||
@@ -186,8 +174,8 @@ export const nextSatellitePass = defineTool({
|
||||
const hours = args.hours ?? 24;
|
||||
const pass = findNextSatellitePass({
|
||||
satrec: satellite.satrec,
|
||||
latDeg: args.lat,
|
||||
lonDeg: args.lon,
|
||||
latDeg: point.lat,
|
||||
lonDeg: point.lon,
|
||||
fromMs: nowMs(services),
|
||||
minElevDeg: args.min_elevation_deg ?? 10,
|
||||
horizonHours: hours,
|
||||
@@ -196,15 +184,21 @@ export const nextSatellitePass = defineTool({
|
||||
const name = satellite.name;
|
||||
if (!pass)
|
||||
return {
|
||||
summary: `${name} has no ${args.visible_only ? 'visible ' : ''}pass over this location in the next ${hours} hours.`,
|
||||
data: { satellite: name, norad: satellite.norad, pass: null },
|
||||
summary: `${name} has no ${args.visible_only ? 'visible ' : ''}pass over ${point.label} in the next ${hours} hours.`,
|
||||
data: {
|
||||
location: point,
|
||||
satellite: name,
|
||||
norad: satellite.norad,
|
||||
pass: null,
|
||||
},
|
||||
};
|
||||
return {
|
||||
summary:
|
||||
`${name} next rises ${isoTime(pass.riseMs)} in the ${compass(pass.riseAzDeg)}, ` +
|
||||
`${name} next rises over ${point.label} at ${isoTime(pass.riseMs)} in the ${compass(pass.riseAzDeg)}, ` +
|
||||
`peaking at ${Math.round(pass.maxElevDeg)}°` +
|
||||
`${pass.visible ? '; visible to the naked eye' : ''}.`,
|
||||
data: {
|
||||
location: point,
|
||||
satellite: name,
|
||||
norad: satellite.norad,
|
||||
pass: {
|
||||
@@ -230,22 +224,23 @@ export const satellitesOverhead = defineTool({
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...OBSERVER,
|
||||
location: POINT_SCHEMA,
|
||||
group: GROUP_SCHEMA,
|
||||
min_elevation_deg: { type: 'number', minimum: 0, maximum: 90 },
|
||||
limit: LIMIT_SCHEMA,
|
||||
},
|
||||
required: ['lat', 'lon'],
|
||||
required: ['location'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
requires: ['satellites'],
|
||||
async run(args, { services, signal }) {
|
||||
const point = await resolvePoint(args.location, { services, signal });
|
||||
const group = args.group ?? 'stations';
|
||||
const minimum = args.min_elevation_deg ?? 10;
|
||||
const at = nowMs(services);
|
||||
const rows = (await readCatalog(services, group, signal))
|
||||
.flatMap((entry) => {
|
||||
const look = lookAnglesAt(entry.satrec, at, args.lat, args.lon);
|
||||
const look = lookAnglesAt(entry.satrec, at, point.lat, point.lon);
|
||||
return look && look.elevDeg >= minimum
|
||||
? [
|
||||
{
|
||||
@@ -260,8 +255,13 @@ export const satellitesOverhead = defineTool({
|
||||
})
|
||||
.sort((a, b) => b.elevation_deg - a.elevation_deg);
|
||||
return {
|
||||
summary: `${countNoun(rows.length, 'satellite')} from the ${group} group ${rows.length === 1 ? 'is' : 'are'} at least ${minimum}° above this location at ${isoTime(at)}.`,
|
||||
data: { ...capRows(rows, args.limit), group, at: isoTime(at) },
|
||||
summary: `${countNoun(rows.length, 'satellite')} from the ${group} group ${rows.length === 1 ? 'is' : 'are'} at least ${minimum}° above ${point.label} at ${isoTime(at)}.`,
|
||||
data: {
|
||||
...capRows(rows, args.limit),
|
||||
location: point,
|
||||
group,
|
||||
at: isoTime(at),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -36,8 +36,7 @@ test('the next pass defaults to the ISS and reports times, peak and direction',
|
||||
services: { satellites: satellites(), clock: CLOCK },
|
||||
});
|
||||
const result = await catalog.call('next_satellite_pass', {
|
||||
lat: 40.7,
|
||||
lon: -74,
|
||||
location: { lat: 40.7, lon: -74 },
|
||||
});
|
||||
const { pass } = result.data;
|
||||
assert.equal(result.data.satellite, 'ISS (ZARYA)');
|
||||
@@ -50,7 +49,7 @@ test('the next pass defaults to the ISS and reports times, peak and direction',
|
||||
assert.match(
|
||||
result.summary,
|
||||
new RegExp(
|
||||
`^ISS \\(ZARYA\\) next rises ${pass.rise} in the ${pass.rise_direction}, peaking at \\d+°`,
|
||||
`^ISS \\(ZARYA\\) next rises over 40.7000, -74.0000 at ${pass.rise} in the ${pass.rise_direction}, peaking at \\d+°`,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -62,14 +61,12 @@ test('satellites are matched by name or catalog number within the group', async
|
||||
services: { satellites: source, clock: CLOCK },
|
||||
});
|
||||
const byName = await catalog.call('next_satellite_pass', {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
location: { lat: 0, lon: 0 },
|
||||
satellite: 'tianhe',
|
||||
});
|
||||
assert.equal(byName.data.norad, 48274);
|
||||
const byNumber = await catalog.call('next_satellite_pass', {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
location: { lat: 0, lon: 0 },
|
||||
satellite: '48274',
|
||||
group: 'visual',
|
||||
});
|
||||
@@ -77,14 +74,16 @@ test('satellites are matched by name or catalog number within the group', async
|
||||
assert.deepEqual(source.calls, ['stations', 'visual']);
|
||||
await assert.rejects(
|
||||
catalog.call('next_satellite_pass', {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
location: { lat: 0, lon: 0 },
|
||||
satellite: 'Hubble',
|
||||
}),
|
||||
/No satellite matching "Hubble" is in the stations group/,
|
||||
);
|
||||
await assert.rejects(
|
||||
catalog.call('next_satellite_pass', { lat: 0, lon: 0, group: 'weather' }),
|
||||
catalog.call('next_satellite_pass', {
|
||||
location: { lat: 0, lon: 0 },
|
||||
group: 'weather',
|
||||
}),
|
||||
/must be one of/,
|
||||
);
|
||||
});
|
||||
@@ -96,21 +95,20 @@ test('a pass that never comes is a normal answer, and catalog failures are unava
|
||||
});
|
||||
// The ISS orbit is inclined 51.6°, so it never rises 10° above the pole.
|
||||
const none = await catalog.call('next_satellite_pass', {
|
||||
lat: 89.9,
|
||||
lon: 0,
|
||||
location: { lat: 89.9, lon: 0 },
|
||||
hours: 6,
|
||||
});
|
||||
assert.equal(none.data.pass, null);
|
||||
assert.equal(
|
||||
none.summary,
|
||||
'ISS (ZARYA) has no pass over this location in the next 6 hours.',
|
||||
'ISS (ZARYA) has no pass over 89.9000, 0.0000 in the next 6 hours.',
|
||||
);
|
||||
const down = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { satellites: satellites('', false), clock: CLOCK },
|
||||
});
|
||||
await assert.rejects(
|
||||
down.call('next_satellite_pass', { lat: 0, lon: 0 }),
|
||||
down.call('next_satellite_pass', { location: { lat: 0, lon: 0 } }),
|
||||
(error) => error.code === 'unavailable' && /HTTP 503/.test(error.message),
|
||||
);
|
||||
});
|
||||
@@ -120,7 +118,9 @@ test('satellites overhead are those above the elevation, highest first', async (
|
||||
const services = { satellites: source, clock: CLOCK };
|
||||
const catalog = composeCatalog({ tools: coreTools, services });
|
||||
const pass = (
|
||||
await catalog.call('next_satellite_pass', { lat: 40.7, lon: -74 })
|
||||
await catalog.call('next_satellite_pass', {
|
||||
location: { lat: 40.7, lon: -74 },
|
||||
})
|
||||
).data.pass;
|
||||
const atPeak = {
|
||||
satellites: source,
|
||||
@@ -130,8 +130,7 @@ test('satellites overhead are those above the elevation, highest first', async (
|
||||
tools: coreTools,
|
||||
services: atPeak,
|
||||
}).call('satellites_overhead', {
|
||||
lat: 40.7,
|
||||
lon: -74,
|
||||
location: { lat: 40.7, lon: -74 },
|
||||
});
|
||||
assert.equal(overhead.data.rows[0].name, 'ISS (ZARYA)');
|
||||
assert.ok(
|
||||
@@ -142,8 +141,7 @@ test('satellites overhead are those above the elevation, highest first', async (
|
||||
tools: coreTools,
|
||||
services: atPeak,
|
||||
}).call('satellites_overhead', {
|
||||
lat: 40.7,
|
||||
lon: -74,
|
||||
location: { lat: 40.7, lon: -74 },
|
||||
min_elevation_deg: 90,
|
||||
});
|
||||
assert.equal(strict.data.total, 0);
|
||||
|
||||
Reference in new issue
Block a user