af60ca446d
The fabledscryer->steward rename had only ever reached host_agent. The other five bundled plugins (http, snmp, traefik, unifi, docker) still imported `from fabledscryer.*` (package no longer exists) and read FABLEDSCRYER_* env vars — so every one of them was broken at import since the original rebrand. CI stayed green only because none are enabled by default and migrations don't import plugin modules. Now that they version in-tree, complete the rename: - fabledscryer.* -> steward.* imports across all five plugins - FABLEDSCRYER_* -> STEWARD_* in plugin migration env.py files - author/repository/homepage + user-facing 'Fabled Scryer' strings -> Steward - snmp/scheduler.py: also drop dead `now`/datetime; record_metric from steward Adds tests/test_no_legacy_names.py — fails if 'scryer'/'roundtable' ever reappear in shipped code (the drift bit twice; this stops a third time). Also clears pre-existing ruff lint debt (unused imports, semicolon statements, mid-file import) surfaced by the new lint lane. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
4.1 KiB
Python
115 lines
4.1 KiB
Python
import urllib.error
|
|
from unittest.mock import patch
|
|
from plugins.host_agent import agent as a
|
|
|
|
|
|
def test_build_sample_shape():
|
|
fake_mem = {"total_bytes": 100, "used_bytes": 40,
|
|
"available_bytes": 60, "swap_used_bytes": 0}
|
|
fake_load = {"1m": 0.1, "5m": 0.2, "15m": 0.3}
|
|
fake_storage = [{"mount": "/", "total_bytes": 1000, "used_bytes": 400}]
|
|
|
|
with patch.object(a, "collect_cpu", return_value=12.5), \
|
|
patch.object(a, "collect_memory", return_value=fake_mem), \
|
|
patch.object(a, "collect_load", return_value=fake_load), \
|
|
patch.object(a, "collect_uptime", return_value=1234), \
|
|
patch.object(a, "collect_storage", return_value=fake_storage):
|
|
sample = a.build_sample(mounts=["/"])
|
|
|
|
assert sample["cpu_pct"] == 12.5
|
|
assert sample["mem"]["used_bytes"] == 40
|
|
assert sample["load"]["1m"] == 0.1
|
|
assert sample["uptime_secs"] == 1234
|
|
assert sample["storage"][0]["mount"] == "/"
|
|
assert "ts" in sample
|
|
# ISO-8601 UTC string
|
|
assert "T" in sample["ts"]
|
|
|
|
|
|
def test_build_sample_tolerates_collector_failure():
|
|
# If a collector raises, the sample should still be built with None for that field.
|
|
def _boom():
|
|
raise RuntimeError("no /proc")
|
|
|
|
with patch.object(a, "collect_cpu", side_effect=_boom), \
|
|
patch.object(a, "collect_memory", return_value={"total_bytes": 1, "used_bytes": 0,
|
|
"available_bytes": 1, "swap_used_bytes": 0}), \
|
|
patch.object(a, "collect_load", return_value={"1m": 0.0, "5m": 0.0, "15m": 0.0}), \
|
|
patch.object(a, "collect_uptime", return_value=0), \
|
|
patch.object(a, "collect_storage", return_value=[]):
|
|
sample = a.build_sample(mounts=["/"])
|
|
assert sample["cpu_pct"] is None
|
|
assert sample["mem"]["total_bytes"] == 1
|
|
|
|
|
|
def test_build_payload_wraps_samples():
|
|
metadata = {"kernel": "6.8", "distro": "Ubuntu", "arch": "x86_64"}
|
|
payload = a.build_payload(
|
|
samples=[{"ts": "2026-04-14T00:00:00+00:00", "cpu_pct": 5.0}],
|
|
hostname="myhost",
|
|
metadata=metadata,
|
|
)
|
|
assert payload["agent_version"] == a.AGENT_VERSION
|
|
assert payload["hostname"] == "myhost"
|
|
assert payload["metadata"] == metadata
|
|
assert len(payload["samples"]) == 1
|
|
assert payload["samples"][0]["cpu_pct"] == 5.0
|
|
|
|
|
|
def test_post_payload_success():
|
|
from plugins.host_agent import agent as a
|
|
captured = {}
|
|
|
|
class FakeResp:
|
|
status = 200
|
|
def __enter__(self): return self
|
|
def __exit__(self, *a): return False
|
|
def read(self): return b'{"ok":true}'
|
|
|
|
def fake_urlopen(req, timeout=10):
|
|
captured["url"] = req.full_url
|
|
captured["headers"] = dict(req.header_items())
|
|
captured["body"] = req.data
|
|
return FakeResp()
|
|
|
|
with patch.object(a.urllib.request, "urlopen", side_effect=fake_urlopen):
|
|
ok, status = a.post_payload("https://x", "tok", {"hello": "world"})
|
|
assert ok is True
|
|
assert status == 200
|
|
assert captured["url"] == "https://x/plugins/host_agent/ingest"
|
|
assert captured["headers"]["Authorization"] == "Bearer tok"
|
|
|
|
|
|
def test_post_payload_http_error_returns_status():
|
|
from plugins.host_agent import agent as a
|
|
|
|
def raise_401(req, timeout=10):
|
|
raise urllib.error.HTTPError(req.full_url, 401, "unauthorized", {}, None)
|
|
|
|
with patch.object(a.urllib.request, "urlopen", side_effect=raise_401):
|
|
ok, status = a.post_payload("https://x", "tok", {})
|
|
assert ok is False
|
|
assert status == 401
|
|
|
|
|
|
def test_post_payload_network_error_returns_none_status():
|
|
from plugins.host_agent import agent as a
|
|
|
|
def raise_url(req, timeout=10):
|
|
raise urllib.error.URLError("conn refused")
|
|
|
|
with patch.object(a.urllib.request, "urlopen", side_effect=raise_url):
|
|
ok, status = a.post_payload("https://x", "tok", {})
|
|
assert ok is False
|
|
assert status is None
|
|
|
|
|
|
def test_backoff_schedule():
|
|
from plugins.host_agent.agent import next_backoff
|
|
assert next_backoff(0) == 30
|
|
assert next_backoff(30) == 60
|
|
assert next_backoff(60) == 120
|
|
assert next_backoff(120) == 240
|
|
assert next_backoff(240) == 300 # capped
|
|
assert next_backoff(300) == 300
|