Skip to content

Commit 2377444

Browse files
author
Yoshihiro Takahara
committed
test: add tests for redirection error handling
1 parent a7f9edb commit 2377444

File tree

4 files changed

+134
-8
lines changed

4 files changed

+134
-8
lines changed

pyproject.toml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ authors = [
66
]
77
dependencies = [
88
"asyncio>=3.4.3",
9-
"mcp>=1.1.2",
9+
"mcp @ git+https://github.com/tumf/mcp-python-sdk.git@fix/handle-cancelled-notifications",
1010
]
1111
requires-python = ">=3.11"
1212
readme = "README.md"
@@ -70,3 +70,23 @@ path = "src/mcp_shell_server/version.py"
7070

7171
[tool.hatch.build.targets.wheel]
7272
packages = ["src/mcp_shell_server"]
73+
74+
[tool.hatch.metadata]
75+
allow-direct-references = true
76+
77+
78+
[tool.coverage.report]
79+
exclude_lines = [
80+
"pragma: no cover",
81+
"def __repr__",
82+
"raise NotImplementedError",
83+
"if __name__ == .__main__.:",
84+
"pass",
85+
"raise ImportError",
86+
"__version__",
87+
]
88+
89+
omit = [
90+
"src/mcp_shell_server/__init__.py",
91+
"src/mcp_shell_server/version.py",
92+
]

tests/test_init.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import asyncio
2+
3+
4+
def test_main(mocker):
5+
"""Test the main entry point"""
6+
# Mock asyncio.run
7+
mock_run = mocker.patch("asyncio.run")
8+
9+
# Import main after mocking
10+
from mcp_shell_server import main
11+
12+
# Call the main function
13+
main()
14+
15+
# Verify that asyncio.run was called
16+
assert mock_run.call_count == 1
17+
# The first argument of the call should be a coroutine object
18+
args = mock_run.call_args[0]
19+
assert len(args) == 1
20+
assert asyncio.iscoroutine(args[0])

tests/test_server.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,93 @@ async def test_disallowed_command(monkeypatch):
211211
},
212212
)
213213
assert "Command not allowed: sudo" in str(exc.value)
214+
215+
216+
@pytest.mark.asyncio
217+
async def test_call_tool_with_stderr(monkeypatch):
218+
"""Test command execution with stderr output"""
219+
monkeypatch.setenv("ALLOW_COMMANDS", "ls")
220+
result = await call_tool(
221+
"shell_execute",
222+
{"command": ["ls", "/nonexistent/directory"]},
223+
)
224+
assert len(result) >= 1
225+
stderr_content = next(
226+
(c for c in result if isinstance(c, TextContent) and "No such file" in c.text),
227+
None,
228+
)
229+
assert stderr_content is not None
230+
assert stderr_content.type == "text"
231+
232+
233+
@pytest.mark.asyncio
234+
async def test_main_server(mocker):
235+
"""Test the main server function"""
236+
# Mock the stdio_server
237+
mock_read_stream = mocker.AsyncMock()
238+
mock_write_stream = mocker.AsyncMock()
239+
240+
# Create an async context manager mock
241+
context_manager = mocker.AsyncMock()
242+
context_manager.__aenter__ = mocker.AsyncMock(
243+
return_value=(mock_read_stream, mock_write_stream)
244+
)
245+
context_manager.__aexit__ = mocker.AsyncMock(return_value=None)
246+
247+
# Set up stdio_server mock to return a regular function that returns the context manager
248+
def stdio_server_impl():
249+
return context_manager
250+
251+
mock_stdio_server = mocker.Mock(side_effect=stdio_server_impl)
252+
253+
# Mock app.run and create_initialization_options
254+
mock_server_run = mocker.patch("mcp_shell_server.server.app.run")
255+
mock_create_init_options = mocker.patch(
256+
"mcp_shell_server.server.app.create_initialization_options"
257+
)
258+
259+
# Import main after setting up mocks
260+
from mcp_shell_server.server import main
261+
262+
# Execute main function
263+
mocker.patch("mcp.server.stdio.stdio_server", mock_stdio_server)
264+
await main()
265+
266+
# Verify interactions
267+
mock_stdio_server.assert_called_once()
268+
context_manager.__aenter__.assert_awaited_once()
269+
context_manager.__aexit__.assert_awaited_once()
270+
mock_server_run.assert_called_once_with(
271+
mock_read_stream, mock_write_stream, mock_create_init_options.return_value
272+
)
273+
274+
275+
@pytest.mark.asyncio
276+
async def test_main_server_error_handling(mocker):
277+
"""Test error handling in the main server function"""
278+
# Mock app.run to raise an exception
279+
mocker.patch(
280+
"mcp_shell_server.server.app.run", side_effect=RuntimeError("Test error")
281+
)
282+
283+
# Mock the stdio_server
284+
context_manager = mocker.AsyncMock()
285+
context_manager.__aenter__ = mocker.AsyncMock(
286+
return_value=(mocker.AsyncMock(), mocker.AsyncMock())
287+
)
288+
context_manager.__aexit__ = mocker.AsyncMock(return_value=None)
289+
290+
def stdio_server_impl():
291+
return context_manager
292+
293+
mock_stdio_server = mocker.Mock(side_effect=stdio_server_impl)
294+
295+
# Import main after setting up mocks
296+
from mcp_shell_server.server import main
297+
298+
# Execute main function and expect it to raise the error
299+
mocker.patch("mcp.server.stdio.stdio_server", mock_stdio_server)
300+
with pytest.raises(RuntimeError) as exc:
301+
await main()
302+
303+
assert str(exc.value) == "Test error"

uv.lock

Lines changed: 3 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)