feat(muse): harden choice watcher concurrency, add rules dictionary, resume pool, and session bind

This commit is contained in:
operator
2026-10-07 01:50:18 +00:00
parent 90f4ef661a
commit 094bd7d691
10 changed files with 2166 additions and 40 deletions
+196
View File
@@ -505,6 +505,83 @@ class TestPidfileClaim(unittest.TestCase):
self.assertTrue(w._claim_pidfile(path))
class TestAnsweredPersistence(unittest.TestCase):
"""Answered sigs survive restarts: a daemon that restarts while an
answered prompt is still visible must not answer it again (live
double-answer -> stray "1" in the input box)."""
def test_restart_suppresses_answered_sig(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
path = td + "/x.answered.json"
s1 = w.WatcherState(persist_path=path)
now = time.time()
s1.record_answer("sig-abc", now)
s2 = w.WatcherState(persist_path=path)
self.assertEqual(s2.observe({"sig": "sig-abc"}, now + 5),
"none")
def test_expired_sig_not_loaded(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
path = td + "/x.answered.json"
s1 = w.WatcherState(persist_path=path)
s1.record_answer("sig-old",
time.time() - w.ANSWERED_TTL_SECONDS - 10)
s2 = w.WatcherState(persist_path=path)
self.assertNotIn("sig-old", s2.answered_sigs)
def test_corrupt_store_loads_empty(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
path = td + "/x.answered.json"
with open(path, "w") as f:
f.write("not json{{{")
s = w.WatcherState(persist_path=path)
self.assertEqual(s.answered_sigs, {})
def test_memory_only_without_path(self):
s = w.WatcherState()
s.record_answer("sig-x", time.time())
self.assertIn("sig-x", s.answered_sigs)
class TestPidfileFlock(unittest.TestCase):
"""Single daemon per pane, kernel-enforced: concurrent starters
must not pile up (double answers, '11' in the input box)."""
def _child_claim(self, path):
import subprocess
code = ("import sys; sys.path.insert(0, %r); "
"import muse_choice_watcher as w; "
"sys.exit(0 if w._claim_pidfile(%r) else 3)") % (
str(BIN_DIR), path)
return subprocess.run([sys.executable, "-c", code],
capture_output=True, timeout=30)
def test_second_process_refused_while_held(self):
import tempfile
with tempfile.TemporaryDirectory() as td:
path = td + "/x.pid"
self.assertTrue(w._claim_pidfile(path))
try:
r = self._child_claim(path)
self.assertEqual(r.returncode, 3)
finally:
w._release_pidfile(path)
def test_claim_succeeds_after_release(self):
import os
import tempfile
with tempfile.TemporaryDirectory() as td:
path = td + "/x.pid"
self.assertTrue(w._claim_pidfile(path))
w._release_pidfile(path)
self.assertFalse(os.path.exists(path))
r = self._child_claim(path)
self.assertEqual(r.returncode, 0)
class TestReconcileFailed(unittest.TestCase):
def test_failed_starts_recorded(self):
with mock.patch.object(w, "get_desired",
@@ -1451,5 +1528,124 @@ class TestBoxResolve(unittest.TestCase):
self.assertIsNone(data["held"])
class TestPeerAnsweredReload(unittest.TestCase):
"""A peer watcher answering first must suppress this watcher.
Concurrent duplicates share only the on-disk answered store: a
second watcher that loaded before the peer's answer must re-check
the store before typing, or both type '1' ('11' in the input box).
"""
def test_disk_answer_suppresses_send(self):
import json
import tempfile
import time
with tempfile.TemporaryDirectory() as td:
path = td + "/x.answered.json"
state = w.WatcherState(persist_path=path)
log = mock.Mock()
sig = w.find_choice_prompt(PROMPT_YN)["sig"]
with mock.patch.object(w, "pane_exists", return_value=True), \
mock.patch.object(w, "capture_pane",
side_effect=[PROMPT_YN] * 4), \
mock.patch.object(w, "send_answer",
return_value=True) as send, \
mock.patch.object(w, "audit"):
out1 = w._poll_once("/tmp/s", "%1", state, log,
dry_run=False)
self.assertEqual(out1, "seen")
# Peer answers the same prompt and persists first.
with open(path, "w") as f:
json.dump({sig: time.time()}, f)
out2 = w._poll_once("/tmp/s", "%1", state, log,
dry_run=False)
send.assert_not_called()
self.assertIn(out2, ("duplicate-suppressed", "none"))
self.assertIn(sig, state.answered_sigs)
class TestCaptureJoinWrapped(unittest.TestCase):
def test_capture_joins_wrapped_lines(self):
with mock.patch.object(w, "_tmux") as t:
t.return_value = mock.Mock(returncode=0, stdout="ok")
w.capture_pane("/tmp/s", "%1")
args = t.call_args[0]
self.assertIn("-J", args)
class TestLogPosture(unittest.TestCase):
"""Watcher start records the pane's permission posture.
The watcher answers with per-choice logging in every mode; bypass
postures (yolo / approval disabled) additionally audit, since the
session then acts outside the choice trail.
"""
def test_bypass_audits(self):
log = mock.Mock()
with mock.patch.object(w, "pane_muse_argv",
return_value=["muse", "--yolo"]), \
mock.patch.object(w, "audit") as audit:
w._log_posture("/tmp/s", "%1", log)
log.log.assert_called_once()
self.assertEqual(log.log.call_args[0][1], "pane posture")
self.assertEqual(log.log.call_args[1]["mode"], "yolo")
audit.assert_called_once()
self.assertEqual(audit.call_args[0][0], "muse-choice-posture")
self.assertTrue(audit.call_args[1]["extra"]["bypass"])
def test_default_logs_without_audit(self):
log = mock.Mock()
with mock.patch.object(w, "pane_muse_argv",
return_value=["muse"]), \
mock.patch.object(w, "audit") as audit:
w._log_posture("/tmp/s", "%1", log)
log.log.assert_called_once()
audit.assert_not_called()
def test_nondefault_profile_audits(self):
log = mock.Mock()
with mock.patch.object(w, "pane_muse_argv",
return_value=["muse", "--permission-profile",
":unrestricted"]), \
mock.patch.object(w, "audit") as audit:
w._log_posture("/tmp/s", "%1", log)
audit.assert_called_once()
extra = audit.call_args[1]["extra"]
self.assertEqual(extra["mode"], ":unrestricted")
self.assertFalse(extra["bypass"])
def test_argv_failure_never_raises(self):
log = mock.Mock()
with mock.patch.object(w, "pane_muse_argv",
side_effect=RuntimeError("tmux")), \
mock.patch.object(w, "audit") as audit:
w._log_posture("/tmp/s", "%1", log) # must not raise
log.log.assert_not_called()
audit.assert_not_called()
class TestReconcileOrphan(unittest.TestCase):
"""Reconcile must not start a second watcher when an orphan with a
lost pidfile is already alive for the pane."""
def test_orphan_counts_as_already(self):
orphan = [{"pid": 7777, "socket": "/tmp/sock", "pane": "%37"}]
with mock.patch.object(w, "get_desired",
return_value={"enabled": True,
"dry_run": False}), \
mock.patch.object(w, "muse_panes", return_value=["%37"]), \
mock.patch.object(w, "is_running", return_value=None), \
mock.patch.object(w, "_watch_procs", return_value=orphan), \
mock.patch.object(w, "_start_detached") as start, \
mock.patch.object(w, "status_all", return_value=[]), \
mock.patch.object(w, "audit"), \
mock.patch.object(w.os.path, "exists", return_value=True):
res = w.reconcile(sockets=["/tmp/sock"])
start.assert_not_called()
self.assertEqual(res["already"], ["/tmp/sock:%37"])
self.assertEqual(res["failed"], [])
if __name__ == "__main__":
unittest.main()