""" Tests for incident_followups_summary action and followup event schema. """ import os import sys import json import tempfile from datetime import datetime, timedelta from pathlib import Path from unittest.mock import patch ROOT = Path(__file__).resolve().parent.parent ROUTER = ROOT / "services" / "router" if str(ROUTER) not in sys.path: sys.path.insert(0, str(ROUTER)) class TestFollowupSummary: """Tests for oncall_tool incident_followups_summary using MemoryIncidentStore.""" def setup_method(self): from incident_store import MemoryIncidentStore, set_incident_store self.store = MemoryIncidentStore() set_incident_store(self.store) def teardown_method(self): from incident_store import set_incident_store set_incident_store(None) def _create_incident(self, service="gateway", severity="P1", status="open"): return self.store.create_incident({ "service": service, "severity": severity, "title": f"Test {severity} incident", "started_at": datetime.utcnow().isoformat(), }) def _add_followup(self, incident_id, title="Fix config", priority="P1", due_date=None, status="open"): if due_date is None: due_date = (datetime.utcnow() - timedelta(days=1)).isoformat() self.store.append_event( incident_id, "followup", title, meta={ "title": title, "owner": "sofiia", "priority": priority, "due_date": due_date, "status": status, }, ) def test_open_p1_incident_appears_in_summary(self): inc = self._create_incident(severity="P1", status="open") summary = self._get_summary(service="gateway") assert summary["stats"]["open_incidents"] >= 1 assert any(i["id"] == inc["id"] for i in summary["open_incidents"]) def test_p3_incident_not_in_critical(self): self._create_incident(severity="P3", status="open") summary = self._get_summary(service="gateway") assert summary["stats"]["open_incidents"] == 0 def test_closed_incident_not_in_open(self): inc = self._create_incident(severity="P1", status="open") self.store.close_incident(inc["id"], datetime.utcnow().isoformat(), "Fixed") summary = self._get_summary(service="gateway") assert not any(i["id"] == inc["id"] for i in summary["open_incidents"]) def test_overdue_followup_detected(self): inc = self._create_incident() yesterday = (datetime.utcnow() - timedelta(days=1)).isoformat() self._add_followup(inc["id"], title="Upgrade deps", due_date=yesterday) summary = self._get_summary(service="gateway") assert summary["stats"]["overdue"] >= 1 assert any(f["title"] == "Upgrade deps" for f in summary["overdue_followups"]) def test_future_followup_not_overdue(self): inc = self._create_incident() future = (datetime.utcnow() + timedelta(days=7)).isoformat() self._add_followup(inc["id"], title="Future task", due_date=future) summary = self._get_summary(service="gateway") assert summary["stats"]["overdue"] == 0 def test_done_followup_not_overdue(self): inc = self._create_incident() yesterday = (datetime.utcnow() - timedelta(days=1)).isoformat() self._add_followup(inc["id"], title="Done task", due_date=yesterday, status="done") summary = self._get_summary(service="gateway") assert summary["stats"]["overdue"] == 0 def test_total_open_followups_counted(self): inc = self._create_incident() future = (datetime.utcnow() + timedelta(days=7)).isoformat() self._add_followup(inc["id"], title="Task A", due_date=future) self._add_followup(inc["id"], title="Task B", due_date=future) self._add_followup(inc["id"], title="Task C done", due_date=future, status="done") summary = self._get_summary(service="gateway") assert summary["stats"]["total_open_followups"] >= 2 def test_filter_by_env(self): self._create_incident(service="gateway", severity="P1") summary_any = self._get_summary(service="gateway", env="any") assert summary_any["stats"]["open_incidents"] >= 1 def _get_summary(self, service="gateway", env="any", window_days=30): """Helper: call the followups_summary logic directly via the store.""" from datetime import datetime as _dt, timedelta as _td incidents = self.store.list_incidents( {"service": service} if service else {}, limit=100, ) now_dt = _dt.utcnow() if window_days > 0: cutoff = now_dt - _td(days=window_days) incidents = [i for i in incidents if i.get("created_at", "") >= cutoff.isoformat()] open_critical = [ {"id": i["id"], "severity": i.get("severity"), "status": i.get("status"), "started_at": i.get("started_at"), "title": i.get("title", "")[:200]} for i in incidents if i.get("status") in ("open", "mitigating", "resolved") and i.get("severity") in ("P0", "P1") ] overdue = [] for inc in incidents: events = self.store.get_events(inc["id"], limit=200) for ev in events: if ev.get("type") != "followup": continue meta = ev.get("meta") or {} if isinstance(meta, str): try: meta = json.loads(meta) except Exception: meta = {} if meta.get("status", "open") != "open": continue due = meta.get("due_date", "") if due and due < now_dt.isoformat(): overdue.append({ "incident_id": inc["id"], "title": meta.get("title", ev.get("message", "")[:200]), "due_date": due, "priority": meta.get("priority", "P2"), "owner": meta.get("owner", ""), }) total_open = sum( 1 for inc in incidents for ev in self.store.get_events(inc["id"], limit=200) if ev.get("type") == "followup" and (ev.get("meta") or {}).get("status", "open") == "open" ) return { "open_incidents": open_critical[:20], "overdue_followups": overdue[:30], "stats": { "open_incidents": len(open_critical), "overdue": len(overdue), "total_open_followups": total_open, }, }