mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
When the model upstream fails, the wind proxy keeps serving its last run, however old, with manifest.stale set. The wind layer shows that, but get_wind ignored it and gave the old run's wind as current. get_wind now returns stale and adds "(forecast may be stale)" to its summary, the way get_weather and the space tools flag stale data. Co-Authored-By: Claude Opus 5.5 <[email protected]>
203 lines
6.2 KiB
JavaScript
203 lines
6.2 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { composeCatalog, coreTools } from '../index.js';
|
|
import { weatherWindow } from './atmosphere.js';
|
|
import { resolveArea } from '../area.js';
|
|
|
|
const CONUS = { west: -130, south: 20, east: -60, north: 55 };
|
|
const PNG = Uint8Array.from([0x89, 0x50, 0x4e, 0x47]);
|
|
const weatherMaps = (snapshot = {}) => ({
|
|
requests: [],
|
|
async getSnapshot({ product }) {
|
|
return {
|
|
schemaVersion: 1,
|
|
product,
|
|
title: 'MRMS radar',
|
|
coverage: 'Contiguous United States',
|
|
source: 'NOAA nowCOAST',
|
|
attribution: 'NOAA',
|
|
bounds: CONUS,
|
|
times: ['2026-10-01T19:00:00.000Z'],
|
|
latest: '2026-10-01T19:00:00.000Z',
|
|
...snapshot,
|
|
};
|
|
},
|
|
async getImage(request) {
|
|
this.requests.push(request);
|
|
return { contentType: 'image/png', bytes: PNG };
|
|
},
|
|
});
|
|
|
|
test('weather windows are 2:1, snapped to 0.25° and kept inside the bounds', () => {
|
|
const austin = weatherWindow(
|
|
{
|
|
center: { lat: 30.27, lon: -97.74, radiusKm: 50 },
|
|
west: -98.3,
|
|
east: -97.2,
|
|
south: 29.8,
|
|
north: 30.7,
|
|
},
|
|
CONUS,
|
|
);
|
|
assert.deepEqual(austin, {
|
|
west: -98.75,
|
|
south: 29.75,
|
|
east: -96.75,
|
|
north: 30.75,
|
|
});
|
|
const edge = weatherWindow(
|
|
{
|
|
center: { lat: 21, lon: -129, radiusKm: 200 },
|
|
west: -131,
|
|
east: -127,
|
|
south: 19,
|
|
north: 23,
|
|
},
|
|
CONUS,
|
|
);
|
|
assert.equal(edge.west, -130);
|
|
assert.equal(edge.south, 20);
|
|
assert.equal((edge.east - edge.west) / (edge.north - edge.south), 2);
|
|
assert.equal(
|
|
weatherWindow(
|
|
{ bbox: true, west: -180, east: 180, south: -80, north: 80 },
|
|
CONUS,
|
|
),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test('weather maps return the latest frame as an image over the area', async () => {
|
|
const maps = weatherMaps();
|
|
const catalog = composeCatalog({
|
|
tools: coreTools,
|
|
services: { weatherMaps: maps },
|
|
});
|
|
const result = await catalog.call('get_weather_map', {
|
|
map: 'radar',
|
|
area: { lat: 30.27, lon: -97.74, radius_km: 50 },
|
|
});
|
|
assert.equal(
|
|
result.summary,
|
|
'MRMS radar at 2026-10-01T19:00:00.000Z over 50 km around 30.270, -97.740.',
|
|
);
|
|
assert.deepEqual(result.images, [
|
|
{ mimeType: 'image/png', data: 'iVBORw==' },
|
|
]);
|
|
assert.deepEqual(maps.requests[0], {
|
|
product: 'radar',
|
|
time: '2026-10-01T19:00:00.000Z',
|
|
size: { width: 1024, height: 512 },
|
|
bbox: { west: -98.75, south: 29.75, east: -96.75, north: 30.75 },
|
|
signal: undefined,
|
|
});
|
|
const whole = await catalog.call('get_weather_map', { map: 'satellite' });
|
|
assert.equal(maps.requests[1].product, 'clouds-regional');
|
|
assert.equal(maps.requests[1].bbox, null);
|
|
assert.deepEqual(whole.data.box, CONUS);
|
|
await assert.rejects(
|
|
catalog.call('get_weather_map', {
|
|
map: 'radar',
|
|
area: { lat: 51.5, lon: -0.1, radius_km: 50 },
|
|
}),
|
|
/outside the radar map's coverage: Contiguous United States/,
|
|
);
|
|
const down = composeCatalog({
|
|
tools: coreTools,
|
|
services: { weatherMaps: weatherMaps({ unavailable: true }) },
|
|
});
|
|
await assert.rejects(
|
|
down.call('get_weather_map', { map: 'lightning' }),
|
|
(error) => error.code === 'unavailable',
|
|
);
|
|
});
|
|
|
|
test('wind is sampled from the model grid at the location', async () => {
|
|
// A 1° global grid where every cell blows 10 m/s toward the east (from the west).
|
|
const nx = 360;
|
|
const ny = 181;
|
|
const u = new Float32Array(nx * ny).fill(10);
|
|
const v = new Float32Array(nx * ny).fill(0);
|
|
const wind = {
|
|
requested: [],
|
|
async getSnapshot({ model }) {
|
|
this.requested.push(model);
|
|
return {
|
|
model,
|
|
cycle: '2026-10-01T12:00:00Z',
|
|
level: '10 m above ground',
|
|
grid: { nx, ny, lo1: 0, la1: 90, dx: 1, dy: 1 },
|
|
u,
|
|
v,
|
|
};
|
|
},
|
|
};
|
|
const catalog = composeCatalog({ tools: coreTools, services: { wind } });
|
|
const result = await catalog.call('get_wind', {
|
|
location: { lat: 30.27, lon: -97.74 },
|
|
model: 'ifs',
|
|
});
|
|
assert.equal(
|
|
result.summary,
|
|
'Wind at 30.2700, -97.7400: 36 km/h from the W.',
|
|
);
|
|
assert.equal(result.data.speed_mps, 10);
|
|
assert.equal(result.data.from, 'W');
|
|
assert.deepEqual(wind.requested, ['ifs']);
|
|
u.fill(0);
|
|
const calm = await catalog.call('get_wind', { location: { lat: 0, lon: 0 } });
|
|
assert.equal(calm.summary, 'Wind at 0.0000, 0.0000: 0 km/h, calm.');
|
|
assert.equal(calm.data.calm, true);
|
|
assert.equal(calm.data.stale, false);
|
|
// The proxy's last run, served while the model upstream fails.
|
|
const cached = composeCatalog({
|
|
tools: coreTools,
|
|
services: {
|
|
wind: {
|
|
getSnapshot: async (options) => ({
|
|
...(await wind.getSnapshot(options)),
|
|
stale: true,
|
|
reason: 'Wind upstream unavailable',
|
|
}),
|
|
},
|
|
},
|
|
});
|
|
const stale = await cached.call('get_wind', { location: { lat: 0, lon: 0 } });
|
|
assert.equal(
|
|
stale.summary,
|
|
'Wind at 0.0000, 0.0000: 0 km/h, calm (forecast may be stale).',
|
|
);
|
|
assert.equal(stale.data.stale, true);
|
|
u.fill(10);
|
|
const staleWest = await cached.call('get_wind', {
|
|
location: { lat: 30.27, lon: -97.74 },
|
|
});
|
|
assert.equal(
|
|
staleWest.summary,
|
|
'Wind at 30.2700, -97.7400: 36 km/h from the W (forecast may be stale).',
|
|
);
|
|
const down = composeCatalog({
|
|
tools: coreTools,
|
|
services: { wind: { getSnapshot: async () => ({ unavailable: true }) } },
|
|
});
|
|
await assert.rejects(
|
|
down.call('get_wind', { location: { lat: 0, lon: 0 } }),
|
|
(error) => error.code === 'unavailable',
|
|
);
|
|
});
|
|
|
|
test('a weather window that cannot cover the area falls back to the whole map', async () => {
|
|
const world = { west: -180, south: -90, east: 180, north: 90 };
|
|
// 100 km around 179.8° crosses the antimeridian.
|
|
const dateline = await resolveArea({ lat: 0, lon: 179.8, radius_km: 100 });
|
|
assert.equal(weatherWindow(dateline, world), null);
|
|
// Near a product edge the window moves inside and still covers the area.
|
|
const conus = { west: -130, south: 20, east: -60, north: 55 };
|
|
const edge = await resolveArea({ lat: 40, lon: -129.5, radius_km: 50 });
|
|
const box = weatherWindow(edge, conus);
|
|
assert.ok(box);
|
|
assert.ok(box.west <= Math.max(edge.west, conus.west));
|
|
assert.ok(box.east >= edge.east);
|
|
assert.ok(box.south <= edge.south && box.north >= edge.north);
|
|
});
|