fix(work): import hashlib and wire heal subparser into main CLI
This commit is contained in:
+82
-25
@@ -25,6 +25,58 @@ import approvals
|
||||
import gravity
|
||||
|
||||
|
||||
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_approvaltest", "bin/box-ctl.py")
|
||||
super_cli = _load("super_cli_approvaltest", "bin/super-cli.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).
|
||||
|
||||
Real main(argv): identical parsing, dispatch, audit, stdout JSON.
|
||||
"""
|
||||
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())
|
||||
|
||||
|
||||
def _super_cli_inproc(*args):
|
||||
"""In-process super-cli call (main() reads sys.argv; patch it)."""
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
buf = io.StringIO()
|
||||
returncode = 0
|
||||
with mock.patch.object(sys, "argv", ["super-cli.py", *args]):
|
||||
with redirect_stdout(buf):
|
||||
try:
|
||||
super_cli.main()
|
||||
except SystemExit as e:
|
||||
returncode = e.code if isinstance(e.code, int) else 1
|
||||
return _InProcResult(returncode, buf.getvalue())
|
||||
|
||||
|
||||
class TestApprovalsModule(unittest.TestCase):
|
||||
"""Test approvals.py core module functionality."""
|
||||
|
||||
@@ -79,8 +131,7 @@ class TestBoxApprovalsCli(unittest.TestCase):
|
||||
"""Test 'box approvals' and 'box approval' CLI commands."""
|
||||
|
||||
def test_box_approvals_check_json(self):
|
||||
cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "approvals", "check", "--json"]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
r = _super_cli_inproc("approvals", "check", "--json")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
@@ -88,15 +139,13 @@ class TestBoxApprovalsCli(unittest.TestCase):
|
||||
self.assertIsInstance(data["approvals"], list)
|
||||
|
||||
def test_box_approval_alias(self):
|
||||
cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "approval", "--json"]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
r = _super_cli_inproc("approval", "--json")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
|
||||
def test_box_approvals_auto_json(self):
|
||||
cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "approvals", "auto", "--node", "pip", "--json"]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
r = _super_cli_inproc("approvals", "auto", "--node", "pip", "--json")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
@@ -106,16 +155,14 @@ class TestBoxCtlApprovals(unittest.TestCase):
|
||||
"""Test box-ctl.py allowlisted RPC actions."""
|
||||
|
||||
def test_box_ctl_approval_check(self):
|
||||
cmd = [sys.executable, str(BIN_DIR / "box-ctl.py"), "approval-check"]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
r = _box_ctl_inproc("approval-check")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
self.assertIn("approvals", data)
|
||||
|
||||
def test_box_ctl_approval_auto(self):
|
||||
cmd = [sys.executable, str(BIN_DIR / "box-ctl.py"), "approval-auto", "pip"]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
r = _box_ctl_inproc("approval-auto", "pip")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
@@ -162,6 +209,22 @@ class TestApprovalsReplySafety(unittest.TestCase):
|
||||
class TestKeyApprovalsAndPasskey(unittest.TestCase):
|
||||
"""Test key approval workflow and passkey retrieval architecture."""
|
||||
|
||||
def test_key_scan_tail_fallback_finds_old_request(self):
|
||||
# A live request older than the tail cap must still resolve via the
|
||||
# full-scan fallback (synthetic log; never touches real audit state).
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
p = Path(td) / "box-ctl.jsonl"
|
||||
old = {"ts": "2026-01-01T00:00:00Z", "action": "key-approval-request",
|
||||
"type": "key", "name": "dev", "reason": "buried-old-request",
|
||||
"caller": "t", "expires_at": "2030-01-01T00:00:00Z"}
|
||||
filler = {"ts": "2026-06-01T00:00:00Z", "action": "noop", "name": "x"}
|
||||
lines = [json.dumps(old)] + [json.dumps(filler)] * (approvals.KEY_SCAN_TAIL_LINES + 10)
|
||||
p.write_text("\n".join(lines) + "\n")
|
||||
with mock.patch.object(approvals, "CTL_LOG", p):
|
||||
res = approvals.check_node_key_request("dev")
|
||||
self.assertIsNotNone(res)
|
||||
self.assertEqual(res.get("reason"), "buried-old-request")
|
||||
|
||||
def test_request_and_resolve_key_approval(self):
|
||||
req = approvals.request_key_approval("dev", reason="UnitTest passkey verification", caller="unit-test")
|
||||
self.assertTrue(req.get("ok"))
|
||||
@@ -188,8 +251,7 @@ class TestKeyApprovalsAndPasskey(unittest.TestCase):
|
||||
self.assertIsNone(cleared)
|
||||
|
||||
def test_box_passkey_info_json(self):
|
||||
cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "passkey", "--json"]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
r = _super_cli_inproc("passkey", "--json")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
@@ -199,8 +261,7 @@ class TestKeyApprovalsAndPasskey(unittest.TestCase):
|
||||
self.assertEqual(data["key_location"]["canonical_path"], "/srv/box/passkey.txt")
|
||||
|
||||
def test_box_passkey_fetch_json(self):
|
||||
cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "passkey", "fetch", "--json"]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
r = _super_cli_inproc("passkey", "fetch", "--json")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertIn("operator_pin", data)
|
||||
@@ -210,8 +271,7 @@ class TestKeyApprovalsAndPasskey(unittest.TestCase):
|
||||
self.assertIn("operator_command", data)
|
||||
|
||||
def test_box_lookup_key(self):
|
||||
cmd = [sys.executable, str(BIN_DIR / "super-cli.py"), "lookup", "key", "--json"]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
r = _super_cli_inproc("lookup", "key", "--json")
|
||||
self.assertEqual(r.returncode, 0)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
@@ -219,29 +279,26 @@ class TestKeyApprovalsAndPasskey(unittest.TestCase):
|
||||
|
||||
def test_cli_request_key_lifecycle(self):
|
||||
# 1. Request key
|
||||
r_req = subprocess.run([
|
||||
sys.executable, str(BIN_DIR / "super-cli.py"),
|
||||
r_req = _super_cli_inproc(
|
||||
"approvals", "request-key", "dev", "--reason", "CLI lifecycle test", "--json"
|
||||
], capture_output=True, text=True)
|
||||
)
|
||||
self.assertEqual(r_req.returncode, 0)
|
||||
req_data = json.loads(r_req.stdout)
|
||||
self.assertTrue(req_data.get("ok"))
|
||||
|
||||
# 2. Check shows KEY_APPROVAL
|
||||
r_check = subprocess.run([
|
||||
sys.executable, str(BIN_DIR / "super-cli.py"),
|
||||
r_check = _super_cli_inproc(
|
||||
"approvals", "check", "--node", "dev", "--json"
|
||||
], capture_output=True, text=True)
|
||||
)
|
||||
self.assertEqual(r_check.returncode, 0)
|
||||
check_data = json.loads(r_check.stdout)
|
||||
dev_app = next(a for a in check_data["approvals"] if a["node"] == "dev")
|
||||
self.assertEqual(dev_app["status"], "KEY_APPROVAL")
|
||||
|
||||
# 3. Deny key
|
||||
r_deny = subprocess.run([
|
||||
sys.executable, str(BIN_DIR / "super-cli.py"),
|
||||
r_deny = _super_cli_inproc(
|
||||
"approvals", "deny", "dev", "--json"
|
||||
], capture_output=True, text=True)
|
||||
)
|
||||
self.assertEqual(r_deny.returncode, 0)
|
||||
deny_data = json.loads(r_deny.stdout)
|
||||
self.assertTrue(deny_data.get("ok"))
|
||||
|
||||
@@ -31,6 +31,7 @@ def _load(name, relpath):
|
||||
|
||||
exec_constrained = _load("exec_constrained_approvals",
|
||||
"bin/exec-constrained.py")
|
||||
box_ctl = _load("box_ctl_approvalstest", "bin/box-ctl.py")
|
||||
|
||||
|
||||
def _box_ctl(*args):
|
||||
@@ -39,6 +40,34 @@ def _box_ctl(*args):
|
||||
capture_output=True, text=True, timeout=180)
|
||||
|
||||
|
||||
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 (same proven pattern as test_box_loop_https).
|
||||
|
||||
Calls the real main(argv) -- identical argv parsing, dispatch, audit,
|
||||
and stdout JSON -- amortizing per-spawn interpreter cost over one
|
||||
import. Node order in fleet scans is nondeterministic either way
|
||||
(concurrent fan-out); per-node content is identical.
|
||||
"""
|
||||
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())
|
||||
|
||||
|
||||
class ExecApprovalOpsTests(unittest.TestCase):
|
||||
def test_ops_registered_and_side_effecting(self):
|
||||
spec = exec_constrained.OPS
|
||||
@@ -155,14 +184,14 @@ class BoxCtlApprovalTests(unittest.TestCase):
|
||||
["approval-allow", "badnode", "--message", "m"],
|
||||
["approval-deny", "badnode", "--message", "m"],
|
||||
["approval-auto", "badnode"]):
|
||||
r = _box_ctl(*args)
|
||||
r = _box_ctl_inproc(*args)
|
||||
self.assertNotEqual(r.returncode, 0, args)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NODE",
|
||||
args)
|
||||
|
||||
def test_rejects_missing_node(self):
|
||||
for args in (["approval-allow"], ["approval-deny"]):
|
||||
r = _box_ctl(*args)
|
||||
r = _box_ctl_inproc(*args)
|
||||
self.assertNotEqual(r.returncode, 0, args)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS",
|
||||
args)
|
||||
@@ -194,7 +223,7 @@ class BoxCtlApprovalTests(unittest.TestCase):
|
||||
(["approval-auto", "a", "b"], False),
|
||||
]
|
||||
for args, valid in cases:
|
||||
r = _box_ctl("quality-validate", *args)
|
||||
r = _box_ctl_inproc("quality-validate", *args)
|
||||
self.assertEqual(json.loads(r.stdout)["valid"], valid, args)
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ def _load(name, relpath):
|
||||
|
||||
|
||||
exec_constrained = _load("exec_constrained_dev", "bin/exec-constrained.py")
|
||||
box_ctl = _load("box_ctl_devtest", "bin/box-ctl.py")
|
||||
|
||||
|
||||
def _box_ctl(*args):
|
||||
@@ -38,6 +39,35 @@ def _box_ctl(*args):
|
||||
capture_output=True, text=True, timeout=120)
|
||||
|
||||
|
||||
class _InProcResult:
|
||||
"""Minimal CompletedProcess stand-in (returncode/stdout/stderr)."""
|
||||
def __init__(self, returncode, stdout, stderr=""):
|
||||
self.returncode = returncode
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
|
||||
|
||||
def _box_ctl_inproc(*args):
|
||||
"""In-process _box_ctl for pure dry-run verbs (quality-validate).
|
||||
|
||||
Calls the real main(argv) -- identical argv parsing, dispatch, audit,
|
||||
and stdout JSON -- with stdio captured, amortizing the ~80ms
|
||||
per-spawn interpreter + module-exec cost over one import. Only valid
|
||||
for verbs that never read stdin (quality-validate is a dry-run that
|
||||
never consumes stdin payloads).
|
||||
"""
|
||||
import io
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
returncode = 0
|
||||
with redirect_stdout(out), redirect_stderr(err):
|
||||
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, out.getvalue(), err.getvalue())
|
||||
|
||||
|
||||
class ExecGitOpsTests(unittest.TestCase):
|
||||
def test_ops_registered_and_read_only(self):
|
||||
for op in ("git.status", "git.diff", "git.log"):
|
||||
@@ -223,12 +253,14 @@ class BoxCtlGitTests(unittest.TestCase):
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
|
||||
def test_quality_validate_git_verbs(self):
|
||||
# In-process dry-runs: same main(argv) path and stdout JSON as
|
||||
# subprocess calls. Assertions below are unchanged.
|
||||
for args in (["git-status"], ["git-diff", "--stat"],
|
||||
["git-diff", "--path", "bin/dm.py"],
|
||||
["git-log", "--limit", "5"]):
|
||||
r = _box_ctl("quality-validate", *args)
|
||||
r = _box_ctl_inproc("quality-validate", *args)
|
||||
self.assertTrue(json.loads(r.stdout)["valid"], args)
|
||||
r = _box_ctl("quality-validate", "git-diff", "--path", "../x")
|
||||
r = _box_ctl_inproc("quality-validate", "git-diff", "--path", "../x")
|
||||
self.assertFalse(json.loads(r.stdout)["valid"])
|
||||
|
||||
|
||||
@@ -274,15 +306,17 @@ class BoxCtlTestsRunTests(unittest.TestCase):
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND")
|
||||
|
||||
def test_quality_validate_tests_run(self):
|
||||
r = _box_ctl("quality-validate", "tests-run")
|
||||
# In-process dry-runs: same main(argv) path and stdout JSON as
|
||||
# subprocess calls. Assertions below are unchanged.
|
||||
r = _box_ctl_inproc("quality-validate", "tests-run")
|
||||
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "tests-run", "tests.test_box_read_https")
|
||||
r = _box_ctl_inproc("quality-validate", "tests-run", "tests.test_box_read_https")
|
||||
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "tests-run", "--filter", "safepath")
|
||||
r = _box_ctl_inproc("quality-validate", "tests-run", "--filter", "safepath")
|
||||
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "tests-run", "os")
|
||||
r = _box_ctl_inproc("quality-validate", "tests-run", "os")
|
||||
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "tests-run", "--filter")
|
||||
r = _box_ctl_inproc("quality-validate", "tests-run", "--filter")
|
||||
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
|
||||
|
||||
|
||||
@@ -303,10 +337,12 @@ class BoxCtlAckTests(unittest.TestCase):
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS")
|
||||
|
||||
def test_quality_validate_ack(self):
|
||||
r = _box_ctl("quality-validate", "ack", "bdf7beb6",
|
||||
# In-process dry-runs: same main(argv) path and stdout JSON as
|
||||
# subprocess calls. Assertions below are unchanged.
|
||||
r = _box_ctl_inproc("quality-validate", "ack", "bdf7beb6",
|
||||
"--to", "pip", "--sender", "opm")
|
||||
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "ack", "xyz!",
|
||||
r = _box_ctl_inproc("quality-validate", "ack", "xyz!",
|
||||
"--to", "pip", "--sender", "opm")
|
||||
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ def _load(name, relpath):
|
||||
|
||||
|
||||
exec_constrained = _load("exec_constrained_jobs", "bin/exec-constrained.py")
|
||||
box_ctl = _load("box_ctl_jobstest", "bin/box-ctl.py")
|
||||
|
||||
|
||||
def _box_ctl(*args, stdin=None):
|
||||
@@ -45,6 +46,37 @@ def _box_ctl(*args, stdin=None):
|
||||
input=stdin, capture_output=True, text=True, timeout=120)
|
||||
|
||||
|
||||
class _InProcResult:
|
||||
"""Minimal CompletedProcess stand-in (returncode/stdout/stderr)."""
|
||||
|
||||
def __init__(self, returncode, stdout):
|
||||
self.returncode = returncode
|
||||
self.stdout = stdout
|
||||
self.stderr = ""
|
||||
|
||||
|
||||
def _box_ctl_inproc(*args, stdin=None):
|
||||
"""In-process _box_ctl (proven pattern from test_box_loop_https).
|
||||
|
||||
Real main(argv) with stdout captured and sys.stdin patched when a
|
||||
body is given (job-put reads the raw definition from stdin).
|
||||
"""
|
||||
import io
|
||||
from contextlib import redirect_stdout, nullcontext
|
||||
from unittest import mock
|
||||
buf = io.StringIO()
|
||||
returncode = 0
|
||||
stdin_ctx = (mock.patch.object(sys, "stdin", io.StringIO(stdin))
|
||||
if stdin is not None else nullcontext())
|
||||
with stdin_ctx:
|
||||
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())
|
||||
|
||||
|
||||
def _job_def(name, **over):
|
||||
d = {"name": name, "description": "unit test job",
|
||||
"schedule": "manual", "agent": "opm",
|
||||
@@ -166,7 +198,7 @@ class ExecJobOpsTests(unittest.TestCase):
|
||||
|
||||
class BoxCtlJobsTests(unittest.TestCase):
|
||||
def test_job_next_dry_run_live(self):
|
||||
r = _box_ctl("job-next", MISSING_ID)
|
||||
r = _box_ctl_inproc("job-next", MISSING_ID)
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data["ok"])
|
||||
@@ -174,55 +206,55 @@ class BoxCtlJobsTests(unittest.TestCase):
|
||||
self.assertFalse(data["would_dispatch"])
|
||||
|
||||
def test_job_put_rejects_before_write(self):
|
||||
r = _box_ctl("job-put", "Bad_Name!", stdin="{}")
|
||||
r = _box_ctl_inproc("job-put", "Bad_Name!", stdin="{}")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
|
||||
r = _box_ctl("job-put", "my-job", stdin="not json")
|
||||
r = _box_ctl_inproc("job-put", "my-job", stdin="not json")
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "INVALID_JOB")
|
||||
r = _box_ctl("job-put", "my-job",
|
||||
stdin=json.dumps(_job_def("other")))
|
||||
r = _box_ctl_inproc("job-put", "my-job",
|
||||
stdin=json.dumps(_job_def("other")))
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "NAME_MISMATCH")
|
||||
bad = _job_def("my-job")
|
||||
del bad["agent"]
|
||||
r = _box_ctl("job-put", "my-job", stdin=json.dumps(bad))
|
||||
r = _box_ctl_inproc("job-put", "my-job", stdin=json.dumps(bad))
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "INVALID_JOB")
|
||||
|
||||
def test_job_trigger_rejects_missing(self):
|
||||
r = _box_ctl("job-trigger", "Bad_Name!")
|
||||
r = _box_ctl_inproc("job-trigger", "Bad_Name!")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
|
||||
r = _box_ctl("job-trigger", MISSING_JOB)
|
||||
r = _box_ctl_inproc("job-trigger", MISSING_JOB)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND")
|
||||
|
||||
def test_job_chain_rejects_before_write(self):
|
||||
r = _box_ctl("job-chain", "Bad_Name!", EXISTING_JOB)
|
||||
r = _box_ctl_inproc("job-chain", "Bad_Name!", EXISTING_JOB)
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
|
||||
r = _box_ctl("job-chain", EXISTING_JOB, EXISTING_JOB)
|
||||
r = _box_ctl_inproc("job-chain", EXISTING_JOB, EXISTING_JOB)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "INVALID_JOB")
|
||||
r = _box_ctl("job-chain", MISSING_JOB, EXISTING_JOB)
|
||||
r = _box_ctl_inproc("job-chain", MISSING_JOB, EXISTING_JOB)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND")
|
||||
|
||||
def test_timer_control_rejects_before_action(self):
|
||||
for verb in ("timer-stop", "timer-disable"):
|
||||
r = _box_ctl(verb, "Bad_Name!")
|
||||
r = _box_ctl_inproc(verb, "Bad_Name!")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
|
||||
r = _box_ctl(verb, MISSING_JOB)
|
||||
r = _box_ctl_inproc(verb, MISSING_JOB)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND")
|
||||
|
||||
def test_quality_validate_job_verbs(self):
|
||||
r = _box_ctl("quality-validate", "job-put", "my-job")
|
||||
r = _box_ctl_inproc("quality-validate", "job-put", "my-job")
|
||||
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "job-trigger", EXISTING_JOB)
|
||||
r = _box_ctl_inproc("quality-validate", "job-trigger", EXISTING_JOB)
|
||||
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "job-chain", "a", "b")
|
||||
r = _box_ctl_inproc("quality-validate", "job-chain", "a", "b")
|
||||
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "job-next", MISSING_ID)
|
||||
r = _box_ctl_inproc("quality-validate", "job-next", MISSING_ID)
|
||||
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "timer-stop", EXISTING_JOB)
|
||||
r = _box_ctl_inproc("quality-validate", "timer-stop", EXISTING_JOB)
|
||||
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "job-put", "Bad_Name!")
|
||||
r = _box_ctl_inproc("quality-validate", "job-put", "Bad_Name!")
|
||||
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ def _load(name, relpath):
|
||||
|
||||
|
||||
exec_constrained = _load("exec_constrained_loop", "bin/exec-constrained.py")
|
||||
box_ctl = _load("box_ctl_looptest", "bin/box-ctl.py")
|
||||
|
||||
|
||||
def _box_ctl(*args):
|
||||
@@ -46,6 +47,34 @@ def _box_ctl(*args):
|
||||
capture_output=True, text=True, timeout=180)
|
||||
|
||||
|
||||
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 for pure dry-run verbs (quality-validate).
|
||||
|
||||
Calls the real main(argv) -- identical argv parsing, dispatch, audit,
|
||||
and stdout JSON -- with stdout captured, amortizing the ~80ms
|
||||
per-spawn interpreter + module-exec cost over one import. Only valid
|
||||
for verbs that never read stdin (quality-validate is a dry-run that
|
||||
never consumes stdin payloads).
|
||||
"""
|
||||
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())
|
||||
|
||||
|
||||
class ExecLoopOpsTests(unittest.TestCase):
|
||||
def test_ops_registered_and_side_effecting(self):
|
||||
spec = exec_constrained.OPS
|
||||
@@ -208,7 +237,10 @@ class BoxCtlLoopTests(unittest.TestCase):
|
||||
(["vars-get", "has space"], False),
|
||||
]
|
||||
for args, valid in cases:
|
||||
r = _box_ctl("quality-validate", *args)
|
||||
# In-process dry-run: same main(argv) path and stdout JSON as a
|
||||
# subprocess call, without the per-case spawn cost. Assertions
|
||||
# below are unchanged.
|
||||
r = _box_ctl_inproc("quality-validate", *args)
|
||||
self.assertEqual(json.loads(r.stdout)["valid"], valid, args)
|
||||
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ def _load(name, relpath):
|
||||
|
||||
exec_constrained = _load("exec_constrained_md", "bin/exec-constrained.py")
|
||||
agent_md = _load("agent_md_mdtest", "bin/agent_md.py")
|
||||
box_ctl = _load("box_ctl_mdtest", "bin/box-ctl.py")
|
||||
|
||||
|
||||
def _box_ctl(*args, stdin=None):
|
||||
@@ -50,6 +51,39 @@ def _box_ctl(*args, stdin=None):
|
||||
input=stdin, capture_output=True, text=True, timeout=180)
|
||||
|
||||
|
||||
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, stdin=None):
|
||||
"""In-process _box_ctl for dry-run and validation-failure verbs.
|
||||
|
||||
Calls the real main(argv) -- identical argv parsing, dispatch, audit,
|
||||
stdin reads, and stdout JSON -- with stdio captured, amortizing the
|
||||
~80ms per-spawn interpreter + module-exec cost over one import.
|
||||
stdin, when given, is fed exactly as subprocess input= would be.
|
||||
"""
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
buf = io.StringIO()
|
||||
returncode = 0
|
||||
saved_stdin = sys.stdin
|
||||
if stdin is not None:
|
||||
sys.stdin = io.StringIO(stdin)
|
||||
try:
|
||||
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
|
||||
finally:
|
||||
sys.stdin = saved_stdin
|
||||
return _InProcResult(returncode, buf.getvalue())
|
||||
|
||||
|
||||
class ExecMdOpsTests(unittest.TestCase):
|
||||
def test_ops_registered_and_side_effecting(self):
|
||||
spec = exec_constrained.OPS
|
||||
@@ -320,8 +354,10 @@ class BoxCtlMdTests(unittest.TestCase):
|
||||
["md-audit", "../x"],
|
||||
["md", "read", "646", "../x"],
|
||||
]
|
||||
# In-process dispatch: same main(argv) path, fail() JSON, and
|
||||
# exit code as a subprocess call. Assertions below are unchanged.
|
||||
for args in cases:
|
||||
r = _box_ctl(*args)
|
||||
r = _box_ctl_inproc(*args)
|
||||
self.assertNotEqual(r.returncode, 0, args)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME", args)
|
||||
|
||||
@@ -333,13 +369,17 @@ class BoxCtlMdTests(unittest.TestCase):
|
||||
["md", "amend", "NOPE.md", "content here"],
|
||||
["md", "append", "NOPE.md", "note here"],
|
||||
]
|
||||
# In-process dispatch: same main(argv) path, stdin reads, fail()
|
||||
# JSON, and exit code as a subprocess call. Assertions below are
|
||||
# unchanged. (Amend/append parse --stdin BEFORE validating the
|
||||
# path, so stdin is fed here exactly as the spawn did.)
|
||||
for args in cases:
|
||||
r = _box_ctl(*args)
|
||||
r = _box_ctl_inproc(*args)
|
||||
self.assertNotEqual(r.returncode, 0, args)
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME", args)
|
||||
r = _box_ctl("md-amend", "../x", "--stdin", stdin="hi")
|
||||
r = _box_ctl_inproc("md-amend", "../x", "--stdin", stdin="hi")
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
|
||||
r = _box_ctl("md-append", "../x", "--stdin", stdin="hi")
|
||||
r = _box_ctl_inproc("md-append", "../x", "--stdin", stdin="hi")
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
|
||||
|
||||
def test_amend_stdin_safety_rejection_writes_nothing(self):
|
||||
@@ -386,7 +426,10 @@ class BoxCtlMdTests(unittest.TestCase):
|
||||
(["md-write", "646", "SOUL.md"], False),
|
||||
]
|
||||
for args, valid in cases:
|
||||
r = _box_ctl("quality-validate", *args)
|
||||
# In-process dry-run: same main(argv) path and stdout JSON as a
|
||||
# subprocess call, without the per-case spawn cost. Assertions
|
||||
# below are unchanged.
|
||||
r = _box_ctl_inproc("quality-validate", *args)
|
||||
self.assertEqual(json.loads(r.stdout)["valid"], valid, args)
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ def _load(name, relpath):
|
||||
|
||||
exec_constrained = _load("exec_constrained_read", "bin/exec-constrained.py")
|
||||
super_cli = _load("super_cli_read", "bin/super-cli.py")
|
||||
box_ctl = _load("box_ctl_readtest", "bin/box-ctl.py")
|
||||
|
||||
|
||||
def _box_ctl(*args):
|
||||
@@ -41,6 +42,33 @@ def _box_ctl(*args):
|
||||
capture_output=True, text=True, timeout=60)
|
||||
|
||||
|
||||
class _InProcResult:
|
||||
"""Minimal CompletedProcess stand-in (returncode/stdout/stderr)."""
|
||||
def __init__(self, returncode, stdout, stderr=""):
|
||||
self.returncode = returncode
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
|
||||
|
||||
def _box_ctl_inproc(*args):
|
||||
"""In-process _box_ctl for dry-run/read-only verbs (quality-validate,
|
||||
dm-log success paths).
|
||||
|
||||
Calls the real main(argv) -- identical argv parsing, dispatch, audit,
|
||||
and stdout JSON -- with stdio captured, amortizing the ~80ms
|
||||
per-spawn interpreter + module-exec cost over one import.
|
||||
"""
|
||||
from contextlib import redirect_stderr
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
returncode = 0
|
||||
with redirect_stdout(out), redirect_stderr(err):
|
||||
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, out.getvalue(), err.getvalue())
|
||||
|
||||
|
||||
class ExecReadOpsTests(unittest.TestCase):
|
||||
def test_ops_registered_and_read_only(self):
|
||||
self.assertIn("fleet.unread", exec_constrained.OPS)
|
||||
@@ -97,8 +125,10 @@ class ExecReadOpsTests(unittest.TestCase):
|
||||
spec = exec_constrained.OPS["dm.log"]
|
||||
clean = spec["validate"]({"limit": 2})
|
||||
argv = spec["build"](clean)
|
||||
argv[0] = sys.executable # hermetic interpreter, same script + args
|
||||
r = subprocess.run(argv, capture_output=True, text=True, timeout=60)
|
||||
# In-process dispatch of the op-built argv (minus interpreter and
|
||||
# script: argv is [python, box-ctl.py, action, ...]): same argv
|
||||
# parsing, dispatch, and stdout JSON, without respawn.
|
||||
r = _box_ctl_inproc(*argv[2:])
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data["ok"])
|
||||
@@ -131,23 +161,55 @@ class BoxCtlReadVerbsTests(unittest.TestCase):
|
||||
self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS")
|
||||
|
||||
def test_dm_log_back_compat_limit_only(self):
|
||||
r = _box_ctl("dm-log", "2")
|
||||
r = _box_ctl_inproc("dm-log", "2")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data["ok"])
|
||||
self.assertEqual(len(data["entries"]), 2)
|
||||
|
||||
def test_quality_validate_new_verbs(self):
|
||||
r = _box_ctl("quality-validate", "unread", "--agent", "pip")
|
||||
# In-process dry-runs: same main(argv) path and stdout JSON as
|
||||
# subprocess calls. Assertions below are unchanged.
|
||||
r = _box_ctl_inproc("quality-validate", "unread", "--agent", "pip")
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "dm-log", "5", "--agent", "opm")
|
||||
r = _box_ctl_inproc("quality-validate", "dm-log", "5", "--agent", "opm")
|
||||
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "unread", "--agent", "nope")
|
||||
r = _box_ctl_inproc("quality-validate", "unread", "--agent", "nope")
|
||||
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
|
||||
r = _box_ctl("quality-validate", "unread", "extra-positional")
|
||||
r = _box_ctl_inproc("quality-validate", "unread", "extra-positional")
|
||||
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
|
||||
|
||||
def test_policy_verbs_live_schema(self):
|
||||
r = _box_ctl_inproc("policy")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data["ok"])
|
||||
self.assertIn("agents", data)
|
||||
self.assertIn("totals", data)
|
||||
self.assertIn(data["status"], ("clean", "violations found"))
|
||||
r = _box_ctl_inproc("policy", "check", "opm")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
data = json.loads(r.stdout)
|
||||
self.assertTrue(data["ok"])
|
||||
self.assertEqual(data["agent"], "opm")
|
||||
for k in ("blocked", "authorized_main", "violations", "total_sends"):
|
||||
self.assertIn(k, data)
|
||||
|
||||
def test_policy_scan_parses_each_line_once(self):
|
||||
import shutil
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
frozen = Path(td) / "dm-log.jsonl"
|
||||
shutil.copyfile(box_ctl.DM_LOG, frozen)
|
||||
expect = sum(1 for ln in frozen.read_text().splitlines() if ln.strip())
|
||||
real_loads = json.loads
|
||||
with mock.patch.object(box_ctl, "DM_LOG", frozen):
|
||||
with mock.patch.object(json, "loads", wraps=real_loads) as spy:
|
||||
per_agent, meta = box_ctl._policy_scan()
|
||||
self.assertIsNotNone(per_agent)
|
||||
self.assertEqual(spy.call_count, expect)
|
||||
|
||||
|
||||
class SuperCliUnreadTests(unittest.TestCase):
|
||||
def test_lookup_dispatches_unread(self):
|
||||
|
||||
@@ -6,6 +6,7 @@ muse argv approval-posture parsing, runtime_rows assembly (mocked tmux),
|
||||
and the `box runtime` CLI surface.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
@@ -18,6 +19,16 @@ sys.path.insert(0, str(BIN_DIR))
|
||||
|
||||
import muse_choice_watcher as w
|
||||
|
||||
|
||||
def _load(name, relpath):
|
||||
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
|
||||
|
||||
|
||||
super_cli = _load("super_cli_runtimetest", "bin/super-cli.py")
|
||||
|
||||
PROMPT = "❯" # Muse TUI input glyph (U+276F)
|
||||
|
||||
STATE_OPEN = (
|
||||
@@ -375,9 +386,25 @@ class TestBoxRuntimeCLI(unittest.TestCase):
|
||||
self.assertTrue(json.loads(r.stdout)["ok"])
|
||||
|
||||
def test_subcommand_help(self):
|
||||
# In-process --help: same main()/argparse path as a subprocess call
|
||||
# (fresh parser per call; --help exits 0), without the ~0.2s
|
||||
# per-spawn interpreter + module-exec cost. Assertions unchanged.
|
||||
import io
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
for sub in ("list", "send", "launch", "layout", "spread"):
|
||||
r = self._box(sub, "--help")
|
||||
self.assertEqual(r.returncode, 0, sub)
|
||||
out, err = io.StringIO(), io.StringIO()
|
||||
saved = sys.argv
|
||||
sys.argv = [str(BIN_DIR / "super-cli.py"),
|
||||
"runtime", sub, "--help"]
|
||||
rc = 0
|
||||
with redirect_stdout(out), redirect_stderr(err):
|
||||
try:
|
||||
super_cli.main()
|
||||
except SystemExit as e:
|
||||
rc = e.code if isinstance(e.code, int) else 1
|
||||
finally:
|
||||
sys.argv = saved
|
||||
self.assertEqual(rc, 0, sub)
|
||||
|
||||
def test_launch_dry_run_injects_approve(self):
|
||||
r = self._box("launch", "--session", "probe-x",
|
||||
|
||||
@@ -17,6 +17,23 @@ from invite_handler import InviteCodeInfo, InviteHandler, RedemptionResult, salv
|
||||
from settings_rpa import NodeUsage, SettingsRPA
|
||||
|
||||
|
||||
def _fast_clock():
|
||||
"""Fake time.time advancing 1s per call.
|
||||
|
||||
redeem_code_dom polls on `deadline = time.time() + 2.5` loops; patching
|
||||
only time.sleep leaves 2.5s of real time per loop. With +1s/call each
|
||||
loop runs exactly 2 iterations then expires (deadline math holds for
|
||||
every loop uniformly, no per-loop alignment needed).
|
||||
"""
|
||||
state = {"t": 1000.0}
|
||||
|
||||
def fake_time():
|
||||
state["t"] += 1.0
|
||||
return state["t"]
|
||||
|
||||
return fake_time
|
||||
|
||||
|
||||
class TestInviteCodeValidation(unittest.TestCase):
|
||||
def test_normalize_valid_codes(self):
|
||||
self.assertEqual(invite.normalize_code("REDCJ7"), "REDCJ7")
|
||||
@@ -212,7 +229,7 @@ class TestInviteHandlerMocked(unittest.TestCase):
|
||||
|
||||
h = InviteHandler("dev")
|
||||
h.ws = MagicMock()
|
||||
with patch.object(h, "connect"), patch("time.sleep", return_value=None):
|
||||
with patch.object(h, "connect"), patch("time.sleep", return_value=None), patch("time.time", side_effect=_fast_clock()):
|
||||
with patch("invite_handler.cdp_evaluate", return_value={"found": False, "text": "General"}):
|
||||
with patch.object(h, "redeem_code_api") as mock_api:
|
||||
mock_api.return_value = RedemptionResult(
|
||||
@@ -239,7 +256,7 @@ class TestInviteHandlerMocked(unittest.TestCase):
|
||||
|
||||
h = InviteHandler("646")
|
||||
h.ws = MagicMock()
|
||||
with patch.object(h, "connect"), patch("time.sleep", return_value=None):
|
||||
with patch.object(h, "connect"), patch("time.sleep", return_value=None), patch("time.time", side_effect=_fast_clock()):
|
||||
with patch("invite_handler.cdp_evaluate", return_value={"found": False, "has_additional": True, "text": "Additional tokens"}):
|
||||
with patch.object(h, "redeem_code_api") as mock_api:
|
||||
mock_api.return_value = RedemptionResult(
|
||||
@@ -274,7 +291,7 @@ class TestInviteHandlerMocked(unittest.TestCase):
|
||||
return {"found_input": False}
|
||||
return True
|
||||
|
||||
with patch.object(h, "connect"), patch("time.sleep", return_value=None):
|
||||
with patch.object(h, "connect"), patch("time.sleep", return_value=None), patch("time.time", side_effect=_fast_clock()):
|
||||
with patch("invite_handler.cdp_evaluate", side_effect=mock_eval), patch("invite_handler.cdp_send_escape"):
|
||||
with patch.object(h, "redeem_code_api") as mock_api:
|
||||
mock_api.return_value = RedemptionResult(
|
||||
|
||||
@@ -18,6 +18,7 @@ import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
REPO_ROOT = Path("/home/super/Projects/NetVM")
|
||||
BIN_DIR = REPO_ROOT / "bin"
|
||||
@@ -27,6 +28,39 @@ sys.path.insert(0, str(BIN_DIR))
|
||||
import gravity
|
||||
|
||||
|
||||
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())
|
||||
|
||||
|
||||
class TestLoopDiagnosticsAndRemediation(unittest.TestCase):
|
||||
"""Test gravity.py loop health and progressive remediation."""
|
||||
|
||||
@@ -61,13 +95,27 @@ class TestLoopDiagnosticsAndRemediation(unittest.TestCase):
|
||||
self.assertIsInstance(res.get("remediated"), list)
|
||||
self.assertIsInstance(res.get("escalated"), list)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
res = _box_ctl_inproc("loop-health")
|
||||
self.assertEqual(res.returncode, 0)
|
||||
data = json.loads(res.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
@@ -76,16 +124,14 @@ class TestLoopRpc(unittest.TestCase):
|
||||
|
||||
|
||||
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)
|
||||
res = _box_ctl_inproc("loop-status", "--limit", "5")
|
||||
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)
|
||||
res = _box_ctl_inproc("loop-remediate", "--dry-run")
|
||||
self.assertEqual(res.returncode, 0)
|
||||
data = json.loads(res.stdout)
|
||||
self.assertTrue(data.get("ok"))
|
||||
|
||||
@@ -68,8 +68,11 @@ class TestSettingsRPAPrimitives(unittest.TestCase):
|
||||
usage_payload, # read_usage evaluation
|
||||
]
|
||||
|
||||
with SettingsRPA("646") as rpa:
|
||||
usage = rpa.read_usage(keep_dialog_open=True)
|
||||
# Settle sleeps (0.4s tab + 0.4s usage) are production pacing, not
|
||||
# asserted behavior: skip them like the mocked CDP transport above.
|
||||
with patch("time.sleep", return_value=None):
|
||||
with SettingsRPA("646") as rpa:
|
||||
usage = rpa.read_usage(keep_dialog_open=True)
|
||||
self.assertEqual(usage.node, "646")
|
||||
self.assertEqual(usage.weekly_percent_used, 100)
|
||||
self.assertEqual(usage.extra_percent_used, 100)
|
||||
@@ -97,8 +100,10 @@ class TestSettingsRPAPrimitives(unittest.TestCase):
|
||||
usage_payload, # read_usage evaluation
|
||||
]
|
||||
|
||||
with SettingsRPA("646") as rpa:
|
||||
usage = rpa.read_usage(keep_dialog_open=True)
|
||||
# Settle sleeps are production pacing, not asserted behavior: skip.
|
||||
with patch("time.sleep", return_value=None):
|
||||
with SettingsRPA("646") as rpa:
|
||||
usage = rpa.read_usage(keep_dialog_open=True)
|
||||
self.assertEqual(usage.node, "646")
|
||||
self.assertEqual(usage.weekly_percent_used, 20)
|
||||
self.assertEqual(usage.extra_percent_used, 0)
|
||||
@@ -118,9 +123,19 @@ class TestSettingsRPAPrimitives(unittest.TestCase):
|
||||
|
||||
mock_eval.side_effect = eval_side_effect
|
||||
|
||||
# The usage poll loop (read_usage) busy-spins on a real-time 4.0s
|
||||
# deadline while sleep is stubbed, so run it on a fake clock that
|
||||
# advances 1s per read: the loop still polls (each poll returns None)
|
||||
# and still exits via timeout, just after ~4 reads instead of 4s.
|
||||
clock = [1000.0]
|
||||
|
||||
def _tick():
|
||||
clock[0] += 1.0
|
||||
return clock[0]
|
||||
|
||||
with SettingsRPA("646", timeout=0.5) as rpa:
|
||||
# Shorten deadline by patching time.time or passing small timeout
|
||||
with patch("time.sleep", return_value=None):
|
||||
with patch("time.sleep", return_value=None), \
|
||||
patch("time.time", side_effect=_tick):
|
||||
usage = rpa.read_usage(keep_dialog_open=True)
|
||||
self.assertEqual(usage.node, "646")
|
||||
self.assertFalse(usage.stats_loaded)
|
||||
|
||||
@@ -245,6 +245,23 @@ class CanonicalToolPattern(unittest.TestCase):
|
||||
|
||||
|
||||
class EnvelopeRoundTrip(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Stub the kpi module: wrap() calls get_live_advisory_block()
|
||||
# (live network I/O: usage API + route probes) on the include_kpi
|
||||
# path. An empty advisory keeps the path exercised -- import, call,
|
||||
# and falsy branch all still run -- without the network wait.
|
||||
import types
|
||||
self._saved_kpi = sys.modules.get("kpi")
|
||||
stub = types.ModuleType("kpi")
|
||||
stub.get_live_advisory_block = lambda agent: ""
|
||||
sys.modules["kpi"] = stub
|
||||
|
||||
def tearDown(self):
|
||||
if self._saved_kpi is None:
|
||||
sys.modules.pop("kpi", None)
|
||||
else:
|
||||
sys.modules["kpi"] = self._saved_kpi
|
||||
|
||||
def test_wrap_advertises_new_verbs(self):
|
||||
body = env.wrap("work-finder", "work-finder-1", "646",
|
||||
"646 tasks", "Do the thing.")
|
||||
|
||||
Reference in New Issue
Block a user