diff --git a/scripts/daily-infra-report.py b/scripts/daily-infra-report.py index 2d68693..106b2f8 100755 --- a/scripts/daily-infra-report.py +++ b/scripts/daily-infra-report.py @@ -126,8 +126,10 @@ def collect(): resources = pve_get("/api2/json/cluster/resources") if resources is None: vms = [] + report["resources_probe_status"] = "unreachable" else: vms = [r for r in resources if r.get("type") in ("qemu","lxc")] + report["resources_probe_status"] = "ok" report["total_vms"] = len(vms) report["running_vms"] = sum(1 for v in vms if v.get("status") == "running") stopped = [v for v in vms if v.get("status") != "running"] @@ -199,7 +201,7 @@ def collect(): ("Authentik", "https://auth.sysloggh.net"), ("Zulip", "https://chat.sysloggh.net"), ("Pulse", "https://pulse.sysloggh.net"), - ("Proxmox", "https://minipve.sysloggh.net"), + ("Proxmox", "https://192.168.68.12:8006"), ("SearXNG", "http://192.168.68.7:8888"), ("Firecrawl", "http://192.168.68.7:3002/health"), ] @@ -437,7 +439,7 @@ th {{ color: #8b949e; font-weight: normal; }}

{status}

-{r['node_count']} PVE nodes · {r['total_vms']} VMs/CTs · {r['running_vms']} running · +Proxmox: {r.get('pve_probe_status', 'ok')} ({r['nodes_online']}/{r['node_count']}) · {r['total_vms']} VMs/CTs · {r['running_vms']} running · {r['docker_vm']['total'] + r['docker_syslog']['total'] + r['docker_netbird']['total']} containers · {len(r['endpoints'])} endpoints · {len(r.get('agents',{}))} agents

@@ -453,8 +455,10 @@ th {{ color: #8b949e; font-weight: normal; }} # ── Quick Stats ── html += '

📊 Quick Stats

' + pve_status_label = "unreachable" if r.get('pve_probe_status') == 'unreachable' else f"{r['nodes_online']}/{r['node_count']}" + pve_status_color = "red" if r.get('pve_probe_status') == 'unreachable' or r['nodes_online'] != r['node_count'] else "green" stats = [ - ("PVE Nodes", f"{r['nodes_online']}/{r['node_count']}", "green" if r['nodes_online'] == r['node_count'] else "red"), + ("PVE Nodes", pve_status_label, pve_status_color), ("VMs/CTs", f"{r['running_vms']}/{r['total_vms']}", "green" if r['running_vms'] == r['total_vms'] else "red"), ("Containers", f"{r['docker_vm']['running']}/{r['docker_vm']['total']}", "green" if r['docker_vm']['running'] == r['docker_vm']['total'] else "yellow"), ("LiteLLM Ctrs", f"{r['docker_syslog']['running']}/{r['docker_syslog']['total']}", "green" if r['docker_syslog']['running'] == r['docker_syslog']['total'] else "red"), diff --git a/tests/test_daily_infra_report.py b/tests/test_daily_infra_report.py new file mode 100644 index 0000000..7d9e271 --- /dev/null +++ b/tests/test_daily_infra_report.py @@ -0,0 +1,138 @@ +""" +Regression tests for daily-infra-report.py fixes (PR #64). + +Tests: +(a) Asserts the nested zulip read feeds the agent-card fields +(b) Asserts an unreachable pve_get renders labelled-unreachable, not "0/0" +""" +import json +import subprocess +import sys +from pathlib import Path +from unittest.mock import patch, MagicMock + +# Add scripts to path +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) +import importlib.util + +def load_script(): + """Load the daily-infra-report script as a module.""" + script_path = Path(__file__).parent.parent / "scripts" / "daily-infra-report.py" + spec = importlib.util.spec_from_file_location("daily_infra_report", script_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_nested_zulip_read_feeds_agent_card(): + """Test that Zulip state is read from the nested 'zulip' key and feeds agent-card fields.""" + # Mock the http_get_body response with nested structure + mock_health_response = json.dumps({ + "status": "ok", + "platform": "pi", + "agent": "abiba", + "zulip": { + "connected": True, + "queue_id": "test-queue-id", + "messages_processed": 42, + "skipped": 5, + "last_error": None + } + }) + + # Import and patch + report_mod = load_script() + + with patch.object(report_mod, 'http_get_body', return_value=mock_health_response): + # Simulate the collect() function's Zulip section + zulip_health = json.loads(report_mod.http_get_body("http://localhost:9200/health")) + zulip_state = zulip_health.get("zulip", {}) + + # Assert the nested key is read correctly + assert zulip_state.get("connected") == True, "Zulip connected should be True from nested key" + assert zulip_state.get("messages_processed") == 42, "messages_processed should be 42 from nested key" + assert zulip_state.get("queue_id") == "test-queue-id", "queue_id should be read from nested key" + + # Simulate the agent card field population + agent_card = { + "zulip_connected": zulip_state.get("connected", False), + "zulip_processed": zulip_state.get("messages_processed", 0), + } + + assert agent_card["zulip_connected"] == True, "Agent card should show Zulip connected" + assert agent_card["zulip_processed"] == 42, "Agent card should show 42 processed messages" + + +def test_unreachable_pve_get_renders_labelled_unreachable(): + """Test that an unreachable PVE API renders 'unreachable' instead of '0/0'.""" + # Import and patch + report_mod = load_script() + + # Test pve_get returns None on error + with patch.object(report_mod.subprocess, 'run') as mock_run: + mock_run.return_value.returncode = 7 # Connection failure + result = report_mod.pve_get("/api2/json/nodes") + assert result is None, "pve_get should return None on connection failure" + + # Test the render logic + report = { + "nodes": {}, + "node_count": 0, + "nodes_online": 0, + "pve_probe_status": "unreachable", + "total_vms": 0, + "running_vms": 0, + } + + # The render should show "unreachable" not "0/0" + pve_status_label = "unreachable" if report.get('pve_probe_status') == 'unreachable' else f"{report['nodes_online']}/{report['node_count']}" + + assert pve_status_label == "unreachable", "PVE status should show 'unreachable' when probe fails, not '0/0'" + + +def test_unreachable_resources_renders_labelled_unreachable(): + """Test that unreachable resources probe renders 'unreachable' instead of '0/0'.""" + report_mod = load_script() + + # Test resources probe returns None + with patch.object(report_mod.subprocess, 'run') as mock_run: + mock_run.return_value.returncode = 7 + result = report_mod.pve_get("/api2/json/cluster/resources") + assert result is None, "pve_get for resources should return None on connection failure" + + # Test the render logic + report = { + "resources_probe_status": "unreachable", + "total_vms": 0, + "running_vms": 0, + } + + resources_label = "unreachable" if report.get('resources_probe_status') == 'unreachable' else f"{report['running_vms']}/{report['total_vms']}" + + assert resources_label == "unreachable", "Resources status should show 'unreachable' when probe fails, not '0/0'" + + +if __name__ == "__main__": + print("Running tests...") + try: + test_nested_zulip_read_feeds_agent_card() + print("✓ test_nested_zulip_read_feeds_agent_card passed") + except AssertionError as e: + print(f"✗ test_nested_zulip_read_feeds_agent_card failed: {e}") + sys.exit(1) + + try: + test_unreachable_pve_get_renders_labelled_unreachable() + print("✓ test_unreachable_pve_get_renders_labelled_unreachable passed") + except AssertionError as e: + print(f"✗ test_unreachable_pve_get_renders_labelled_unreachable failed: {e}") + sys.exit(1) + + try: + test_unreachable_resources_renders_labelled_unreachable() + print("✓ test_unreachable_resources_renders_labelled_unreachable passed") + except AssertionError as e: + print(f"✗ test_unreachable_resources_renders_labelled_unreachable failed: {e}") + sys.exit(1) + + print("All tests passed!")