feat(tmux): add server death watchdog daemon and multi-socket approver enhancements

This commit is contained in:
operator
2026-10-07 01:50:06 +00:00
parent f2527f183c
commit 90f4ef661a
7 changed files with 551 additions and 5 deletions
+123
View File
@@ -220,5 +220,128 @@ class TestTallyGathering(unittest.TestCase):
self.assertEqual(tally.panes[1].agent_node, "dev")
class TestMuseDeferral(unittest.TestCase):
"""tmux approver must defer muse panes owned by muse_choice_watcher.
Live double-answer regression: both daemons answered the same
Would-you-like prompt within the same second (box-ctl + tmux audit
overlap on %40/%0/%2), producing '11' + stray keys in the input box.
When a per-pane muse watcher is alive, tmux must skip the pane.
"""
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.orig_state = tmux_auto_approver.STATE_FILE
self.orig_audit = tmux_auto_approver.AUDIT_LOG_FILE
tmux_auto_approver.STATE_FILE = Path(self.temp_dir.name) / "test_state.json"
tmux_auto_approver.AUDIT_LOG_FILE = Path(self.temp_dir.name) / "test_audit.jsonl"
def tearDown(self):
tmux_auto_approver.STATE_FILE = self.orig_state
tmux_auto_approver.AUDIT_LOG_FILE = self.orig_audit
self.temp_dir.cleanup()
def _muse_pane(self, pane_id="%37", socket="/tmp/tmux-1000/default"):
return TmuxPaneInfo(
socket=socket, session="muse", window_idx=0, pane_id=pane_id,
pane_pid=12345, current_command="muse-bin", active=True,
attached=True, title="muse terminal", agent_node="muse",
auto_approve=True,
)
def _tally(self, panes):
return TmuxWorkerTally(
total_sockets=1, total_sessions=1, total_panes=len(panes),
active_workers=len(panes), by_agent={}, panes=panes,
)
@patch("tmux_auto_approver.run_tmux_cmd")
@patch("tmux_auto_approver.capture_pane_text")
@patch("tmux_auto_approver.gather_tmux_tally")
def test_muse_pane_skipped_when_watcher_alive(
self, mock_tally, mock_capture, mock_tmux_cmd):
import muse_choice_watcher as mcw
mock_tally.return_value = self._tally([self._muse_pane()])
mock_capture.return_value = (
"Would you like to run the following?\n› 1. Yes, proceed (y)")
with patch.object(mcw, "is_running", return_value=99999):
runner = AutoApproverRunner(dry_run=True)
results = runner.run_once()
self.assertEqual(results, [])
mock_tmux_cmd.assert_not_called()
@patch("tmux_auto_approver.run_tmux_cmd")
@patch("tmux_auto_approver.capture_pane_text")
@patch("tmux_auto_approver.gather_tmux_tally")
def test_non_muse_pane_still_approved(
self, mock_tally, mock_capture, mock_tmux_cmd):
pane = TmuxPaneInfo(
socket="/tmp/tmux-pip.sock", session="worker", window_idx=0,
pane_id="%1", pane_pid=999, current_command="agent-worker",
active=True, attached=True, title="w", agent_node="pip",
auto_approve=True,
)
mock_tally.return_value = self._tally([pane])
mock_capture.return_value = (
"Would you like to run the following?\n› 1. Yes, proceed (y)")
runner = AutoApproverRunner(dry_run=True)
results = runner.run_once()
self.assertEqual(len(results), 1)
self.assertEqual(results[0]["action"], "DRY_RUN_MATCH")
class TestDedupSocketScoped(unittest.TestCase):
"""Dedup must be keyed by socket:pane, not bare pane id.
Same %N exists on every tmux socket; bare-pane dedup suppresses a
real prompt on socket B because socket A saw one (the %0-on-two-
sockets collision, tmux-side).
"""
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.orig_state = tmux_auto_approver.STATE_FILE
self.orig_audit = tmux_auto_approver.AUDIT_LOG_FILE
tmux_auto_approver.STATE_FILE = Path(self.temp_dir.name) / "test_state.json"
tmux_auto_approver.AUDIT_LOG_FILE = Path(self.temp_dir.name) / "test_audit.jsonl"
def tearDown(self):
tmux_auto_approver.STATE_FILE = self.orig_state
tmux_auto_approver.AUDIT_LOG_FILE = self.orig_audit
self.temp_dir.cleanup()
@patch("tmux_auto_approver.run_tmux_cmd")
@patch("tmux_auto_approver.capture_pane_text")
@patch("tmux_auto_approver.gather_tmux_tally")
def test_same_pane_id_on_two_sockets_both_approved(
self, mock_tally, mock_capture, mock_tmux_cmd):
def mk(sock):
return TmuxPaneInfo(
socket=sock, session="w", window_idx=0, pane_id="%1",
pane_pid=999, current_command="agent-worker", active=True,
attached=True, title="w", agent_node="pip",
auto_approve=True,
)
mock_tally.return_value = TmuxWorkerTally(
total_sockets=2, total_sessions=2, total_panes=2,
active_workers=2, by_agent={},
panes=[mk("/tmp/tmux-pip.sock"), mk("/tmp/tmux-opm.sock")],
)
mock_capture.return_value = (
"Would you like to run the following?\n› 1. Yes, proceed (y)")
runner = AutoApproverRunner(dry_run=True)
results = runner.run_once()
self.assertEqual(len(results), 2)
class TestCaptureJoinWrapped(unittest.TestCase):
def test_capture_joins_wrapped_lines(self):
with patch("tmux_auto_approver.run_tmux_cmd",
return_value=(0, "ok", "")) as m:
tmux_auto_approver.capture_pane_text("/tmp/s", "%1", lines=30)
args = m.call_args[0]
self.assertIn("-J", args)
if __name__ == "__main__":
unittest.main()