Skip to content

Commit 6d748cf

Browse files
committed
mcp(test): Add 49 tests for MCP tools, resources, and utilities
why: Ensure MCP server functionality works correctly with live tmux. what: - Add conftest.py with mcp_server, mcp_session, mcp_window, mcp_pane fixtures and server cache cleanup - Add test_utils.py for resolver and serializer functions - Add test files for all 6 tool modules - Add test_resources.py with mock MCP for resource functions
1 parent 54b4c2a commit 6d748cf

File tree

10 files changed

+696
-0
lines changed

10 files changed

+696
-0
lines changed

tests/mcp/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for libtmux MCP server."""

tests/mcp/conftest.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Test fixtures for libtmux MCP server tests."""
2+
3+
from __future__ import annotations
4+
5+
import typing as t
6+
7+
import pytest
8+
9+
from libtmux.mcp._utils import _server_cache
10+
11+
if t.TYPE_CHECKING:
12+
from libtmux.pane import Pane
13+
from libtmux.server import Server
14+
from libtmux.session import Session
15+
from libtmux.window import Window
16+
17+
18+
@pytest.fixture(autouse=True)
19+
def _clear_server_cache() -> t.Generator[None, None, None]:
20+
"""Clear the MCP server cache between tests."""
21+
_server_cache.clear()
22+
yield
23+
_server_cache.clear()
24+
25+
26+
@pytest.fixture
27+
def mcp_server(server: Server) -> Server:
28+
"""Provide a libtmux Server pre-registered in the MCP cache.
29+
30+
This fixture sets up the server cache so MCP tools can find the
31+
test server without environment variables.
32+
"""
33+
cache_key = (server.socket_name, None)
34+
_server_cache[cache_key] = server
35+
# Also register as default (None, None) for tools that don't specify a socket
36+
_server_cache[(None, None)] = server
37+
return server
38+
39+
40+
@pytest.fixture
41+
def mcp_session(mcp_server: Server, session: Session) -> Session:
42+
"""Provide a session accessible via MCP tools."""
43+
return session
44+
45+
46+
@pytest.fixture
47+
def mcp_window(mcp_session: Session) -> Window:
48+
"""Provide a window accessible via MCP tools."""
49+
return mcp_session.active_window
50+
51+
52+
@pytest.fixture
53+
def mcp_pane(mcp_window: Window) -> Pane:
54+
"""Provide a pane accessible via MCP tools."""
55+
active_pane = mcp_window.active_pane
56+
assert active_pane is not None
57+
return active_pane

tests/mcp/test_env_tools.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Tests for libtmux MCP environment tools."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import typing as t
7+
8+
from libtmux.mcp.tools.env_tools import set_environment, show_environment
9+
10+
if t.TYPE_CHECKING:
11+
from libtmux.server import Server
12+
from libtmux.session import Session
13+
14+
15+
def test_show_environment(mcp_server: Server, mcp_session: Session) -> None:
16+
"""show_environment returns environment variables."""
17+
result = show_environment(socket_name=mcp_server.socket_name)
18+
data = json.loads(result)
19+
assert isinstance(data, dict)
20+
21+
22+
def test_set_environment(mcp_server: Server, mcp_session: Session) -> None:
23+
"""set_environment sets an environment variable."""
24+
result = set_environment(
25+
name="MCP_TEST_VAR",
26+
value="test_value",
27+
socket_name=mcp_server.socket_name,
28+
)
29+
data = json.loads(result)
30+
assert data["status"] == "set"
31+
assert data["name"] == "MCP_TEST_VAR"
32+
33+
34+
def test_set_and_show_environment(mcp_server: Server, mcp_session: Session) -> None:
35+
"""set_environment value is readable via show_environment."""
36+
set_environment(
37+
name="MCP_ROUND_TRIP",
38+
value="hello",
39+
socket_name=mcp_server.socket_name,
40+
)
41+
result = show_environment(socket_name=mcp_server.socket_name)
42+
data = json.loads(result)
43+
assert data.get("MCP_ROUND_TRIP") == "hello"
44+
45+
46+
def test_show_environment_session(mcp_server: Server, mcp_session: Session) -> None:
47+
"""show_environment can target a specific session."""
48+
result = show_environment(
49+
session_name=mcp_session.session_name,
50+
socket_name=mcp_server.socket_name,
51+
)
52+
data = json.loads(result)
53+
assert isinstance(data, dict)

