"""Tests for the tmux terminal backend."""
import subprocess
import pytest
from maniple_mcp.terminal_backends.base import TerminalSession
from maniple_mcp.terminal_backends.tmux import TmuxBackend, tmux_session_name_for_project
# subprocess is still needed for tests that mock tmux CalledProcessError
@pytest.mark.asyncio
async def test_send_text_uses_send_keys(monkeypatch):
backend = TmuxBackend()
calls = []
async def fake_run(args):
calls.append(args)
return ""
monkeypatch.setattr(backend, "_run_tmux", fake_run)
session = TerminalSession("tmux", "%1", "%1")
await backend.send_text(session, "hello")
assert calls == [["send-keys", "-t", "%1", "-l", "hello"]]
@pytest.mark.asyncio
async def test_send_key_maps_ctrl_c(monkeypatch):
backend = TmuxBackend()
calls = []
async def fake_run(args):
calls.append(args)
return ""
monkeypatch.setattr(backend, "_run_tmux", fake_run)
session = TerminalSession("tmux", "%2", "%2")
await backend.send_key(session, "ctrl-c")
assert calls == [["send-keys", "-t", "%2", "C-c"]]
@pytest.mark.asyncio
async def test_list_sessions_parses_panes(monkeypatch):
backend = TmuxBackend()
session_one = tmux_session_name_for_project("/Users/test/claude-team")
session_two = tmux_session_name_for_project("/Users/test/other-project")
async def fake_run(args):
assert args[:2] == ["list-panes", "-a"]
return (
f"{session_one}\t@1\tworker-1\t0\t0\t%1\n"
"unrelated\t@2\tother\t0\t0\t%5\n"
f"{session_two}\t@3\tworker-2\t1\t2\t%9\n"
)
monkeypatch.setattr(backend, "_run_tmux", fake_run)
sessions = await backend.list_sessions()
assert len(sessions) == 2
assert sessions[0].native_id == "%1"
assert sessions[0].metadata["session_name"] == session_one
assert sessions[0].metadata["window_name"] == "worker-1"
assert sessions[1].metadata["pane_index"] == "2"
@pytest.mark.asyncio
async def test_list_sessions_includes_legacy_prefix(monkeypatch):
backend = TmuxBackend()
legacy_session = "claude-team-legacy-project"
async def fake_run(args):
assert args[:2] == ["list-panes", "-a"]
return f"{legacy_session}\t@1\tworker-1\t0\t0\t%1\n"
monkeypatch.setattr(backend, "_run_tmux", fake_run)
sessions = await backend.list_sessions()
assert len(sessions) == 1
assert sessions[0].metadata["session_name"] == legacy_session
@pytest.mark.asyncio
async def test_create_session_uses_tmux_commands(monkeypatch):
backend = TmuxBackend()
calls = []
project_path = "/Users/test/claude-team/.worktrees/feature-foo"
session_name = tmux_session_name_for_project(project_path)
async def fake_run(args):
calls.append(args)
if args[:2] == ["has-session", "-t"]:
raise subprocess.CalledProcessError(1, ["tmux"])
if args[:2] == ["new-session", "-d"]:
return "%7\t@7\t0"
return ""
monkeypatch.setattr(backend, "_run_tmux", fake_run)
session = await backend.create_session(
"test-session",
project_path=project_path,
issue_id="cic-e55",
)
assert calls[0] == ["has-session", "-t", session_name]
assert calls[1][:4] == ["new-session", "-d", "-s", session_name]
assert session.native_id == "%7"
assert session.metadata["session_name"] == session_name
assert session.metadata["window_name"] == "test-session | claude-team [cic-e55]"
assert session.metadata["project_name"] == "claude-team"
assert session.metadata["issue_id"] == "cic-e55"
@pytest.mark.asyncio
async def test_create_session_uses_annotation_issue_id(monkeypatch):
backend = TmuxBackend()
calls = []
project_path = "/Users/test/deedee-ai"
session_name = tmux_session_name_for_project(project_path)
async def fake_run(args):
calls.append(args)
if args[:2] == ["has-session", "-t"]:
raise subprocess.CalledProcessError(1, ["tmux"])
if args[:2] == ["new-session", "-d"]:
return "%8\t@8\t1"
return ""
monkeypatch.setattr(backend, "_run_tmux", fake_run)
session = await backend.create_session(
"worker",
project_path=project_path,
coordinator_annotation="Handle BEA-123 follow-up",
)
assert calls[0] == ["has-session", "-t", session_name]
assert session.metadata["window_name"] == "worker | deedee-ai [BEA-123]"
assert session.metadata["issue_id"] == "BEA-123"
@pytest.mark.asyncio
async def test_find_available_window_prefers_active_pane(monkeypatch):
backend = TmuxBackend()
session_one = tmux_session_name_for_project("/Users/test/alpha")
session_two = tmux_session_name_for_project("/Users/test/bravo")
async def fake_run(args):
assert args[:2] == ["list-panes", "-a"]
return (
f"{session_one}\t@1\t0\t0\t0\t%1\n"
f"{session_one}\t@1\t0\t1\t1\t%2\n"
f"{session_two}\t@2\t0\t0\t1\t%3\n"
)
monkeypatch.setattr(backend, "_run_tmux", fake_run)
result = await backend.find_available_window(max_panes=3, managed_session_ids=None)
assert result is not None
session_name, window_index, session = result
assert session_name == session_one
assert window_index == "0"
assert session.native_id == "%2"
assert session.metadata["pane_index"] == "1"
@pytest.mark.asyncio
async def test_find_available_window_includes_legacy_prefix(monkeypatch):
backend = TmuxBackend()
legacy_session = "claude-team-legacy-project"
async def fake_run(args):
assert args[:2] == ["list-panes", "-a"]
return f"{legacy_session}\t@1\t0\t0\t1\t%1\n"
monkeypatch.setattr(backend, "_run_tmux", fake_run)
result = await backend.find_available_window(max_panes=4, managed_session_ids=None)
assert result is not None
session_name, _, _ = result
assert session_name == legacy_session
@pytest.mark.asyncio
async def test_find_available_window_respects_managed_filter(monkeypatch):
backend = TmuxBackend()
session_one = tmux_session_name_for_project("/Users/test/alpha")
session_two = tmux_session_name_for_project("/Users/test/bravo")
async def fake_run(args):
assert args[:2] == ["list-panes", "-a"]
return (
f"{session_one}\t@1\t0\t0\t1\t%1\n"
f"{session_one}\t@1\t0\t1\t0\t%2\n"
f"{session_two}\t@2\t1\t0\t1\t%3\n"
)
monkeypatch.setattr(backend, "_run_tmux", fake_run)
result = await backend.find_available_window(
max_panes=4,
managed_session_ids={"%3"},
)
assert result is not None
session_name, window_index, session = result
assert session_name == session_two
assert window_index == "1"
assert session.native_id == "%3"
@pytest.mark.asyncio
async def test_find_available_window_returns_none_when_full(monkeypatch):
backend = TmuxBackend()
session_one = tmux_session_name_for_project("/Users/test/alpha")
async def fake_run(args):
assert args[:2] == ["list-panes", "-a"]
return (
f"{session_one}\t@1\t0\t0\t1\t%1\n"
f"{session_one}\t@1\t0\t1\t0\t%2\n"
)
monkeypatch.setattr(backend, "_run_tmux", fake_run)
result = await backend.find_available_window(max_panes=2)
assert result is None
def test_tmux_session_name_format():
"""Test that session names follow the format maniple-{slug}."""
session = tmux_session_name_for_project("/Users/test/my-project")
assert session == "maniple-my-project"
def test_tmux_session_name_same_for_worktree_and_main():
"""Test that worktree and main repo produce the same session name."""
worktree_path = "/Users/test/claude-team/.worktrees/feature-foo"
main_repo_path = "/Users/test/claude-team"
worktree_session = tmux_session_name_for_project(worktree_path)
main_session = tmux_session_name_for_project(main_repo_path)
assert worktree_session == main_session
assert worktree_session == "maniple-claude-team"
def test_tmux_session_name_fallback_for_none():
"""Test that None project path produces fallback session name."""
session = tmux_session_name_for_project(None)
assert session == "maniple-project"