Files
FabledSteward/tests/core/test_ansible_runs_page.py
T
bvandeusenandClaude Opus 5.5 7275fad767
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 51s
CI / unit (push) Successful in 1m13s
CI / publish (push) Successful in 1m7s
feat(ui): audit log, alert history and Ansible runs load as you scroll (#5211)
#5211 "Lists load as you scroll: audit, alert history, Ansible runs"
(milestone 477), per preference 172.

- core/paging.py: keyset paging over (time, id), newest first, with an
  opaque "<iso>|<id>" cursor; a page reads one row past itself to know
  whether another follows. No OFFSET, so rows added while someone scrolls
  never shift the pages after them.
- _macros.html scroll_more / scroll_end and static/scroll-more.js, written
  once: an IntersectionObserver with a 600px margin fetches the next page
  before the reader reaches the end; "Loading more…" while in flight,
  "End of the list." at the end, and a failed page says so with a Retry
  button instead of re-firing on every scroll.
- Audit log: no "Load more" link and no 500-row cap.
- Alert history: no 200-row stop with "pick a shorter range";
  alert_svc.history takes the cursor, so the MCP tool and the page share
  one query.
- Ansible runs: no silent cap of 50.

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

93 lines
4.5 KiB
Python

"""Ansible runs are named by what ran where, say how long ago and for how
long, and wear the one status badge every page uses (#5125)."""
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
import pytest
from quart import render_template, session
from steward.ansible.inventory_gen import run_title
from steward.ansible.routes import run_duration, run_view
from steward.models.ansible import AnsibleRunStatus
from tests.test_style_tokens import hex_colours
@pytest.mark.parametrize("label, title", [
("target web-1", "update.yml on web-1"),
("group Docker Swarm", "update.yml on group Docker Swarm"),
("all inventory targets", "update.yml on all inventory targets"),
("homelab/hosts.ini", "update.yml on homelab/hosts.ini"),
("", "update.yml"),
])
def test_a_run_is_named_by_what_ran_where(label, title):
assert run_title("update.yml", label) == title
def _run(rid, status, started_min_ago, took_s=None, scope="steward:target:t1", check=False):
now = datetime.now(timezone.utc)
started = now - timedelta(minutes=started_min_ago)
return SimpleNamespace(
id=rid, playbook_path="update.yml", source_name="builtin", inventory_scope=scope,
status=status, started_at=started,
finished_at=started + timedelta(seconds=took_s) if took_s is not None else None,
params={"check": True} if check else None, results=None, output="")
def test_a_running_run_counts_its_duration_so_far():
now = datetime.now(timezone.utc)
run = _run("r", AnsibleRunStatus.running, 3)
assert run_duration(run, now) == pytest.approx(180, abs=2)
assert run_duration(_run("f", AnsibleRunStatus.success, 10, took_s=95), now) == 95
async def test_the_runs_list_names_each_run_with_one_badge(app):
now = datetime.now(timezone.utc)
labels = {"steward:target:t1": "target web-1", "steward:all": "all inventory targets"}
runs = [run_view(r, labels, now) for r in (
_run("aaaa1111", AnsibleRunStatus.running, 2),
_run("bbbb2222", AnsibleRunStatus.failed, 60, took_s=125, scope="steward:all", check=True),
_run("cccc3333", AnsibleRunStatus.success, 120, took_s=40),
)]
async with app.test_request_context("/ansible/"):
session["user_role"] = "viewer"
page = await render_template("ansible/index.html", runs=runs, after="2026-10-07T01:00:00+00:00|x")
main = page[page.index("<main"):]
assert '<a href="/ansible/runs/aaaa1111" title="steward:target:t1">update.yml on web-1</a>' in main
assert ">update.yml on all inventory targets</a>" in main
assert '<span class="badge badge-info">Running</span>' in main
assert '<span class="badge badge-crit">Failed</span>' in main
assert '<span class="badge badge-ok">Succeeded</span>' in main
# The list continues itself as the reader scrolls (#5211): no cap of 50.
assert 'data-scroll-more hx-get="/ansible/?before=2026-10-07T01%3A00%3A00%2B00%3A00%7Cx"' in main
assert "2m 5s" in main and "40s" in main and " so far" in main
assert "dry run" in main and "ago" in main
assert "RUNNING" not in main and "FAILED" not in main # the old shouted status text is gone
assert not hex_colours(main), hex_colours(main)
async def test_run_detail_titles_the_run_and_keeps_the_trail(app):
now = datetime.now(timezone.utc)
run = _run("dddd4444", AnsibleRunStatus.interrupted, 30, took_s=600)
async with app.test_request_context("/ansible/runs/dddd4444"):
session["user_role"] = "operator"
page = await render_template("ansible/run_detail.html", run=run, triggered_label="system",
row=run_view(run, {"steward:target:t1": "target web-1"}, now))
assert "<title>update.yml on web-1 — Steward</title>" in page
assert 'href="/ansible/"' in page # breadcrumb back to the runs
assert '<span class="badge badge-warn">Interrupted</span>' in page
assert "<dd>10m</dd>" in page
assert "/cancel" not in page # an interrupted run has nothing to cancel
main = page[page.index("<main"):]
assert not hex_colours(main), hex_colours(main)
async def test_run_started_is_on_the_palette(app):
run = _run("eeee5555", AnsibleRunStatus.running, 0)
async with app.test_request_context("/ansible/runs"):
page = await render_template("ansible/run_started.html", run=run, title="update.yml on web-1")
assert '<span class="badge badge-info">Running</span>' in page
assert "update.yml on web-1" in page and 'href="/ansible/runs/eeee5555"' in page
assert not hex_colours(page), hex_colours(page)