Files
FabledSteward/tests/core/test_alerts_widget.py
T
bvandeusenandClaude Opus 5.5 715ddfd72f
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m10s
CI / unit (push) Successful in 1m27s
CI / publish (push) Successful in 1m26s
test(alerts): the widget test reads the time as |ago writes it (#5510)
Under an hour |ago says "12m 3s ago", so the test looks for the minutes
alone; the reading cell drops an empty class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:31:57 -04:00

105 lines
5.2 KiB
Python

"""The dashboard Alerts widget (#5510): a verdict, what is firing or silent
with its reading and since when, then what someone has acknowledged, a line
when alert email is failing, and the quiet rules counted."""
import re
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from quart import render_template, session
from steward.alerts.service import widget_view
from steward.core.widgets import WIDGET_REGISTRY
from steward.models.alerts import AlertOperator
from tests.test_style_tokens import hex_colours
NOW = datetime.now(timezone.utc)
QUIET_EMAIL = {"failed": 0, "retrying": 0, "said": ""}
def _rule(rid, name, resource="web-1", metric="cpu_pct", op=">", threshold=90.0,
parent_id=None, enabled=True):
return SimpleNamespace(id=rid, name=name, resource_name=resource, metric_name=metric,
operator=AlertOperator(op), threshold=threshold,
parent_id=parent_id, enabled=enabled)
def _row(rule, label, value=None, since=None, acked_by=None, acked_at=None):
return {"rule": rule, "label": label, "value": value, "since": since, "acked_by": acked_by,
"state": SimpleNamespace(acknowledged_at=acked_at)}
async def _render(app, view, email=QUIET_EMAIL, role="admin"):
async with app.test_request_context("/alerts/widget"):
if role:
session["user_id"] = "u1"
session["user_role"] = role
return await render_template("alerts/widget.html", **view, email=email)
def test_the_widget_is_registered_and_points_at_alerts():
w = WIDGET_REGISTRY["alerts"]
assert w["hx_url"] == "/alerts/widget" and w["detail_url"] == "/alerts/" and w["plugin"] is None
def test_a_covering_rule_counts_once_and_is_quiet_only_when_all_its_hosts_are():
cover = _rule("c", "disk on every host", resource="*")
kid_a = _rule("c-a", "disk on every host", resource="nas", parent_id="c")
kid_b = _rule("c-b", "disk on every host", resource="web-1", parent_id="c")
lone = _rule("l", "web up", metric="is_up", op="<", threshold=0.5)
off = _rule("o", "old rule", enabled=False)
view = widget_view([_row(kid_a, "firing", 91.0, NOW)], [cover, kid_a, kid_b, lone, off])
assert view["total"] == 2 and view["quiet"] == 1 # "web up"; the covering rule is busy
assert view["rows"][0]["condition"] == "CPU > 90%"
assert widget_view([], [cover, kid_a, kid_b, lone, off])["quiet"] == 2
async def test_firing_and_silent_come_first_then_who_acknowledged(app):
hot = _rule("r1", "web-1 CPU high")
gone = _rule("r2", "nas agent", resource="nas")
taken = _rule("r3", "db disk", resource="db-1:/var", metric="disk_used_pct", op=">=", threshold=85.0)
rows = [_row(hot, "firing", 94.0, NOW - timedelta(minutes=12)),
_row(gone, "no_data", None, NOW - timedelta(hours=2)),
_row(taken, "acknowledged", 88.0, NOW - timedelta(hours=3), acked_by="bryan",
acked_at=NOW - timedelta(minutes=40))]
page = await _render(app, widget_view(rows, [hot, gone, taken, _rule("r4", "quiet one")]))
assert re.search(r'w-verdict tone-crit">\s*1 firing, 1 silent, 1 acknowledged\.', page)
assert page.index("web-1 CPU high") < page.index("nas agent") < page.index("db disk")
assert 'href="/alerts/history?rule=r1"' in page
assert "web-1 · fires on CPU &gt; 90%" in page and ">94%<" in page and ">12m " in page
assert "nas · no reading" in page
assert "db-1:/var · acknowledged by bryan <time" in page and ">40m " in page
assert "+1 rule quiet" in page and 'href="/alerts/"' in page
assert len(re.findall(r"<th[ >]", page)) == 3
assert not hex_colours(page), hex_colours(page)
async def test_all_quiet_is_the_verdict_alone(app):
rules = [_rule("r1", "a"), _rule("r2", "b")]
page = await _render(app, widget_view([], rules))
assert re.search(r'w-verdict tone-ok">\s*Nothing firing: all 2 rules are quiet\.', page)
assert "<table" not in page and "rule quiet" not in page and "rules quiet" not in page
page = await _render(app, widget_view([], rules[:1]))
assert "Nothing firing: the rule is quiet." in page
async def test_no_rules_offers_to_add_one(app):
page = await _render(app, widget_view([], []), role="operator")
assert "No alert rules yet." in page and 'href="/alerts/rules/new"' in page
page = await _render(app, widget_view([], []), role="viewer")
assert "No alert rules yet." in page and "/alerts/rules/new" not in page
async def test_failing_alert_email_gets_a_line(app):
email = {"failed": 2, "retrying": 1, "said": "2 alert emails failed in the last 7 days; 1 still retrying"}
page = await _render(app, widget_view([], [_rule("r1", "a")]), email=email)
assert "2 alert emails failed in the last 7 days; 1 still retrying" in page
assert 'href="/settings/system/"' in page and "tone-crit" in page
page = await _render(app, widget_view([], [_rule("r1", "a")]), email=email, role="viewer")
assert "1 still retrying" in page and "/settings/system/" not in page
async def test_the_share_view_shows_the_alerts_without_links(app):
hot = _rule("r1", "web-1 CPU high")
page = await _render(app, widget_view([_row(hot, "firing", 94.0, NOW)], [hot]), role=None)
assert "web-1 CPU high" in page and "<a " not in page