On 2026-10-25 the 09:00 job on VDNT-Docker02 drained the node, which ran Traefik and Steward. The agent's result reaches Steward through Traefik, and Traefik took 11.5 minutes to start on Docker03 (its image had to be pulled). The job gave up at the 10-minute drain deadline and finished timed_out where it stood, so the node stayed drained for hours. - A job that fails or times out while its node may be drained (the drain was handed out, or went through and wasn't undone) goes to undraining first. The outcome is kept in the new column ends_as, and the job finishes as that once the node is active again. If the undrain goes wrong too, detail says both. - The drain deadline is 30 minutes instead of 10, so a result held up while the proxy moves still lands. - Migration host_agent_012_ends_as. get_maintenance_job returns ends_as. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
50 lines
2.5 KiB
Python
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,
|
|
ends_as=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"]
|