summaryrefslogtreecommitdiff
path: root/tests/test_server.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_server.py')
-rw-r--r--tests/test_server.py80
1 files changed, 78 insertions, 2 deletions
diff --git a/tests/test_server.py b/tests/test_server.py
index 26d01b7..c7f93d5 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -1,8 +1,10 @@
from __future__ import annotations
+from io import StringIO
import json
import os
from pathlib import Path
+import subprocess
import tempfile
import unittest
from unittest.mock import patch
@@ -14,8 +16,8 @@ from sailfish_devel_mcp.config import (
PathConfig,
load_config,
)
-from sailfish_devel_mcp.runner import CommandResult
-from sailfish_devel_mcp.server import McpServer
+from sailfish_devel_mcp.runner import CommandResult, run
+from sailfish_devel_mcp.server import McpServer, run_stdio
from sailfish_devel_mcp.tools import _device_home_path, _screenshot_prepare_command
from sailfish_devel_mcp.vendor import build_sailfishos
@@ -143,6 +145,80 @@ class McpServerTests(unittest.TestCase):
)
json.dumps(response)
+ def test_timeout_output_bytes_are_normalized_to_text(self) -> None:
+ timeout = subprocess.TimeoutExpired(
+ cmd=("ssh", "root@test"),
+ timeout=1,
+ output=b"partial stdout\n",
+ stderr=b"partial stderr\xff\n",
+ )
+ with patch("sailfish_devel_mcp.runner.subprocess.run", side_effect=timeout):
+ result = run(["ssh", "root@test"], timeout=1)
+
+ self.assertEqual(result.returncode, 124)
+ self.assertIsInstance(result.stdout, str)
+ self.assertIsInstance(result.stderr, str)
+ self.assertIn("partial stdout", result.stdout)
+ self.assertIn("partial stderr", result.stderr)
+ self.assertIn("command timed out after 1s", result.stderr)
+ json.dumps(result.public_dict())
+
+ def test_tool_exception_returns_tool_error_and_server_recovers(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ server = self.make_server(Path(tmp))
+ with patch(
+ "sailfish_devel_mcp.tools.handle_devices",
+ side_effect=RuntimeError("boom"),
+ ):
+ response = server.handle(
+ {
+ "jsonrpc": "2.0",
+ "id": 6,
+ "method": "tools/call",
+ "params": {"name": "sailfish_devices", "arguments": {}},
+ }
+ )
+ follow_up = server.handle(
+ {"jsonrpc": "2.0", "id": 7, "method": "tools/list", "params": {}}
+ )
+
+ self.assertNotIn("error", response)
+ self.assertTrue(response["result"]["isError"])
+ self.assertEqual(
+ response["result"]["structuredContent"]["exception"],
+ "RuntimeError",
+ )
+ self.assertIn("tools", follow_up["result"])
+
+ def test_stdio_serialization_failure_does_not_stop_server(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ server = self.make_server(Path(tmp))
+ stdin = StringIO(
+ "\n".join(
+ [
+ '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}',
+ '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}',
+ ]
+ )
+ + "\n"
+ )
+ stdout = StringIO()
+ with patch.object(
+ server,
+ "handle",
+ side_effect=[
+ {"jsonrpc": "2.0", "id": 1, "result": {"bad": {1}}},
+ {"jsonrpc": "2.0", "id": 2, "result": {"ok": True}},
+ ],
+ ):
+ run_stdio(server, stdin, stdout)
+
+ responses = [json.loads(line) for line in stdout.getvalue().splitlines()]
+ self.assertEqual(responses[0]["id"], 1)
+ self.assertEqual(responses[0]["error"]["code"], -32603)
+ self.assertIn("response serialization failed", responses[0]["error"]["message"])
+ self.assertEqual(responses[1]["result"], {"ok": True})
+
def test_default_config_uses_bundled_build_helper(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
missing = Path(tmp) / "missing-config.json"