Files
box/tests/test_box_read_https.py
T

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()