Files
gods-eye-view/src/tools/queries/environment.test.mjs
T
Sameh Khamis 0a74e36ce8 feat: add weather, hazard, terrain and situation tools
Add get_weather, get_regional_brief, get_cyclones, get_fire_perimeters,
get_terrain_height, find_military_installations and get_map_features
over the application request services and the layers' sources.

situation_brief combines weather, earthquakes, fires, aircraft and
cyclones for an area by reusing those tools, marking sections that are
unavailable instead of failing. get_hud_caption returns the app's
heads-up display caption for the same overview.
2026-10-01 12:28:34 -07:00

456 lines
12 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { composeCatalog, coreTools, ToolError } from '../index.js';
const conditions = {
observedAt: '2026-10-01T19:15:00.000Z',
temperatureC: 26.3,
apparentTemperatureC: 32,
precipitationMm: 0,
cloudCoverPct: 100,
windKph: 5.4,
windDirectionDeg: 318,
visibilityM: 15400,
weatherCode: 3,
};
const weather = {
calls: [],
async getConditions(latitude, longitude) {
this.calls.push([latitude, longitude]);
return { status: 'ready', weather: conditions };
},
};
const places = {
resolve: async (name) =>
name === 'Austin'
? {
name: 'Austin, Texas',
bounds: { west: -97.94, south: 30.1, east: -97.56, north: 30.52 },
}
: null,
};
const catalogWith = (services) =>
composeCatalog({ tools: coreTools, services });
test('weather describes current conditions at a point or place', async () => {
const catalog = catalogWith({ weather, places });
const result = await catalog.call('get_weather', {
location: { place: 'Austin' },
});
assert.equal(
result.summary,
'Weather at Austin, Texas: overcast, 26°C, wind 5 km/h.',
);
assert.deepEqual(result.data.weather, {
observed_at: '2026-10-01T19:15:00.000Z',
conditions: 'overcast',
weather_code: 3,
temperature_c: 26.3,
feels_like_c: 32,
precipitation_mm: 0,
cloud_cover_pct: 100,
wind_kph: 5.4,
wind_direction_deg: 318,
visibility_m: 15400,
});
const [latitude, longitude] = weather.calls.at(-1);
assert.ok(Math.abs(latitude - 30.31) < 1e-9 && longitude === -97.75);
});
test('the regional brief returns place, weather and at most ten headlines', async () => {
const regional = {
async getBrief() {
return {
place: {
label: 'Edwards Plateau',
locality: null,
region: 'Edwards Plateau',
country: null,
kind: 'natural',
},
weather: conditions,
newsSource: 'Google News RSS',
articles: Array.from({ length: 12 }, (_, index) => ({
title: `Story ${index}`,
url: `https://news.example/${index}`,
domain: 'news.example',
publishedAt: '2026-10-01T00:00:00Z',
})),
};
},
};
const result = await catalogWith({ regional }).call('get_regional_brief', {
location: { lat: 30.27, lon: -97.74 },
});
assert.equal(
result.summary,
'Edwards Plateau: overcast, 26°C, wind 5 km/h; 10 recent headlines.',
);
assert.equal(result.data.articles.length, 10);
assert.deepEqual(result.data.articles[0], {
title: 'Story 0',
url: 'https://news.example/0',
source: 'news.example',
published_at: '2026-10-01T00:00:00Z',
});
});
const storm = (id, name, latitude, longitude, windKt) => ({
id,
name,
classification: 'Hurricane',
basin: 'Atlantic',
position: { latitude, longitude },
positionAt: '2026-10-01T18:00:00.000Z',
advisoryNumber: '12',
issuedAt: '2026-10-01T15:00:00.000Z',
windKt,
pressureHpa: 980,
movement: { towardDeg: 300, speedKt: 10 },
advisoryUrl: 'https://www.nhc.noaa.gov/text/X.shtml',
});
test('cyclones are listed strongest first and can be limited to an area', async () => {
const cyclones = {
async getSnapshot() {
return {
source: 'NOAA NHC / CPHC',
coverage: 'Atlantic and eastern/central North Pacific',
stale: false,
unavailable: false,
storms: [
storm('a', 'Alpha', 25, -70, 65),
storm('b', 'Beta', 15, -110, 120),
],
};
},
};
const all = await catalogWith({ cyclones }).call('get_cyclones', {});
assert.equal(
all.summary,
'2 active tropical cyclones; strongest Beta at 120 kt.',
);
assert.deepEqual(
all.data.storms.map((row) => row.id),
['b', 'a'],
);
const atlantic = await catalogWith({ cyclones }).call('get_cyclones', {
area: { bbox: [-100, 0, -10, 50] },
});
assert.equal(
atlantic.summary,
'1 active tropical cyclone in the requested box; strongest Alpha at 65 kt.',
);
const down = { getSnapshot: async () => ({ unavailable: true, storms: [] }) };
await assert.rejects(
catalogWith({ cyclones: down }).call('get_cyclones', {}),
(error) => error.code === 'unavailable',
);
});
test('fire perimeters are placed by their outline and sorted by size', async () => {
const ring = (lon, lat) => [
[
[lon, lat],
[lon + 0.1, lat],
[lon + 0.1, lat + 0.1],
[lon, lat + 0.1],
],
];
const perimeters = {
async getSnapshot() {
return [
{
stableId: 'small',
name: 'Small Fire',
acres: 120.4,
containedPct: 80,
state: 'US-CA',
county: 'Kern',
category: 'WF',
cause: 'Human',
behavior: null,
personnel: 20,
discoveredTime: Date.UTC(2026, 8, 1),
updatedTime: null,
polygons: [ring(-118.5, 35.2)],
},
{
stableId: 'big',
name: 'Big Fire',
acres: 5000,
containedPct: 10,
polygons: [ring(-118.8, 35.4)],
},
{
stableId: 'far',
name: 'Far Fire',
acres: 9000,
polygons: [ring(-80, 30)],
},
{ stableId: 'empty', name: 'No Outline', acres: 1, polygons: [] },
];
},
};
const result = await catalogWith({ perimeters }).call('get_fire_perimeters', {
area: { bbox: [-120, 34, -117, 36] },
});
assert.equal(
result.summary,
'2 mapped wildfire perimeters in the requested box.',
);
assert.deepEqual(
result.data.rows.map((row) => row.id),
['big', 'small'],
);
assert.deepEqual(result.data.rows[1], {
id: 'small',
name: 'Small Fire',
acres: 120,
contained_pct: 80,
state: 'US-CA',
county: 'Kern',
category: 'WF',
cause: 'Human',
behavior: null,
personnel: 20,
discovered: '2026-09-01T00:00:00.000Z',
updated: null,
lat: 35.25,
lon: -118.45,
});
});
test('terrain heights are returned in request order', async () => {
const terrain = {
async getHeights(points) {
return points.map((point) => ({
...point,
elevation: point.lat * 100,
geoid: -30.04,
ellipsoid: point.lat * 100 - 30.04,
}));
},
};
const one = await catalogWith({ terrain }).call('get_terrain_height', {
points: [{ lat: 27.9881, lon: 86.925 }],
});
assert.equal(one.summary, 'Ground elevation is 2798.8 m at 27.9881, 86.925.');
const many = await catalogWith({ terrain }).call('get_terrain_height', {
points: [
{ lat: 1, lon: 1 },
{ lat: 2, lon: 2 },
],
});
assert.deepEqual(
many.data.points.map((point) => point.elevation_m),
[100, 200],
);
const short = { getHeights: async () => [] };
await assert.rejects(
catalogWith({ terrain: short }).call('get_terrain_height', {
points: [{ lat: 1, lon: 1 }],
}),
(error) => error.code === 'malformed',
);
await assert.rejects(
catalogWith({ terrain }).call('get_terrain_height', {
points: Array(21).fill({ lat: 0, lon: 0 }),
}),
/at most 20 items/,
);
});
test('military installations need a bounded area and are nearest first', async () => {
const requested = [];
const installations = {
async getMappedSites(box) {
requested.push(box);
return {
source: 'OpenStreetMap',
records: [
{
id: 'osm:1',
kind: 'installation',
class: 'base',
name: 'Naval Base Point Loma',
latitude: 32.6941,
longitude: -117.2494,
},
{
id: 'osm:2',
kind: 'installation',
class: 'airfield',
name: 'North Island',
latitude: 32.7,
longitude: -117.2,
},
],
};
},
};
const catalog = catalogWith({ installations });
const result = await catalog.call('find_military_installations', {
area: { lat: 32.7, lon: -117.2, radius_km: 20 },
});
assert.equal(
result.summary,
'2 mapped military installations in 20 km around 32.700, -117.200.',
);
assert.deepEqual(
result.data.rows.map((row) => row.id),
['osm:2', 'osm:1'],
);
assert.equal(result.data.source, 'OpenStreetMap');
assert.ok(requested[0].north - requested[0].south < 1);
await assert.rejects(
catalog.call('find_military_installations', {
area: { bbox: [-130, 20, -100, 50] },
}),
/at most 10° on each side/,
);
});
test('map features name what is at a location and report an unconfigured server', async () => {
const features = {
async getAdministrativeAreas() {
return [
{
id: 1,
category: 'administrative',
names: { primary: 'Texas' },
level: 4,
point: null,
},
{
id: 2,
category: 'administrative',
names: { primary: 'Travis County', english: 'Travis County' },
level: 6,
point: null,
},
{ id: 3, category: 'administrative', names: {}, level: 8, point: null },
];
},
getEnclosingAreas: async () => null,
getMonuments: async () => ({
unavailable: true,
code: 'OVERPASS_NOT_CONFIGURED',
}),
};
const catalog = catalogWith({ features });
const result = await catalog.call('get_map_features', {
location: { lat: 30.27, lon: -97.74 },
kind: 'administrative_areas',
});
assert.equal(
result.summary,
'2 named features (administrative areas) at 30.2700, -97.7400.',
);
assert.deepEqual(
result.data.rows.map((row) => [row.name, row.admin_level]),
[
['Texas', 4],
['Travis County', 6],
],
);
await assert.rejects(
catalog.call('get_map_features', {
location: { lat: 30.27, lon: -97.74 },
kind: 'monuments',
}),
(error) => error.code === 'unavailable' && /Overpass/.test(error.message),
);
await assert.rejects(
catalog.call('get_map_features', {
location: { lat: 30.27, lon: -97.74 },
kind: 'named_places',
}),
(error) => error.code === 'retry_later',
);
});
test('the situation brief combines available sections and marks failures', async () => {
const services = {
weather,
earthquakes: {
getSnapshot: async () => [
{
stableId: 'q',
usgsId: 'us1',
lat: 30.3,
lon: -97.7,
mag: 3.1,
place: 'near Austin',
time: 0,
depthKm: 5,
},
],
},
fires: { getSnapshot: async () => ({ keyRequired: true }) },
cyclones: {
getSnapshot: async () => {
throw new Error('upstream down');
},
},
places,
};
const result = await catalogWith(services).call('situation_brief', {
area: { place: 'Austin' },
});
assert.equal(
result.summary,
'Situation in Austin, Texas: Weather at 30.3100, -97.7500: overcast, 26°C, wind 5 km/h. ' +
'1 earthquake of M2.5+ in the last 24 hours in the requested box; strongest M3.1 near Austin.',
);
assert.deepEqual(Object.keys(result.data.sections), [
'weather',
'earthquakes',
'fires',
'cyclones',
]);
assert.deepEqual(result.data.sections.fires, {
unavailable: true,
reason: 'Fire detections need a NASA FIRMS key configured for this server',
});
assert.deepEqual(result.data.sections.cyclones, {
unavailable: true,
reason: 'unavailable right now',
});
assert.equal(result.data.sections.aircraft, undefined);
});
test('the HUD caption summarizes the brief through the summary service', async () => {
const sent = [];
const summary = {
async summarize(context) {
sent.push(context);
return {
ok: true,
status: 200,
data: { summary: 'Overcast skies over Austin', error: null },
};
},
};
const result = await catalogWith({ weather, summary, places }).call(
'get_hud_caption',
{ area: { place: 'Austin' } },
);
assert.equal(result.summary, 'Overcast skies over Austin');
assert.equal(sent[0].location, 'Austin, Texas');
assert.match(sent[0].observations[0], /^Weather at /);
const failing = {
summarize: async () => ({
ok: false,
status: 502,
data: { summary: null },
}),
};
await assert.rejects(
catalogWith({ weather, summary: failing, places }).call('get_hud_caption', {
area: { place: 'Austin' },
}),
(error) => error instanceof ToolError && error.code === 'unavailable',
);
});