feat(tmux): add server death watchdog daemon and multi-socket approver enhancements
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user