feat(tests): add comprehensive unit test suite for variables, strategy modulation, and loop health
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
#!/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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class TestLoopRpc(unittest.TestCase):
|
||||
"""Test box-ctl.py allowlisted RPC actions for loops."""
|
||||
|
||||
def test_box_ctl_loop_health(self):
|
||||
cmd = [sys.executable, str(BOX_CTL), "loop-health"]
|
||||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||||
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):
|
||||
cmd = [sys.executable, str(BOX_CTL), "loop-status", "--limit", "5"]
|
||||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||||
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):
|
||||
cmd = [sys.executable, str(BOX_CTL), "loop-remediate", "--dry-run"]
|
||||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||||
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()
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
test_modulate_strategy.py — Unit and integration tests for:
|
||||
1. Hierarchical override scoping:
|
||||
(input_type, subtype, agent) -> (input_type, None, agent) ->
|
||||
(input_type, subtype, None) -> (input_type, None, None) -> Builtin table
|
||||
2. Dynamic strategy evaluation (`modulate.py eval` / `box-ctl.py strat-get`)
|
||||
3. Atomic persistence to strategy.json
|
||||
4. Override resets and isolation across input types
|
||||
"""
|
||||
import unittest
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
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 modulate
|
||||
|
||||
|
||||
class TestModulationHierarchy(unittest.TestCase):
|
||||
"""Test strategy lookup cascade and overrides."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp_dir = tempfile.mkdtemp()
|
||||
self.strat_file = str(Path(self.tmp_dir) / "strategy.json")
|
||||
self.orig_default = modulate.DEFAULT_STRATEGY_FILE
|
||||
self.orig_fallback = modulate.FALLBACK_STRATEGY_FILE
|
||||
modulate.DEFAULT_STRATEGY_FILE = self.strat_file
|
||||
modulate.FALLBACK_STRATEGY_FILE = self.strat_file
|
||||
|
||||
def tearDown(self):
|
||||
modulate.DEFAULT_STRATEGY_FILE = self.orig_default
|
||||
modulate.FALLBACK_STRATEGY_FILE = self.orig_fallback
|
||||
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def test_builtin_fallback(self):
|
||||
# Default builtin policy for manual DMs
|
||||
pol = modulate.modulate(modulate.InputType.MANUAL, agent="646")
|
||||
self.assertIsNotNone(pol)
|
||||
self.assertTrue(pol.track)
|
||||
self.assertEqual(pol.timeout_s, 3600)
|
||||
|
||||
def test_agent_specific_override(self):
|
||||
# Set override for agent 'pip' on 'manual'
|
||||
modulate.set_strategy_override(
|
||||
"manual",
|
||||
None,
|
||||
"pip",
|
||||
track=False,
|
||||
timeout_s=1800,
|
||||
nudges=0,
|
||||
escalate="none",
|
||||
)
|
||||
|
||||
# Pip gets the custom override (track=False -> modulate returns None)
|
||||
pip_pol = modulate.modulate(modulate.InputType.MANUAL, agent="pip")
|
||||
self.assertIsNone(pip_pol)
|
||||
|
||||
# 646 gets the default builtin policy
|
||||
other_pol = modulate.modulate(modulate.InputType.MANUAL, agent="646")
|
||||
self.assertTrue(other_pol.track)
|
||||
self.assertEqual(other_pol.timeout_s, 3600)
|
||||
|
||||
|
||||
def test_subtype_hierarchical_resolution(self):
|
||||
# Set type-level default
|
||||
modulate.set_strategy_override("health", None, None, timeout_s=900)
|
||||
# Set subtype override
|
||||
modulate.set_strategy_override("health", "DEGRADED", None, timeout_s=3600)
|
||||
|
||||
# Health without subtype gets type-level
|
||||
pol1 = modulate.modulate(modulate.InputType.HEALTH, subtype="OK")
|
||||
self.assertEqual(pol1.timeout_s, 900)
|
||||
|
||||
# Health with 'DEGRADED' subtype gets subtype-specific
|
||||
pol2 = modulate.modulate(modulate.InputType.HEALTH, subtype="DEGRADED")
|
||||
self.assertEqual(pol2.timeout_s, 3600)
|
||||
|
||||
def test_override_reset(self):
|
||||
modulate.set_strategy_override("wake", None, "opm", timeout_s=600)
|
||||
self.assertEqual(modulate.modulate(modulate.InputType.WAKE, agent="opm", actionable=True).timeout_s, 600)
|
||||
|
||||
modulate.reset_strategy_override("wake", None, "opm")
|
||||
# Reverts to builtin default (7200s for wake)
|
||||
self.assertEqual(modulate.modulate(modulate.InputType.WAKE, agent="opm", actionable=True).timeout_s, 7200)
|
||||
|
||||
|
||||
|
||||
class TestStrategyRpc(unittest.TestCase):
|
||||
"""Test box-ctl.py allowlisted RPC actions for strategy."""
|
||||
|
||||
def test_box_ctl_strat_list(self):
|
||||
cmd = [sys.executable, str(BOX_CTL), "strat-list"]
|
||||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||||
self.assertEqual(res.returncode, 0)
|
||||
data = json.loads(res.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
self.assertIn("strategies", data)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
test_variables_engine.py — Unit and integration tests for:
|
||||
1. Schema validation (int, float, bool, string, ranges, choices)
|
||||
2. Dual-synced atomic persistence between local and /srv/box/
|
||||
3. Append-only audit history logging to variables-history.jsonl
|
||||
4. Atomic rollback to previous revisions
|
||||
5. box-ctl.py allowlisted RPC actions for variables
|
||||
"""
|
||||
import unittest
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path("/home/super/Projects/NetVM")
|
||||
BIN_DIR = REPO_ROOT / "bin"
|
||||
VARS_PY = BIN_DIR / "variables.py"
|
||||
BOX_CTL = BIN_DIR / "box-ctl.py"
|
||||
|
||||
sys.path.insert(0, str(BIN_DIR))
|
||||
import variables
|
||||
|
||||
|
||||
class TestVariablesEngine(unittest.TestCase):
|
||||
"""Test schema enforcement and in-memory variable operations."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp_dir = tempfile.mkdtemp()
|
||||
self.var_file = Path(self.tmp_dir) / "variables.json"
|
||||
self.hist_file = Path(self.tmp_dir) / "variables-history.jsonl"
|
||||
self.engine = variables.Variables(path=str(self.var_file), history_path=str(self.hist_file))
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp_dir, ignore_errors=True)
|
||||
|
||||
def test_default_values_load(self):
|
||||
val = self.engine.get("loop_health_threshold")
|
||||
self.assertEqual(val, 0.5)
|
||||
val_sec = self.engine.get("loop_health_interval_s")
|
||||
self.assertEqual(val_sec, 900)
|
||||
|
||||
def test_schema_validation_out_of_range(self):
|
||||
# loop_health_threshold range: 0.0 .. 1.0
|
||||
with self.assertRaises(ValueError):
|
||||
self.engine.set("loop_health_threshold", 1.5)
|
||||
with self.assertRaises(ValueError):
|
||||
self.engine.set("loop_health_threshold", -0.1)
|
||||
|
||||
def test_schema_validation_type_coercion(self):
|
||||
self.engine.set("loop_health_interval_s", "1200")
|
||||
self.assertEqual(self.engine.get("loop_health_interval_s"), 1200)
|
||||
|
||||
def test_history_logging_and_rollback(self):
|
||||
initial = self.engine.get("max_nudge_count")
|
||||
self.engine.set("max_nudge_count", 5, by="operator_test")
|
||||
self.assertEqual(self.engine.get("max_nudge_count"), 5)
|
||||
|
||||
# Check history recorded
|
||||
hist = self.engine.history(name="max_nudge_count", limit=5)
|
||||
self.assertGreaterEqual(len(hist), 1)
|
||||
self.assertEqual(hist[0]["action"], "set")
|
||||
self.assertEqual(hist[0]["new_value"], 5)
|
||||
|
||||
# Rollback to initial
|
||||
rb_val = self.engine.rollback("max_nudge_count", by="operator_test")
|
||||
self.assertEqual(rb_val, initial)
|
||||
self.assertEqual(self.engine.get("max_nudge_count"), initial)
|
||||
|
||||
def test_reset_variable(self):
|
||||
self.engine.set("sweeper_interval_s", 120)
|
||||
self.assertEqual(self.engine.get("sweeper_interval_s"), 120)
|
||||
res_val = self.engine.reset("sweeper_interval_s")
|
||||
self.assertEqual(res_val, 60)
|
||||
self.assertEqual(self.engine.get("sweeper_interval_s"), 60)
|
||||
|
||||
|
||||
|
||||
class TestVariablesCliAndRpc(unittest.TestCase):
|
||||
"""Test CLI commands and box-ctl RPC actions for variables."""
|
||||
|
||||
def test_box_ctl_vars_list(self):
|
||||
cmd = [sys.executable, str(BOX_CTL), "vars-list"]
|
||||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||||
self.assertEqual(res.returncode, 0)
|
||||
data = json.loads(res.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
self.assertIn("variables", data)
|
||||
self.assertIn("loop_health_threshold", data["variables"])
|
||||
|
||||
def test_box_ctl_vars_history(self):
|
||||
cmd = [sys.executable, str(BOX_CTL), "vars-history", "loop_health_threshold", "5"]
|
||||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||||
self.assertEqual(res.returncode, 0)
|
||||
data = json.loads(res.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
self.assertIsInstance(data.get("history"), list)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user