"""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 "