114 lines
3.3 KiB
Python
114 lines
3.3 KiB
Python
"""ACTIVITY-WP-0014 T03: missed-fire detection verdict tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import httpx
|
|
|
|
from activity_core.schedule_health import evaluate_schedule_health, post_missed_fire_alert
|
|
|
|
NOW = datetime(2026, 6, 23, 12, 0, tzinfo=timezone.utc)
|
|
|
|
|
|
def test_healthy_when_recent_fire_and_no_drops() -> None:
|
|
health = evaluate_schedule_health(
|
|
activity_id="a1",
|
|
missed_catchup_window=0,
|
|
last_fired_at=NOW - timedelta(minutes=5),
|
|
now=NOW,
|
|
expected_interval=timedelta(hours=1),
|
|
)
|
|
assert health.healthy is True
|
|
assert health.missed is False
|
|
assert health.reasons == []
|
|
|
|
|
|
def test_unhealthy_when_catchup_window_dropped_fires() -> None:
|
|
health = evaluate_schedule_health(
|
|
activity_id="a1",
|
|
missed_catchup_window=2,
|
|
last_fired_at=NOW - timedelta(minutes=5),
|
|
now=NOW,
|
|
)
|
|
assert health.missed is True
|
|
assert "2 fire(s) dropped" in health.reasons[0]
|
|
|
|
|
|
def test_unhealthy_when_last_fire_too_stale() -> None:
|
|
health = evaluate_schedule_health(
|
|
activity_id="daily",
|
|
missed_catchup_window=0,
|
|
last_fired_at=NOW - timedelta(days=2),
|
|
now=NOW,
|
|
expected_interval=timedelta(days=1),
|
|
)
|
|
assert health.missed is True
|
|
assert any("exceeding the expected" in r for r in health.reasons)
|
|
assert health.staleness == timedelta(days=2)
|
|
|
|
|
|
def test_within_tolerance_is_healthy() -> None:
|
|
health = evaluate_schedule_health(
|
|
activity_id="daily",
|
|
missed_catchup_window=0,
|
|
last_fired_at=NOW - (timedelta(days=1) + timedelta(minutes=5)),
|
|
now=NOW,
|
|
expected_interval=timedelta(days=1),
|
|
tolerance=timedelta(minutes=10),
|
|
)
|
|
assert health.healthy is True
|
|
|
|
|
|
def test_no_fire_recorded_for_due_schedule_is_unhealthy() -> None:
|
|
health = evaluate_schedule_health(
|
|
activity_id="daily",
|
|
missed_catchup_window=0,
|
|
last_fired_at=None,
|
|
now=NOW,
|
|
expected_interval=timedelta(days=1),
|
|
)
|
|
assert health.missed is True
|
|
assert "no recorded fire" in health.reasons[0]
|
|
|
|
|
|
def test_post_missed_fire_alert_dual_writes_workplan_scope(monkeypatch) -> None:
|
|
posts: list[dict] = []
|
|
|
|
class _Resp:
|
|
status_code = 201
|
|
|
|
def raise_for_status(self) -> None: ...
|
|
def json(self) -> dict[str, str]:
|
|
return {"id": "progress-1"}
|
|
|
|
monkeypatch.setattr(httpx, "post", lambda url, **kwargs: posts.append(kwargs) or _Resp())
|
|
|
|
health = evaluate_schedule_health(
|
|
activity_id="daily",
|
|
missed_catchup_window=1,
|
|
last_fired_at=NOW - timedelta(days=2),
|
|
now=NOW,
|
|
expected_interval=timedelta(days=1),
|
|
)
|
|
result = post_missed_fire_alert(
|
|
health,
|
|
state_hub_url="http://state-hub.test",
|
|
workplan_id="wp-123",
|
|
)
|
|
|
|
assert result["status"] == "posted"
|
|
body = posts[0]["json"]
|
|
assert body["workplan_id"] == "wp-123"
|
|
assert "workstream_id" not in body
|
|
|
|
|
|
def test_no_interval_and_no_fire_is_not_flagged() -> None:
|
|
# Without an expected interval we cannot assert a miss from absence alone.
|
|
health = evaluate_schedule_health(
|
|
activity_id="event-ish",
|
|
missed_catchup_window=0,
|
|
last_fired_at=None,
|
|
now=NOW,
|
|
)
|
|
assert health.healthy is True
|