2026-10-04 16:50:13 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
|
|
|
|
test_loop_health_remediation.py — Unit and integration tests for:
|
|
|
|
|
1. Reconstructing intrinsic loops from followups.json and dm-log.jsonl
|
|
|
|
|
2. Fleet loop health metrics computation and threshold evaluation
|
|
|
|
|
3. Progressive remediation:
|
|
|
|
|
- Soft break healing (auto-resolving answered follow-ups)
|
|
|
|
|
- Deadline re-arming for pending expired nudges
|
|
|
|
|
4. Hard break diagnostic escalations (silent agent, auth rot, systemd failure)
|
|
|
|
|
5. Audit event logging in job-log.jsonl
|
|
|
|
|
"""
|
|
|
|
|
import unittest
|
|
|
|
|
import subprocess
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
import shutil
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from datetime import datetime, timezone
|
2026-10-09 23:13:43 +00:00
|
|
|
from unittest.mock import patch
|
2026-10-04 16:50:13 +00:00
|
|
|
|
|
|
|
|
REPO_ROOT = Path("/home/super/Projects/NetVM")
|
|
|
|
|
BIN_DIR = REPO_ROOT / "bin"
|
|
|
|
|
BOX_CTL = BIN_DIR / "box-ctl.py"
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(BIN_DIR))
|
|
|
|
|
import gravity
|
|
|
|
|
|
|
|
|
|
|
2026-10-09 23:13:43 +00:00
|
|
|
def _load(name, relpath):
|
|
|
|
|
import importlib.util
|
|
|
|
|
spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relpath)
|
|
|
|
|
mod = importlib.util.module_from_spec(spec)
|
|
|
|
|
spec.loader.exec_module(mod)
|
|
|
|
|
return mod
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
box_ctl = _load("box_ctl_loophealthtest", "bin/box-ctl.py")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _InProcResult:
|
|
|
|
|
"""Minimal CompletedProcess stand-in (returncode/stdout only)."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, returncode, stdout):
|
|
|
|
|
self.returncode = returncode
|
|
|
|
|
self.stdout = stdout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _box_ctl_inproc(*args):
|
|
|
|
|
"""In-process box-ctl call (proven pattern from test_box_loop_https)."""
|
|
|
|
|
import io
|
|
|
|
|
from contextlib import redirect_stdout
|
|
|
|
|
buf = io.StringIO()
|
|
|
|
|
returncode = 0
|
|
|
|
|
with redirect_stdout(buf):
|
|
|
|
|
try:
|
|
|
|
|
box_ctl.main(["box-ctl.py", *args])
|
|
|
|
|
except SystemExit as e:
|
|
|
|
|
returncode = e.code if isinstance(e.code, int) else 1
|
|
|
|
|
return _InProcResult(returncode, buf.getvalue())
|
|
|
|
|
|
|
|
|
|
|
2026-10-04 16:50:13 +00:00
|
|
|
class TestLoopDiagnosticsAndRemediation(unittest.TestCase):
|
|
|
|
|
"""Test gravity.py loop health and progressive remediation."""
|
|
|
|
|
|
|
|
|
|
def test_fleet_loop_health_calculation(self):
|
|
|
|
|
health = gravity.get_fleet_loop_health(threshold=0.5)
|
|
|
|
|
self.assertIn("agents", health)
|
|
|
|
|
self.assertIn("summary", health)
|
|
|
|
|
self.assertIn("healthy", health["summary"])
|
|
|
|
|
self.assertIn("overall_health", health["summary"])
|
|
|
|
|
|
|
|
|
|
def test_reconstruct_loops(self):
|
|
|
|
|
loops = gravity.reconstruct_loops(limit=10)
|
|
|
|
|
self.assertIsInstance(loops, list)
|
|
|
|
|
if loops:
|
|
|
|
|
l = loops[0]
|
|
|
|
|
self.assertIn("loop_id", l)
|
|
|
|
|
self.assertIn("agent", l)
|
|
|
|
|
self.assertIn("state", l)
|
|
|
|
|
|
|
|
|
|
def test_diagnose_breaks_structure(self):
|
|
|
|
|
breaks = gravity.diagnose_breaks()
|
|
|
|
|
self.assertIsInstance(breaks, list)
|
|
|
|
|
for b in breaks:
|
|
|
|
|
self.assertIn("type", b)
|
|
|
|
|
self.assertIn("severity", b)
|
|
|
|
|
self.assertIn("detail", b)
|
|
|
|
|
|
|
|
|
|
def test_remediate_breaks_dry_run(self):
|
|
|
|
|
res = gravity.remediate_breaks(dry_run=True)
|
|
|
|
|
self.assertTrue(res.get("ok"))
|
|
|
|
|
self.assertTrue(res.get("dry_run"))
|
|
|
|
|
self.assertIsInstance(res.get("remediated"), list)
|
|
|
|
|
self.assertIsInstance(res.get("escalated"), list)
|
|
|
|
|
|
2026-10-09 23:13:43 +00:00
|
|
|
def test_remediate_single_approval_scan(self):
|
|
|
|
|
import approvals
|
|
|
|
|
real = approvals.check_fleet_approvals
|
|
|
|
|
with patch.object(approvals, "check_fleet_approvals", wraps=real) as spy:
|
|
|
|
|
res = gravity.remediate_breaks(dry_run=True)
|
|
|
|
|
self.assertTrue(res.get("ok"))
|
|
|
|
|
self.assertEqual(spy.call_count, 1)
|
|
|
|
|
|
|
|
|
|
def test_remediate_single_loop_parse(self):
|
|
|
|
|
real = gravity._load_loop_candidates
|
|
|
|
|
with patch.object(gravity, "_load_loop_candidates", wraps=real) as spy:
|
|
|
|
|
res = gravity.remediate_breaks(dry_run=True)
|
|
|
|
|
self.assertTrue(res.get("ok"))
|
|
|
|
|
self.assertEqual(spy.call_count, 1)
|
|
|
|
|
|
2026-10-04 16:50:13 +00:00
|
|
|
|
|
|
|
|
class TestLoopRpc(unittest.TestCase):
|
|
|
|
|
"""Test box-ctl.py allowlisted RPC actions for loops."""
|
|
|
|
|
|
|
|
|
|
def test_box_ctl_loop_health(self):
|
2026-10-09 23:13:43 +00:00
|
|
|
res = _box_ctl_inproc("loop-health")
|
2026-10-04 16:50:13 +00:00
|
|
|
self.assertEqual(res.returncode, 0)
|
|
|
|
|
data = json.loads(res.stdout)
|
|
|
|
|
self.assertTrue(data.get("ok"))
|
|
|
|
|
self.assertIn("agents", data)
|
|
|
|
|
self.assertIn("summary", data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_box_ctl_loop_status(self):
|
2026-10-09 23:13:43 +00:00
|
|
|
res = _box_ctl_inproc("loop-status", "--limit", "5")
|
2026-10-04 16:50:13 +00:00
|
|
|
self.assertEqual(res.returncode, 0)
|
|
|
|
|
data = json.loads(res.stdout)
|
|
|
|
|
self.assertTrue(data.get("ok"))
|
|
|
|
|
self.assertIn("loops", data)
|
|
|
|
|
|
|
|
|
|
def test_box_ctl_loop_remediate_dry_run(self):
|
2026-10-09 23:13:43 +00:00
|
|
|
res = _box_ctl_inproc("loop-remediate", "--dry-run")
|
2026-10-04 16:50:13 +00:00
|
|
|
self.assertEqual(res.returncode, 0)
|
|
|
|
|
data = json.loads(res.stdout)
|
|
|
|
|
self.assertTrue(data.get("ok"))
|
|
|
|
|
self.assertTrue(data.get("dry_run"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|