feat(muse): harden choice watcher concurrency, add rules dictionary, resume pool, and session bind
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user