"""Integration: /alerts/history (#5066) against real Postgres. A viewer can read it; it shows the transition, value against the condition, delivery outcome with its error, and a maintenance mute; ?rule= narrows it to one rule, an HTMX request gets only the rows, and ?before= the next page.""" from __future__ import annotations import asyncio import os import time import uuid from datetime import datetime, timedelta, timezone import pytest pytestmark = pytest.mark.integration @pytest.fixture def app(): if not os.environ.get("STEWARD_DATABASE_URL"): pytest.skip("needs Postgres") from steward.app import create_app return create_app(testing=False) def test_a_viewer_reads_the_alert_history(app): from steward.core.alerts import MUTED from steward.models.alerts import AlertEvent, AlertOperator, AlertRule from steward.models.users import User, UserRole async def _go(): tag = uuid.uuid4().hex[:8] uid = str(uuid.uuid4()) now = datetime.now(timezone.utc) async with app.db_sessionmaker() as s: async with s.begin(): s.add(User(id=uid, username=f"hist-{tag}", email=f"hist-{tag}@test", password_hash="x", role=UserRole.viewer, is_active=True)) await s.flush() hot = AlertRule(name=f"hot {tag}", source_module="host_agent", resource_name=f"box-{tag}", metric_name="cpu_pct", operator=AlertOperator(">"), threshold=90, created_by=uid) cold = AlertRule(name=f"cold {tag}", source_module="host_agent", resource_name=f"box-{tag}", metric_name="cpu_pct", operator=AlertOperator("<"), threshold=1, created_by=uid) s.add_all([hot, cold]) await s.flush() s.add_all([ AlertEvent(rule_id=hot.id, from_state="inactive", to_state="firing", metric_value=97.5, transitioned_at=now - timedelta(minutes=5), email_sent=False, email_error=f"mx-{tag}: timed out", webhook_sent=True), AlertEvent(rule_id=hot.id, from_state="firing", to_state="resolved", metric_value=None, transitioned_at=now - timedelta(minutes=2), note=f"{MUTED} patch-{tag}"), AlertEvent(rule_id=cold.id, from_state="inactive", to_state="firing", metric_value=0.5, transitioned_at=now - timedelta(minutes=1), note=f"cold-note-{tag}"), # Outside a 1h range. AlertEvent(rule_id=hot.id, from_state="pending", to_state="firing", metric_value=95, transitioned_at=now - timedelta(hours=3), note=f"old-{tag}"), ]) hot_id = hot.id client = app.test_client() async with client.session_transaction() as sess: sess.update(user_id=uid, user_role="viewer", username=f"hist-{tag}", login_at=time.time()) resp = await client.get("/alerts/history?range=1h") assert resp.status_code == 200 page = await resp.get_data(as_text=True) assert "Alert History" in page and 'href="/alerts/"' in page # crumbs back to Alerts assert f"hot {tag}" in page and f"cold-note-{tag}" in page and f"old-{tag}" not in page # One episode per stretch out of quiet (#5316): its worst reading against # the rule, who was told across its changes, the changes folded under it. assert '98% (fires above 90%)' in page assert "email failed" in page and f"mx-{tag}: timed out" in page and "webhook sent" in page assert f"muted ยท patch-{tag}" in page and "No CPU reading" in page and "2 changes of state" in page assert "Rule" in page and "Notified" in page resp = await client.get(f"/alerts/history?range=24h&rule={hot_id}", headers={"HX-Request": "true"}) rows = await resp.get_data(as_text=True) assert "Rule" not in rows # one rule: no column for it # Columns only for what this rule's rows hold: it told someone, it ran # nothing. (The all-rules page shares Postgres with other tests' runs.) assert "Notified" in rows and "Action" not in rows assert "Fired 1 time in the last 24h" in rows # its verdict # The next page as the reader scrolls: only older rows, without the table # around them, and the list says where it ends (#5211). from steward.core.paging import cursor resp = await client.get("/alerts/history", query_string={ "range": "24h", "rule": hot_id, "before": cursor(now - timedelta(minutes=3), "")}) older = await resp.get_data(as_text=True) assert resp.status_code == 200 and "