Files
FabledSteward/tests/core/test_host_page.py
T
bvandeusenandClaude Opus 5.5 fb20415a38
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 54s
CI / unit (push) Successful in 1m18s
CI / publish (push) Successful in 1m23s
feat(hosts): a host's health tiles span the card and the gaps match; Add host is a dialog on the list (#5290, #5291)
#5290 "Host page: the health tiles span the card's width; even spacing above Ansible":
the four tiles take equal quarters (halves on a phone) and their sparklines stretch
across them; the Monitors + Agent row drops the Agent card's own margin, so the gap
above Ansible is 1rem like every other.

#5291 "Add host opens as a dialog over the Hosts list": a native <dialog> on /hosts/,
opened by Add host or /hosts/?add=1. A name clash re-renders the list with it open,
the error and what was typed. GET /hosts/new is gone; hosts/form.html is Edit only.

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

133 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The host page: what sits where (#5214)."""
import re
from types import SimpleNamespace
from quart import render_template, session
async def test_ansible_comes_before_the_docker_list(app, monkeypatch):
# The Docker list can run to dozens of containers; Ansible under it was
# easy to miss, so it sits above.
monkeypatch.setitem(app.config, "PLUGINS", {"docker": {"enabled": True}})
host = SimpleNamespace(id="h1", name="box", address="10.0.0.5")
async with app.test_request_context("/hosts/h1"):
session["user_role"] = "operator"
page = await render_template(
"hosts/detail.html", host=host, monitors=[], uptime=None, alerts=[],
linked_target=None, linkable_targets=[], ansible_sources=[])
docker = page.index('hx-get="/plugins/docker/host/h1"')
# #5290: Monitors and Agent share a row whose cards drop their own margin.
assert '<div class="host-pair">' in page
assert page.index(">Ansible</h3>") < docker
assert page.index('hx-get="/plugins/host_agent/panel/h1"') < docker
# Monitors are added on their own form, filed under this host (#5277);
# each row's own page has the labelled Edit/Pause/Delete.
assert 'href="/monitors/new?host_id=h1"' in page
assert "row-actions" not in page and 'action="/monitors/add"' not in page
async def test_adding_a_monitor_from_a_host_page_says_whose_it_is(app):
host = SimpleNamespace(id="h1", name="box", address="10.0.0.5")
async with app.test_request_context("/monitors/new?host_id=h1"):
session["user_role"] = "operator"
page = await render_template(
"monitors/form.html", monitor=None, host=host, mtype="icmp", config={}, types=[],
values={"name": "", "target": "10.0.0.5", "host_id": "h1", "check_interval_seconds": 0})
assert '<a href="/hosts/h1">box</a>' in page # crumb and hint name the host
assert 'name="host_id" value="h1"' in page and 'value="10.0.0.5"' in page
assert 'href="/hosts/h1" class="btn btn-sm btn-ghost">Cancel' in page
async def test_the_agent_card_keeps_its_actions_under_manage(app):
"""#5278: "Remove" said nothing about what it removed."""
from datetime import datetime, timezone
from tests._templates import render_plugin_template
host = SimpleNamespace(id="h1", name="box", address="10.0.0.5")
reg = SimpleNamespace(agent_version="1.8.0", last_seen_at=datetime.now(timezone.utc))
target = SimpleNamespace(id="t1", name="box")
async with app.test_request_context("/plugins/host_agent/panel/h1"):
session["user_role"] = "admin"
page = await render_plugin_template(
app, "host_agent", "panel.html", host=host, reg=reg, reporting=True, stale=False,
target=target, ansible_available=True, managed_key_set=True)
manage = page.index(">Manage agent</summary>")
assert page.index("Full metrics →") < manage
for action in (">Update agent</button>", ">Rotate token</button>", ">Unregister agent</button>"):
assert page.index(action) > manage
assert ">Remove</button>" not in page
async def test_the_hosts_list_says_what_is_wrong_and_labels_its_figures(app):
"""#5279–#5281: labelled CPU and Memory, one uptime figure, status with how
long, no SLA tab and no per-row edit/delete."""
from datetime import datetime, timedelta, timezone
now = datetime.now(timezone.utc)
down = SimpleNamespace(id="h1", name="nas", address="10.0.0.5")
quiet = SimpleNamespace(id="h2", name="pi", address="")
async with app.test_request_context("/hosts/"):
session["user_role"] = "admin"
page = await render_template(
"hosts/list.html", hosts=[down, quiet],
host_status={"h1": {"state": "down", "latency_ms": None, "count": 2,
"down_since": now - timedelta(minutes=12)}},
uptime={"h1": {"24h": 91.5, "7d": 98.0, "30d": 99.42}},
agent={"pi": {"fresh": False, "last_at": now - timedelta(minutes=7),
"cpu_pct": 3.0, "mem_used_pct": 41.0}})
assert re.findall(r"<th[^>]*>([^<]+)</th>", page) == [
"Name", "Address", "Status", "Latency", "Monitors", "30d uptime", "CPU", "Memory"]
assert "for 12m" in page and "agent silent" in page and "for 7m" in page
assert "99.42%" in page and 'title="24h 91.50% · 7d 98.00%"' in page
assert ">41%<" in page and "tone-dim" in page # a silent agent's last reading, greyed
assert "/hosts/uptime" not in page and ">Delete<" not in page and ">Edit<" not in page
async def test_a_host_without_an_agent_can_get_a_manual_install(app):
"""#5282: the manual install moved here from the fleet's add-host form."""
from tests._templates import render_plugin_template
host = SimpleNamespace(id="h1", name="box", address="10.0.0.5")
async with app.test_request_context("/plugins/host_agent/panel/h1"):
session["user_role"] = "admin"
page = await render_plugin_template(
app, "host_agent", "panel.html", host=host, reg=None, reporting=False, stale=False,
target=None, ansible_available=False, managed_key_set=False)
assert 'action="/plugins/host_agent/fleet/h1/register"' in page and ">Install manually</button>" in page
async def test_add_host_is_a_dialog_on_the_hosts_list(app):
"""#5291: the form was two fields on a page of its own."""
async with app.test_request_context("/hosts/"):
session["user_role"] = "operator"
closed = await render_template("hosts/list.html", hosts=[], host_status={}, uptime={}, agent={}, add=None)
dialog = re.search(r'<dialog id="add-host".*?</dialog>', closed, re.S).group(0)
assert 'action="/hosts/"' in dialog and ">Cancel</button>" in dialog
assert re.search(r'<input[^>]*name="address"[^>]*>', dialog).group(0).count("required") == 0
assert "showModal()\">Add host</button>" in closed
assert "getElementById('add-host').showModal();</script>" not in closed
assert "/hosts/new" not in closed
async with app.test_request_context("/hosts/"):
session["user_role"] = "viewer"
viewer = await render_template("hosts/list.html", hosts=[], host_status={}, uptime={}, agent={}, add={})
assert "<dialog" not in viewer and ">Add host<" not in viewer
async def test_add_host_sends_a_clash_back_with_what_was_typed(app):
"""#5283, #5291: the list again, the dialog open, an inline error and the typed values."""
async with app.test_request_context("/hosts/"):
session["user_role"] = "operator"
page = await render_template(
"hosts/list.html", hosts=[], host_status={}, uptime={}, agent={},
add={"values": {"name": "nas", "address": "10.0.0.9"}, "error": "A host named nas already exists."})
assert 'role="alert">A host named nas already exists.<' in page
assert 'value="nas"' in page and 'value="10.0.0.9"' in page
assert "getElementById('add-host').showModal();</script>" in page
async def test_edit_host_keeps_its_page(app):
host = SimpleNamespace(id="h1", name="nas", address="10.0.0.5")
async with app.test_request_context("/hosts/h1/edit"):
session["user_role"] = "operator"
page = await render_template("hosts/form.html", host=host)
assert 'action="/hosts/h1"' in page and 'value="nas"' in page and 'value="10.0.0.5"' in page
assert '<a href="/hosts/h1">nas</a>' in page and ">Save</button>" in page