tests/mcp/test_option_tools.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Tests for libtmux MCP option tools."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import typing as t
7+
8+
from libtmux.mcp.tools.option_tools import set_option, show_option
9+
10+
if t.TYPE_CHECKING:
11+
from libtmux.server import Server
12+
from libtmux.session import Session
13+
14+
15+
def test_show_option(mcp_server: Server, mcp_session: Session) -> None:
16+
"""show_option returns an option value."""
17+
result = show_option(
18+
option="base-index",
19+
scope="session",
20+
global_=True,
21+
socket_name=mcp_server.socket_name,
22+
)
23+
data = json.loads(result)
24+
assert data["option"] == "base-index"
25+
assert "value" in data
26+
27+
28+
def test_set_option(mcp_server: Server, mcp_session: Session) -> None:
29+
"""set_option sets a tmux option."""
30+
result = set_option(
31+
option="display-time",
32+
value="3000",
33+
scope="server",
34+
global_=True,
35+
socket_name=mcp_server.socket_name,
36+
)
37+
data = json.loads(result)
38+
assert data["status"] == "set"
39+
assert data["option"] == "display-time"

tests/mcp/test_pane_tools.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Tests for libtmux MCP pane tools."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import typing as t
7+
8+
from libtmux.mcp.tools.pane_tools import (
9+
capture_pane,
10+
clear_pane,
11+
get_pane_info,
12+
kill_pane,
13+
send_keys,
14+
set_pane_title,
15+
)
16+
17+
if t.TYPE_CHECKING:
18+
from libtmux.pane import Pane
19+
from libtmux.server import Server
20+
from libtmux.session import Session
21+
22+
23+
def test_send_keys(mcp_server: Server, mcp_pane: Pane) -> None:
24+
"""send_keys sends keys to a pane."""
25+
result = send_keys(
26+
keys="echo hello_mcp",
27+
pane_id=mcp_pane.pane_id,
28+
socket_name=mcp_server.socket_name,
29+
)
30+
assert "sent" in result.lower()
31+
32+
33+
def test_capture_pane(mcp_server: Server, mcp_pane: Pane) -> None:
34+
"""capture_pane returns pane content."""
35+
result = capture_pane(
36+
pane_id=mcp_pane.pane_id,
37+
socket_name=mcp_server.socket_name,
38+
)
39+
assert isinstance(result, str)
40+
41+
42+
def test_get_pane_info(mcp_server: Server, mcp_pane: Pane) -> None:
43+
"""get_pane_info returns detailed pane info."""
44+
result = get_pane_info(
45+
pane_id=mcp_pane.pane_id,
46+
socket_name=mcp_server.socket_name,
47+
)
48+
data = json.loads(result)
49+
assert data["pane_id"] == mcp_pane.pane_id
50+
assert "pane_width" in data
51+
assert "pane_height" in data
52+
53+
54+
def test_set_pane_title(mcp_server: Server, mcp_pane: Pane) -> None:
55+
"""set_pane_title sets the pane title."""
56+
result = set_pane_title(
57+
title="my_test_title",
58+
pane_id=mcp_pane.pane_id,
59+
socket_name=mcp_server.socket_name,
60+
)
61+
data = json.loads(result)
62+
assert data["pane_id"] == mcp_pane.pane_id
63+
64+
65+
def test_clear_pane(mcp_server: Server, mcp_pane: Pane) -> None:
66+
"""clear_pane clears pane content."""
67+
result = clear_pane(
68+
pane_id=mcp_pane.pane_id,
69+
socket_name=mcp_server.socket_name,
70+
)
71+
assert "cleared" in result.lower()
72+
73+
74+
def test_kill_pane(mcp_server: Server, mcp_session: Session) -> None:
75+
"""kill_pane kills a pane."""
76+
window = mcp_session.active_window
77+
new_pane = window.split()
78+
pane_id = new_pane.pane_id
79+
result = kill_pane(
80+
pane_id=pane_id,
81+
socket_name=mcp_server.socket_name,
82+
)
83+
assert "killed" in result.lower()

