Files
FabledSteward/tests/integration/test_alert_history_page.py
T
bvandeusenandClaude Opus 5.5 3a772716ae
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 51s
CI / unit (push) Failing after 1m14s
CI / publish (push) Skipped
feat(ui): audit, alert rules and the rule form say things in words (#5212)
#5212 "Plain labels: no raw ids, scopes, metric keys or unexplained jargon",
batch 1:

- Audit log: each action reads as a sentence ("paused monitor"), with the
  code as its tooltip. Detail shows flags as yes/no, lists joined, a run's
  scope by name, spans as spans, and no ids the entity column already gives.
  The filter is now a picker of areas.
- Alert rules, history and Status say the condition in words: "is down",
  "Fullest disk >= 85%". Readings use metric_value, and history times use
  |ago.
- Rule form: sources and metrics by name, the threshold's unit, "fires
  when it is down/up" for an up/down metric instead of 0.5, and a metric
  reference built from the catalog.
- Schedules and Maintenance: viewers no longer see Run now, Pause, Edit,
  Delete or New. Intervals and next runs read as "every 2h" and "in 3h";
  a scope reads "Every alert on Host agent".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:23:02 -04:00

94 lines
4.7 KiB
Python

"""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
assert "CPU 98%" in page and "fires when CPU &gt; 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
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 "<html" not in rows and "<table" in rows
assert f"old-{tag}" in rows and f"cold-note-{tag}" not in rows
# 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 "<table" not in older
assert "CPU 98%" in older and f"old-{tag}" in older and f"patch-{tag}" not in older
assert "End of the list." in older and "data-scroll-more" not in older
# The rules list links each rule to its history.
assert f'/alerts/history?rule={hot_id}' in await (await client.get("/alerts/")).get_data(as_text=True)
asyncio.run(_go())