Merge pull request #1395 from rudycelekli/fix/fires-tool-acquired-time

This commit is contained in:
Said Turksever authored and GitHub committed 2026-10-25 21:32:37 +00:00
commit 2a3e0d669f
5 files changed
+75 -3

No files matched your search

@@ -0,0 +1,6 @@
- Report a fire detection's acquisition time as a UTC timestamp in
`get_active_fires`. The tool joined FIRMS's unpadded date and time, so a
00:45 UTC detection read "2026-01-01 45"; it now reads
`2026-01-01T00:45:00.000Z`. Missing or invalid dates and times remain
unknown rather than rolling into another calendar day
(Rudy Celekli, @rudycelekli, #1395).
+3
View File
@@ -1984,6 +1984,9 @@ accessible name remains "Don't show this again". Control behavior is unchanged.
## FIRMS source status ## FIRMS source status
The FIRMS CSV parser rejects impossible acquisition calendar dates before the proxy filters records to the trailing 24 hours, while retaining valid UTC acquisition times. The FIRMS CSV parser rejects impossible acquisition calendar dates before the proxy filters records to the trailing 24 hours, while retaining valid UTC acquisition times.
The `get_active_fires` tool reports acquisition times as UTC ISO timestamps,
including zero-padded hours and minutes; missing or invalid dates and times
remain `null`, including impossible calendar dates from a custom source.
The FIRMS proxy records source success after appending its rows. If aggregation The FIRMS proxy records source success after appending its rows. If aggregation
throws, that source reports failure without a contradictory success entry. throws, that source reports failure without a contradictory success entry.
+19 -2
View File
@@ -5,6 +5,24 @@ import { defineTool, ToolError } from '../catalog.js';
import { AREA_SCHEMA, areaContains, resolveArea } from '../area.js'; import { AREA_SCHEMA, areaContains, resolveArea } from '../area.js';
import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js'; import { LIMIT_SCHEMA, capRows, countNoun, isoTime } from '../results.js';
function acquiredMsUtc(acqDate, acqTime) {
if (typeof acqDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(acqDate))
return NaN;
const timeText = String(acqTime ?? '').trim();
if (!/^\d{1,4}$/.test(timeText)) return NaN;
const hhmm = timeText.padStart(4, '0');
const hours = Number(hhmm.slice(0, 2));
const minutes = Number(hhmm.slice(2, 4));
if (hours > 23 || minutes > 59) return NaN;
const timestamp = Date.parse(
`${acqDate}T${hhmm.slice(0, 2)}:${hhmm.slice(2, 4)}:00.000Z`,
);
return Number.isFinite(timestamp) &&
new Date(timestamp).toISOString().slice(0, 10) === acqDate
? timestamp
: NaN;
}
export const getEarthquakes = defineTool({ export const getEarthquakes = defineTool({
name: 'get_earthquakes', name: 'get_earthquakes',
title: 'Recent earthquakes', title: 'Recent earthquakes',
@@ -114,8 +132,7 @@ export const getActiveFires = defineTool({
lon: fire.lon, lon: fire.lon,
frp_mw: fire.frp ?? null, frp_mw: fire.frp ?? null,
confidence: fire.confidence ?? null, confidence: fire.confidence ?? null,
acquired: acquired: isoTime(acquiredMsUtc(fire.acqDate, fire.acqTime)),
[fire.acqDate, fire.acqTime].filter(Boolean).join(' ') || null,
satellite: fire.satellite ?? null, satellite: fire.satellite ?? null,
day_night: fire.daynight ?? null, day_night: fire.daynight ?? null,
})), })),
+46
View File
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { composeCatalog } from '../catalog.js';
import { getActiveFires } from './hazards.js';
const catalogWith = (fires) =>
composeCatalog({
tools: [getActiveFires],
services: { fires: { getSnapshot: async () => ({ fires }) } },
});
test('a fire acquired at 00:45 UTC reads as 00:45, not "45"', async () => {
const result = await catalogWith([
{ lat: 24, lon: 121, frp: 5, acqDate: '2026-01-01', acqTime: '45' },
]).call('get_active_fires', { area: { bbox: [120, 23, 122, 25] } });
assert.equal(result.data.rows[0].acquired, '2026-01-01T00:45:00.000Z');
});
test('fire acquisition timestamps reject impossible calendar dates', async () => {
for (const acqDate of [
'2026-02-29',
'2026-02-30',
'2026-04-31',
'1900-02-29',
'2026-13-01',
'2026-01-00',
]) {
const result = await catalogWith([
{ lat: 24, lon: 121, acqDate, acqTime: '45' },
]).call('get_active_fires', { area: { bbox: [120, 23, 122, 25] } });
assert.equal(result.data.rows[0].acquired, null, acqDate);
}
});
test('fire acquisition timestamps preserve valid leap days and UTC times', async () => {
for (const [acqDate, acqTime, expected] of [
['2024-02-29', '45', '2024-02-29T00:45:00.000Z'],
['2000-02-29', 0, '2000-02-29T00:00:00.000Z'],
['2026-04-30', '2359', '2026-04-30T23:59:00.000Z'],
]) {
const result = await catalogWith([
{ lat: 24, lon: 121, acqDate, acqTime },
]).call('get_active_fires', { area: { bbox: [120, 23, 122, 25] } });
assert.equal(result.data.rows[0].acquired, expected);
}
});
+1 -1
View File
@@ -125,7 +125,7 @@ test('fires need an area and report a missing key as unavailable', async () => {
result.data.rows.map((row) => row.frp_mw), result.data.rows.map((row) => row.frp_mw),
[40, 12], [40, 12],
); );
assert.equal(result.data.rows[1].acquired, '2026-01-01 0130'); assert.equal(result.data.rows[1].acquired, '2026-01-01T01:30:00.000Z');
const keyless = composeCatalog({ const keyless = composeCatalog({
tools: coreTools, tools: coreTools,