tests/mcp/test_resources.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Tests for libtmux MCP resources."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import typing as t
7+
8+
import pytest
9+
10+
from libtmux.mcp.resources.hierarchy import register
11+
12+
if t.TYPE_CHECKING:
13+
from libtmux.pane import Pane
14+
from libtmux.server import Server
15+
from libtmux.session import Session
16+
from libtmux.window import Window
17+
18+
19+
@pytest.fixture
20+
def resource_functions(mcp_server: Server) -> dict[str, t.Any]:
21+
"""Register resources and return the function references.
22+
23+
Since resources are registered via decorators, we capture them
24+
by creating a mock FastMCP and collecting registered functions.
25+
"""
26+
functions: dict[str, t.Any] = {}
27+
28+
class MockMCP:
29+
def resource(self, uri: str) -> t.Any:
30+
def decorator(fn: t.Any) -> t.Any:
31+
functions[uri] = fn
32+
return fn
33+
34+
return decorator
35+
36+
register(MockMCP()) # type: ignore[arg-type]
37+
return functions
38+
39+
40+
def test_sessions_resource(
41+
resource_functions: dict[str, t.Any], mcp_session: Session
42+
) -> None:
43+
"""tmux://sessions returns session list."""
44+
fn = resource_functions["tmux://sessions"]
45+
result = fn()
46+
data = json.loads(result)
47+
assert isinstance(data, list)
48+
assert len(data) >= 1
49+
50+
51+
def test_session_detail_resource(
52+
resource_functions: dict[str, t.Any], mcp_session: Session
53+
) -> None:
54+
"""tmux://sessions/{name} returns session with windows."""
55+
fn = resource_functions["tmux://sessions/{session_name}"]
56+
result = fn(mcp_session.session_name)
57+
data = json.loads(result)
58+
assert "session_id" in data
59+
assert "windows" in data
60+
61+
62+
def test_session_windows_resource(
63+
resource_functions: dict[str, t.Any], mcp_session: Session
64+
) -> None:
65+
"""tmux://sessions/{name}/windows returns window list."""
66+
fn = resource_functions["tmux://sessions/{session_name}/windows"]
67+
result = fn(mcp_session.session_name)
68+
data = json.loads(result)
69+
assert isinstance(data, list)
70+
71+
72+
def test_window_detail_resource(
73+
resource_functions: dict[str, t.Any],
74+
mcp_session: Session,
75+
mcp_window: Window,
76+
) -> None:
77+
"""tmux://sessions/{name}/windows/{index} returns window with panes."""
78+
fn = resource_functions["tmux://sessions/{session_name}/windows/{window_index}"]
79+
result = fn(mcp_session.session_name, mcp_window.window_index)
80+
data = json.loads(result)
81+
assert "window_id" in data
82+
assert "panes" in data
83+
84+
85+
def test_pane_detail_resource(
86+
resource_functions: dict[str, t.Any], mcp_pane: Pane
87+
) -> None:
88+
"""tmux://panes/{pane_id} returns pane details."""
89+
fn = resource_functions["tmux://panes/{pane_id}"]
90+
result = fn(mcp_pane.pane_id)
91+
data = json.loads(result)
92+
assert data["pane_id"] == mcp_pane.pane_id
93+
94+
95+
def test_pane_content_resource(
96+
resource_functions: dict[str, t.Any], mcp_pane: Pane
97+
) -> None:
98+
"""tmux://panes/{pane_id}/content returns captured text."""
99+
fn = resource_functions["tmux://panes/{pane_id}/content"]
100+
result = fn(mcp_pane.pane_id)
101+
assert isinstance(result, str)

0 commit comments

Comments
 (0)