From 3de745bd33123b9203e2b160d8bbbb6772d399a1 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 25 Oct 2026 14:08:25 -0700 Subject: [PATCH] fix(fires): reject rolled-over acquisition dates and document UTC times --- changelog.d/1395-fires-tool-acquired-time.md | 4 ++- docs/CURRENT-STATE.md | 3 ++ src/tools/queries/hazards.js | 6 +++- src/tools/queries/hazards.test.mjs | 29 ++++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/changelog.d/1395-fires-tool-acquired-time.md b/changelog.d/1395-fires-tool-acquired-time.md index ea3bddd1d..17d74e481 100644 --- a/changelog.d/1395-fires-tool-acquired-time.md +++ b/changelog.d/1395-fires-tool-acquired-time.md @@ -1,4 +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` (Rudy Celekli, @rudycelekli, #1395). + `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). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index b15babbb2..5332216e5 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1984,6 +1984,9 @@ accessible name remains "Don't show this again". Control behavior is unchanged. ## 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 `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 throws, that source reports failure without a contradictory success entry. diff --git a/src/tools/queries/hazards.js b/src/tools/queries/hazards.js index 194f9b8c1..2b11c639c 100644 --- a/src/tools/queries/hazards.js +++ b/src/tools/queries/hazards.js @@ -14,9 +14,13 @@ function acquiredMsUtc(acqDate, acqTime) { const hours = Number(hhmm.slice(0, 2)); const minutes = Number(hhmm.slice(2, 4)); if (hours > 23 || minutes > 59) return NaN; - return Date.parse( + 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({ diff --git a/src/tools/queries/hazards.test.mjs b/src/tools/queries/hazards.test.mjs index fe2e6ac1f..fa17158dd 100644 --- a/src/tools/queries/hazards.test.mjs +++ b/src/tools/queries/hazards.test.mjs @@ -15,3 +15,32 @@ test('a fire acquired at 00:45 UTC reads as 00:45, not "45"', async () => { ]).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); + } +});