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