Files
FabledSteward/tests/plugins/host_agent/test_mcp_tools.py
T
bvandeusenandClaude Opus 5.5 8e2f17d198
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m31s
CI / integration (push) Successful in 1m39s
CI / publish (push) Successful in 1m26s
fix(maintenance): a late return, once back, stays back, and every reader agrees (#6175)
18032a4 re-checked a "not back in service" job every minute with no end,
so any later swarm wobble on the host (radarr's failed task during the
downloads move, say) flipped maintenance_failed back to 1: five false
firings and ten emails for Docker02 overnight on 10-19. And the overview's
finding never learned the rule at all, so it still said Docker02's
maintenance failed three days on.

The job now keeps came_back_at, set the first time its host is found back.
maintenance.still_failed(job) is the one answer: the figure, the overview,
the cadence page's run rows and the host panel all ask it. A failed update
or a timed-out job still flags its host until a job succeeds.

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

50 lines
2.5 KiB
Python

"""The host_agent plugin's MCP tools."""
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace as NS
from unittest.mock import MagicMock
import plugins.host_agent as agent_plugin
from plugins.host_agent.mcp_tools import agent_state
NOW = datetime(2026, 10, 6, 12, 0, tzinfo=timezone.utc)
def test_agent_state():
assert agent_state(NS(last_seen_at=None), NOW, 180) == "pending"
assert agent_state(NS(last_seen_at=NOW - timedelta(seconds=60)), NOW, 180) == "reporting"
assert agent_state(NS(last_seen_at=NOW - timedelta(minutes=10)), NOW, 180) == "stale"
def test_plugin_exposes_the_mcp_hooks():
reg = MagicMock()
agent_plugin.register_mcp_tools(reg)
assert [c.args[0].__name__ for c in reg.call_args_list] == [
"list_agents", "list_maintenance_plans", "get_maintenance_job", "get_host_senders",
"run_maintenance"]
roles = {c.args[0].__name__: c.kwargs["role"] for c in reg.call_args_list}
assert roles["run_maintenance"].value == "admin" # as Update now on the page
assert roles["get_maintenance_job"].value == "viewer"
assert roles["get_host_senders"].value == "viewer"
assert callable(agent_plugin.mcp_overview)
def test_a_job_row_says_what_it_did_and_when_it_gives_up():
from plugins.host_agent.mcp_tools import _job_row
job = NS(id="j1", host_id="h1", state="rebooting", state_at=NOW - timedelta(minutes=5),
reboot_policy="if_required", packages=12, reboot_needed=True, back_at=None,
node_id="n1", drain_host_id="h2", detail=None, created_by=None, run_id="r1",
created_at=NOW - timedelta(minutes=20), finished_at=None,
drained_at=NOW - timedelta(minutes=11), docker_update=False, came_back_at=None,
checked={"pending": 12, "docker_packages": [], "containers": []})
row = _job_row(job, {"h1": "web-1", "h2": "mgr-1"}, NOW)
assert (row["host"], row["drained_by"], row["label"], row["active"]) == (
"web-1", "mgr-1", "Rebooting", True)
assert row["started_by"] == "maintenance plan" and row["state_age_s"] == 300
assert row["gives_up_at"] == (NOW + timedelta(minutes=25)).isoformat().replace("+00:00", "Z")
done = NS(**{**job.__dict__, "state": "done", "back_at": NOW, "finished_at": NOW,
"created_by": "u1", "run_id": None})
row = _job_row(done, {"h1": "web-1", "u1": "alice"}, NOW)
assert row["summary"] == "Done: 12 packages updated, rebooted"
assert row["started_by"] == "alice" and row["gives_up_at"] is None and not row["active"]