265 lines
11 KiB
Python
265 lines
11 KiB
Python
"""Tests for read-only box lookups over HTTPS (no SSH).
|
|
|
|
Covers the agent-facing read path:
|
|
box-relay.sh (agent client) -> exec-constrained.py named ops
|
|
-> box-ctl.py backend verbs -> super-cli.py lookups.
|
|
|
|
Live-socket round-trips are intentionally NOT covered here (loopback TCP is
|
|
unavailable in some sandboxes); instead we assert the exact argv each op
|
|
builds and execute the fast argv directly.
|
|
"""
|
|
import argparse
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
from contextlib import redirect_stdout
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
BOX_CTL = REPO_ROOT / "bin" / "box-ctl.py"
|
|
RELAY = REPO_ROOT / "bin" / "box-relay.sh"
|
|
|
|
|
|
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
|
|
|
|
|
|
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):
|
|
return subprocess.run(
|
|
[sys.executable, str(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)
|
|
self.assertIn("dm.log", exec_constrained.OPS)
|
|
self.assertFalse(exec_constrained.OPS["fleet.unread"]["side_effecting"])
|
|
self.assertFalse(exec_constrained.OPS["dm.log"]["side_effecting"])
|
|
|
|
def test_default_perms_include_read_ops(self):
|
|
# Any valid fleet signer can read; canary stays ping-only.
|
|
self.assertTrue(exec_constrained.permitted("some-unknown-identity", "fleet.unread"))
|
|
self.assertTrue(exec_constrained.permitted("some-unknown-identity", "dm.log"))
|
|
self.assertFalse(exec_constrained.permitted("exec-canary", "fleet.unread"))
|
|
self.assertFalse(exec_constrained.permitted("exec-canary", "dm.log"))
|
|
|
|
def test_fleet_unread_validate(self):
|
|
v = exec_constrained.OPS["fleet.unread"]["validate"]
|
|
self.assertEqual(v({}), {"agent": None})
|
|
self.assertEqual(v({"agent": "pip"}), {"agent": "pip"})
|
|
with self.assertRaises(exec_constrained.OpError):
|
|
v({"agent": "nope"})
|
|
with self.assertRaises(exec_constrained.OpError):
|
|
v({"bogus": 1})
|
|
|
|
def test_dm_log_validate(self):
|
|
v = exec_constrained.OPS["dm.log"]["validate"]
|
|
self.assertEqual(v({}), {"limit": 20, "agent": None})
|
|
self.assertEqual(v({"limit": 5, "agent": "opm"}), {"limit": 5, "agent": "opm"})
|
|
with self.assertRaises(exec_constrained.OpError):
|
|
v({"limit": 0})
|
|
with self.assertRaises(exec_constrained.OpError):
|
|
v({"limit": 101})
|
|
with self.assertRaises(exec_constrained.OpError):
|
|
v({"agent": "nope"})
|
|
with self.assertRaises(exec_constrained.OpError):
|
|
v({"bogus": 1})
|
|
|
|
def test_build_argv_shapes(self):
|
|
unread = exec_constrained.OPS["fleet.unread"]
|
|
argv = unread["build"]({"agent": None})
|
|
self.assertEqual(argv[-1], "unread")
|
|
self.assertNotIn("--agent", argv)
|
|
argv = unread["build"]({"agent": "muse"})
|
|
self.assertEqual(argv[-3:], ["unread", "--agent", "muse"])
|
|
|
|
dmlog = exec_constrained.OPS["dm.log"]
|
|
argv = dmlog["build"]({"limit": 5, "agent": "opm"})
|
|
self.assertEqual(argv[-4:], ["dm-log", "5", "--agent", "opm"])
|
|
argv = dmlog["build"]({"limit": 20, "agent": None})
|
|
self.assertEqual(argv[-2:], ["dm-log", "20"])
|
|
# argv only, never a shell string.
|
|
self.assertIsInstance(argv, list)
|
|
|
|
def test_dm_log_built_argv_executes(self):
|
|
spec = exec_constrained.OPS["dm.log"]
|
|
clean = spec["validate"]({"limit": 2})
|
|
argv = spec["build"](clean)
|
|
# 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"])
|
|
self.assertEqual(len(data["entries"]), 2)
|
|
|
|
|
|
class BoxCtlReadVerbsTests(unittest.TestCase):
|
|
def test_unread_rejects_unknown_agent(self):
|
|
r = _box_ctl("unread", "--agent", "nope")
|
|
self.assertNotEqual(r.returncode, 0)
|
|
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NODE")
|
|
|
|
def test_unread_rejects_positional_and_missing_value(self):
|
|
r = _box_ctl("unread", "pip")
|
|
self.assertNotEqual(r.returncode, 0)
|
|
self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS")
|
|
r = _box_ctl("unread", "--agent")
|
|
self.assertNotEqual(r.returncode, 0)
|
|
self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS")
|
|
|
|
def test_dm_log_rejects_bad_limit_and_agent(self):
|
|
r = _box_ctl("dm-log", "abc")
|
|
self.assertNotEqual(r.returncode, 0)
|
|
self.assertEqual(json.loads(r.stdout)["code"], "BAD_LIMIT")
|
|
r = _box_ctl("dm-log", "5", "--agent", "nope")
|
|
self.assertNotEqual(r.returncode, 0)
|
|
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NODE")
|
|
r = _box_ctl("dm-log", "1", "2")
|
|
self.assertNotEqual(r.returncode, 0)
|
|
self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS")
|
|
|
|
def test_dm_log_back_compat_limit_only(self):
|
|
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):
|
|
# 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_inproc("quality-validate", "dm-log", "5", "--agent", "opm")
|
|
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
|
|
r = _box_ctl_inproc("quality-validate", "unread", "--agent", "nope")
|
|
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
|
|
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):
|
|
args = argparse.Namespace(target="unread", lookup_args=[], json=False)
|
|
with mock.patch.object(super_cli, "_lookup_unreads") as m:
|
|
super_cli.cmd_lookup(args)
|
|
m.assert_called_once_with(args)
|
|
|
|
def test_lookup_unreads_json_shape(self):
|
|
fleet = [
|
|
{"node": "muse", "title": "muse (2)", "url": "https://muse.ai/thread/abc123",
|
|
"approval_pending": False},
|
|
{"node": "pip", "title": "muse", "url": "https://muse.ai/",
|
|
"approval_pending": True},
|
|
]
|
|
args = argparse.Namespace(json=True)
|
|
buf = io.StringIO()
|
|
with mock.patch.object(super_cli, "collect_fleet_data", return_value=fleet):
|
|
with redirect_stdout(buf):
|
|
super_cli._lookup_unreads(args)
|
|
data = json.loads(buf.getvalue())
|
|
self.assertTrue(data["ok"])
|
|
by_node = {n["node"]: n for n in data["nodes"]}
|
|
self.assertEqual(by_node["muse"]["unread"], 2)
|
|
self.assertEqual(by_node["muse"]["thread"], "abc123")
|
|
self.assertFalse(by_node["muse"]["approval_pending"])
|
|
self.assertEqual(by_node["pip"]["unread"], 0)
|
|
self.assertTrue(by_node["pip"]["approval_pending"])
|
|
self.assertEqual(by_node["pip"]["thread"], "home")
|
|
|
|
|
|
class BoxRelayClientTests(unittest.TestCase):
|
|
def test_relay_syntax_valid(self):
|
|
r = subprocess.run(["bash", "-n", str(RELAY)],
|
|
capture_output=True, text=True, timeout=30)
|
|
self.assertEqual(r.returncode, 0, r.stderr)
|
|
|
|
def test_relay_help_lists_read_commands(self):
|
|
r = subprocess.run(["bash", str(RELAY), "help"],
|
|
capture_output=True, text=True, timeout=30)
|
|
self.assertEqual(r.returncode, 0, r.stderr)
|
|
self.assertIn("box unread", r.stdout)
|
|
self.assertIn("box dm log", r.stdout)
|
|
|
|
def test_relay_maps_read_commands_to_ops(self):
|
|
text = RELAY.read_text()
|
|
self.assertIn("fleet.unread", text)
|
|
self.assertIn('"dm.log"', text)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|