feat(supervision): add choice watcher daemon, HTTPS spec docs, and test suites
- bin/muse_choice_watcher.py + systemd/muse-choices-reconcile.*: automatic choice answering and timer reconciliation - bin/digest.py: fleet log and health summarization - docs/BOX-*-HTTPS.md: comprehensive HTTPS execution contracts and API documentation - docs/MUSE-CHOICES-POLICY.md & docs/SUPERVISION-SPEC.md: autonomous execution specs - tests/test_*.py: unit test suites for HTTPS API, choice watcher, fleet heal, and swarm pruning
This commit is contained in:
+108
-2
@@ -12,8 +12,10 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path("/home/super/Projects/NetVM")
|
||||
BIN_DIR = REPO_ROOT / "bin"
|
||||
@@ -138,8 +140,12 @@ class TestMuseChatApiConnection(unittest.TestCase):
|
||||
def test_muse_chat_api_approvals_command(self):
|
||||
cmd = [sys.executable, str(BIN_DIR / "muse-chat-api.py"), "--account", "pip", "approvals"]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0)
|
||||
self.assertIn("No pending approvals", r.stdout)
|
||||
self.assertIn(r.returncode, (0, 2))
|
||||
if r.returncode == 0:
|
||||
self.assertIn("No pending approvals", r.stdout)
|
||||
else:
|
||||
self.assertIn("APPROVAL_NEEDED", r.stdout)
|
||||
|
||||
|
||||
|
||||
class TestApprovalsReplySafety(unittest.TestCase):
|
||||
@@ -242,6 +248,106 @@ class TestKeyApprovalsAndPasskey(unittest.TestCase):
|
||||
self.assertEqual(deny_data.get("decision"), "deny")
|
||||
|
||||
|
||||
class _FakeWS:
|
||||
"""Scripted stand-in for a CDP websocket (no network)."""
|
||||
|
||||
def __init__(self, recvs):
|
||||
self._recvs = list(recvs)
|
||||
self.sent_ids = []
|
||||
|
||||
def send(self, msg):
|
||||
self.sent_ids.append(json.loads(msg)["id"])
|
||||
|
||||
def recv(self):
|
||||
if not self._recvs:
|
||||
raise Exception("recv queue exhausted")
|
||||
item = self._recvs.pop(0)
|
||||
if callable(item):
|
||||
return item(self)
|
||||
return item
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
def _echo_last_value(value):
|
||||
def _recv(ws):
|
||||
return json.dumps({"id": ws.sent_ids[-1],
|
||||
"result": {"result": {"value": value}}})
|
||||
return _recv
|
||||
|
||||
|
||||
class TestInspectRobustness(unittest.TestCase):
|
||||
"""Regression tests for intermittent approval failures."""
|
||||
|
||||
def test_cdp_request_ids_unique(self):
|
||||
# Millisecond-clock ids collide for rapid successive evaluates;
|
||||
# a stale buffered response can then be misattributed to the
|
||||
# wrong call (e.g. verify-after-click reads the click result).
|
||||
# Frozen clock makes the old collision deterministic.
|
||||
with mock.patch("approvals.time.time", return_value=1728000000.123):
|
||||
ws = _FakeWS([_echo_last_value("a"), _echo_last_value("b")])
|
||||
self.assertEqual(approvals.cdp_evaluate(ws, "1+1"), "a")
|
||||
self.assertEqual(approvals.cdp_evaluate(ws, "2+2"), "b")
|
||||
self.assertNotEqual(ws.sent_ids[0], ws.sent_ids[1])
|
||||
|
||||
def test_cdp_skips_stale_ids(self):
|
||||
stale = json.dumps({"id": 999999999,
|
||||
"result": {"result": {"value": "stale"}}})
|
||||
ws = _FakeWS([stale, _echo_last_value("fresh")])
|
||||
self.assertEqual(approvals.cdp_evaluate(ws, "1+1"), "fresh")
|
||||
|
||||
def test_unreachable_returns_full_shape(self):
|
||||
with mock.patch.object(approvals, "get_node_pages",
|
||||
side_effect=ConnectionError("nope")), \
|
||||
mock.patch.object(approvals, "check_node_key_request",
|
||||
return_value=None):
|
||||
res = approvals.inspect_node_approvals("pip")
|
||||
self.assertEqual(res["status"], "UNREACHABLE")
|
||||
self.assertFalse(res["has_pending"])
|
||||
for key in ("node", "title", "buttons", "is_trusted",
|
||||
"input_waits", "error"):
|
||||
self.assertIn(key, res)
|
||||
|
||||
def test_all_pages_failed_reports_error(self):
|
||||
pages = [{"title": "t", "url": "u", "type": "page",
|
||||
"webSocketDebuggerUrl": "ws://127.0.0.1:9/none"}]
|
||||
|
||||
class _DeadWSModule:
|
||||
@staticmethod
|
||||
def create_connection(*a, **k):
|
||||
raise ConnectionError("refused")
|
||||
|
||||
with mock.patch.object(approvals, "get_node_pages",
|
||||
return_value=pages), \
|
||||
mock.patch.object(approvals, "websocket", _DeadWSModule()), \
|
||||
mock.patch.object(approvals, "check_node_key_request",
|
||||
return_value=None):
|
||||
res = approvals.inspect_node_approvals("pip")
|
||||
# Per-page CDP failures must surface as ERROR, never as a
|
||||
# false CLEAR that hides pending approvals.
|
||||
self.assertEqual(res["status"], "ERROR")
|
||||
self.assertFalse(res["has_pending"])
|
||||
self.assertIn("error", res)
|
||||
|
||||
def test_state_saves_roundtrip_without_leftovers(self):
|
||||
# Guards the atomic-save refactor (tmp + replace): correct
|
||||
# content and no stray temp files left behind.
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
rp = Path(td) / "resp.json"
|
||||
with mock.patch.object(approvals, "RESPONDED_WAITS_FILE", rp):
|
||||
approvals.save_responded_waits({"pip": {"t": "x"}})
|
||||
self.assertEqual(json.loads(rp.read_text()),
|
||||
{"pip": {"t": "x"}})
|
||||
fp = Path(td) / "seen.json"
|
||||
with mock.patch.object(approvals, "FIRST_SEEN_WAITS_FILE", fp):
|
||||
approvals.save_first_seen_waits({"pip": {"t": "x"}})
|
||||
self.assertEqual(json.loads(fp.read_text()),
|
||||
{"pip": {"t": "x"}})
|
||||
self.assertEqual(sorted(p.name for p in Path(td).iterdir()),
|
||||
["resp.json", "seen.json"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user