From 8121a118ece94ad6e84dba7d79588c612b6ee0e2 Mon Sep 17 00:00:00 2001 From: nullhack Date: Tue, 21 Jul 2026 23:44:57 -0400 Subject: [PATCH] fix(store): include summarized news in active_incidents window check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit active_incidents() excluded any incident whose news had been moved into a dated log, by short-circuiting _pending_news_max_dates() on the news→log map. Once generate-logs consumed all pending news for an incident, it dropped out of the active set, and search-news --repoll never queried DDG for it again — so no fresh news could ever arrive. This silently excluded 20 of 25 incidents with news within the 7-day tracking window (Venezuela M7.2, Typhoon BAVI-26, Mexico M6.0, Ebola DRC, China flood, Bangladesh measles, etc.). The 5 still in the active set were only there because they had a couple of straggler pending items. Fix: rename _pending_news_max_dates → _recent_news_max_dates and drop the _news_log short-circuit. The window cutoff already ages old news out, so the summarized-news exclusion was both redundant and harmful. The extended_monitoring flag remains as the override for incidents older than the window that still need polling. Tests: invert test_active_ignores_summarized_news to test_active_keeps_incident_with_recent_summarized_news, and add test_active_drops_incident_when_summarized_news_outside_window to preserve the window contract. Mirrored in the .pyi. --- disaster_report/store/content.py | 6 +++--- tests/integration/content_store_test.py | 20 +++++++++++++++++++- tests/integration/content_store_test.pyi | 8 +++++++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/disaster_report/store/content.py b/disaster_report/store/content.py index 432af61e..91d99ebe 100644 --- a/disaster_report/store/content.py +++ b/disaster_report/store/content.py @@ -513,11 +513,11 @@ def read_news(self, incident_id: str) -> list[NewsItem]: if self._news_incident.get(nuuid) == incident_id ] - def _pending_news_max_dates(self) -> dict[str, datetime]: + def _recent_news_max_dates(self) -> dict[str, datetime]: max_pub: dict[str, datetime] = {} for nuuid, n in self._news.items(): inc = self._news_incident.get(nuuid) - if inc is None or self._news_log.get(nuuid) is not None: + if inc is None: continue try: dt = _as_utc(datetime.fromisoformat(n.get("published_date", ""))) @@ -530,7 +530,7 @@ def _pending_news_max_dates(self) -> dict[str, datetime]: def active_incidents(self, window_days: int) -> list[Incident]: now = _as_utc(self._clock()) cutoff = now - timedelta(days=window_days) - max_pub = self._pending_news_max_dates() + max_pub = self._recent_news_max_dates() active_ids = [inc for inc, dt in max_pub.items() if cutoff <= dt <= now] if not active_ids: return [] diff --git a/tests/integration/content_store_test.py b/tests/integration/content_store_test.py index 772eb5be..067606fe 100644 --- a/tests/integration/content_store_test.py +++ b/tests/integration/content_store_test.py @@ -414,7 +414,9 @@ def test_active_ignores_old_pending_news(self, tmp_path: Path) -> None: store.assign_news_to_incident(nid, inc) assert store.active_incidents(WINDOW) == [] - def test_active_ignores_summarized_news(self, tmp_path: Path) -> None: + def test_active_keeps_incident_with_recent_summarized_news( + self, tmp_path: Path + ) -> None: store = ContentStore(tmp_path, clock=_clock) rid = store.ingest_source_report(_build_report()) nid = store.ingest_news_item(_build_news(published_date="2026-06-30T10:00:00Z")) @@ -424,6 +426,22 @@ def test_active_ignores_summarized_news(self, tmp_path: Path) -> None: store.append_timeline_with_provenance( _build_log(incident_id=inc), {nid} ) + active = store.active_incidents(WINDOW) + assert len(active) == 1 + assert active[0].incident_id == inc + + def test_active_drops_incident_when_summarized_news_outside_window( + self, tmp_path: Path + ) -> None: + store = ContentStore(tmp_path, clock=_clock) + rid = store.ingest_source_report(_build_report()) + nid = store.ingest_news_item(_build_news(published_date="2025-01-01T00:00:00Z")) + inc = _new_incident_id() + store.add_report_incident(rid, inc) + store.assign_news_to_incident(nid, inc) + store.append_timeline_with_provenance( + _build_log(incident_id=inc), {nid} + ) assert store.active_incidents(WINDOW) == [] diff --git a/tests/integration/content_store_test.pyi b/tests/integration/content_store_test.pyi index 21e199c6..a0343998 100644 --- a/tests/integration/content_store_test.pyi +++ b/tests/integration/content_store_test.pyi @@ -90,7 +90,13 @@ class TestReadIncidents: class TestActiveIncidents: def test_active_fires_on_pending_news_in_window(self, tmp_path) -> None: ... def test_active_ignores_old_pending_news(self, tmp_path) -> None: ... - def test_active_ignores_summarized_news(self, tmp_path) -> None: ... + def test_active_keeps_incident_with_recent_summarized_news( + self, tmp_path + ) -> None: ... + + def test_active_drops_incident_when_summarized_news_outside_window( + self, tmp_path + ) -> None: ... class TestMergeIncidents: def test_merge_moves_reports(self, tmp_path) -> None: ...