summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorAndrew Branson <andrew.branson@jolla.com>2026-05-15 12:21:39 +0200
committerAndrew Branson <andrew.branson@jolla.com>2026-05-15 12:21:39 +0200
commit1f14c5483ee111105f94d66fb3b82208946d914a (patch)
treed931715d7a3335324fbcdc51fcef1e0cef590235 /tests
Initial Sailfish devel MCP
Diffstat (limited to 'tests')
-rw-r--r--tests/test_server.py418
1 files changed, 418 insertions, 0 deletions
diff --git a/tests/test_server.py b/tests/test_server.py
new file mode 100644
index 0000000..e04b22b
--- /dev/null
+++ b/tests/test_server.py
@@ -0,0 +1,418 @@
+from __future__ import annotations
+
+import json
+import os
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from sailfish_devel_mcp.config import (
+ BUNDLED_BUILD_HELPER,
+ Config,
+ DeviceConfig,
+ PathConfig,
+ load_config,
+)
+from sailfish_devel_mcp.runner import CommandResult
+from sailfish_devel_mcp.server import McpServer
+from sailfish_devel_mcp.tools import _device_home_path, _screenshot_prepare_command
+from sailfish_devel_mcp.vendor import build_sailfishos
+
+
+class McpServerTests(unittest.TestCase):
+ def make_server(self, root: Path) -> McpServer:
+ config = Config(
+ path=None,
+ default_device="test",
+ devices={"test": DeviceConfig(name="test", ssh_target="root@test")},
+ paths=PathConfig(
+ git_root=root,
+ ssh_config=root / "ssh_config",
+ build_sailfishos=root / "tools" / "build_sailfishos.py",
+ osc_api_alias="jolla",
+ ),
+ )
+ return McpServer(config)
+
+ def test_initialize_uses_requested_supported_version(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ server = self.make_server(Path(tmp))
+ response = server.handle(
+ {
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "initialize",
+ "params": {
+ "protocolVersion": "2025-06-18",
+ "capabilities": {},
+ "clientInfo": {"name": "test", "version": "0"},
+ },
+ }
+ )
+ self.assertIsNotNone(response)
+ self.assertEqual(response["result"]["protocolVersion"], "2025-06-18")
+
+ def test_tools_list_contains_device_and_qml_tools(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ server = self.make_server(Path(tmp))
+ response = server.handle(
+ {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
+ )
+ names = {tool["name"] for tool in response["result"]["tools"]}
+ self.assertIn("sailfish_device_topmost_pid", names)
+ self.assertIn("sailfish_device_touch", names)
+ self.assertIn("sailfish_qml_check_translator_ternaries", names)
+
+ def test_qml_ternary_checker_reports_inline_ternary_qstrid(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ qml = root / "Example.qml"
+ qml.write_text(
+ 'Text { text: enabled ? qsTrId("a") : qsTrId("b") }\n',
+ encoding="utf-8",
+ )
+ server = self.make_server(root)
+ response = server.handle(
+ {
+ "jsonrpc": "2.0",
+ "id": 3,
+ "method": "tools/call",
+ "params": {
+ "name": "sailfish_qml_check_translator_ternaries",
+ "arguments": {"path": str(root)},
+ },
+ }
+ )
+ self.assertTrue(response["result"]["isError"])
+ findings = response["result"]["structuredContent"]["findings"]
+ self.assertEqual(len(findings), 1)
+ self.assertEqual(findings[0]["line"], 1)
+
+ def test_spec_summary_parses_core_fields(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ rpm = root / "rpm"
+ rpm.mkdir()
+ spec = rpm / "sample.spec"
+ spec.write_text(
+ "\n".join(
+ [
+ "Name: sample",
+ "Version: 1.2.3",
+ "Release: 1",
+ "Summary: Sample package",
+ "BuildRequires: pkgconfig(Qt5Core)",
+ "Requires: lipstick",
+ ]
+ ),
+ encoding="utf-8",
+ )
+ server = self.make_server(root)
+ response = server.handle(
+ {
+ "jsonrpc": "2.0",
+ "id": 4,
+ "method": "tools/call",
+ "params": {
+ "name": "sailfish_spec_summary",
+ "arguments": {"repo_path": str(root)},
+ },
+ }
+ )
+ summary = response["result"]["structuredContent"]["summary"]
+ self.assertEqual(summary["Name"], "sample")
+ self.assertEqual(summary["Version"], "1.2.3")
+ self.assertEqual(summary["BuildRequires"], ["pkgconfig(Qt5Core)"])
+
+ def test_server_writes_json_serializable_tool_result(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ server = self.make_server(Path(tmp))
+ response = server.handle(
+ {
+ "jsonrpc": "2.0",
+ "id": 5,
+ "method": "tools/call",
+ "params": {"name": "sailfish_devices", "arguments": {}},
+ }
+ )
+ json.dumps(response)
+
+ def test_default_config_uses_bundled_build_helper(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ missing = Path(tmp) / "missing-config.json"
+ with patch.dict(os.environ, {}, clear=True):
+ config = load_config(missing)
+ self.assertEqual(config.default_device, "device")
+ self.assertEqual(config.devices["device"].ssh_target, "root@device")
+ self.assertEqual(config.devices["device"].username, "defaultuser")
+ self.assertEqual(_device_home_path(config.devices["device"]), "/home/defaultuser")
+ self.assertNotIn("ssh_config", config.devices["device"].public_dict())
+ self.assertEqual(config.paths.ssh_config, Path.home() / ".ssh" / "config")
+ self.assertEqual(config.paths.build_sailfishos, BUNDLED_BUILD_HELPER)
+ self.assertIsNone(config.paths.local_sdk)
+ self.assertTrue(config.paths.build_sailfishos.exists())
+ self.assertNotIn("build-sailfishos-skill", str(config.paths.build_sailfishos))
+
+ def test_config_loads_local_sdk_from_paths(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ local_sdk = root / "sdk-chroot"
+ ssh_config = root / "ssh_config"
+ config_path = root / "config.json"
+ config_path.write_text(
+ json.dumps(
+ {
+ "devices": {"phone": "root@phone"},
+ "paths": {
+ "git_root": str(root),
+ "ssh_config": str(ssh_config),
+ "local_sdk": str(local_sdk),
+ },
+ }
+ ),
+ encoding="utf-8",
+ )
+ config = load_config(config_path)
+ self.assertEqual(config.paths.local_sdk, local_sdk)
+ self.assertEqual(config.paths.ssh_config, ssh_config)
+ self.assertEqual(config.devices["phone"].release, "")
+
+ def test_config_keeps_device_release_literal(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ config_path = root / "config.json"
+ config_path.write_text(
+ json.dumps(
+ {
+ "devices": {
+ "phone": {
+ "ssh_target": "root@phone",
+ "release": "devel",
+ }
+ }
+ }
+ ),
+ encoding="utf-8",
+ )
+ config = load_config(config_path)
+ self.assertEqual(config.devices["phone"].release, "devel")
+
+ def test_screenshot_prepare_command_uses_home_ownership(self) -> None:
+ device = DeviceConfig(name="test", ssh_target="root@test")
+ command = _screenshot_prepare_command(
+ device,
+ "/home/defaultuser/Pictures/Screenshots/example.png",
+ )
+ self.assertIn("stat -Lc %U", command)
+ self.assertIn("install -d -m 775", command)
+ self.assertIn("install -d -m 755", command)
+ self.assertIn("privileged", command)
+
+ def test_device_touch_tap_builds_input_injection_command(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ server = self.make_server(Path(tmp))
+ with patch("sailfish_devel_mcp.tools.run") as mocked_run:
+ mocked_run.return_value = CommandResult(
+ ("ssh", "root@test"),
+ 0,
+ "input_device=/dev/input/event5\ntap=ok\n",
+ "",
+ )
+ response = server.handle(
+ {
+ "jsonrpc": "2.0",
+ "id": 6,
+ "method": "tools/call",
+ "params": {
+ "name": "sailfish_device_touch",
+ "arguments": {
+ "action": "tap",
+ "input_device": "/dev/input/event5",
+ "x": 12,
+ "y": 34,
+ },
+ },
+ }
+ )
+ self.assertFalse(response["result"].get("isError", False))
+ argv = mocked_run.call_args.args[0]
+ remote = argv[-1]
+ self.assertEqual(argv[:3], ["ssh", "-F", str(Path(tmp) / "ssh_config")])
+ self.assertIn("INPUT_DEVICE=/dev/input/event5", remote)
+ self.assertIn("X=12", remote)
+ self.assertIn("Y=34", remote)
+ self.assertIn("ABS_MT_TRACKING_ID", remote)
+
+ def test_device_touch_discover_uses_evdev_trace_when_available(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ server = self.make_server(Path(tmp))
+ with patch("sailfish_devel_mcp.tools.run") as mocked_run:
+ mocked_run.return_value = CommandResult(("ssh", "root@test"), 0, "", "")
+ response = server.handle(
+ {
+ "jsonrpc": "2.0",
+ "id": 7,
+ "method": "tools/call",
+ "params": {
+ "name": "sailfish_device_touch",
+ "arguments": {"action": "discover"},
+ },
+ }
+ )
+ self.assertFalse(response["result"].get("isError", False))
+ remote = mocked_run.call_args.args[0][-1]
+ self.assertNotIn("evdev_trace -i", remote)
+ self.assertIn("/proc/bus/input/devices", remote)
+
+ def test_device_touch_discover_can_include_evdev_trace(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ server = self.make_server(Path(tmp))
+ with patch("sailfish_devel_mcp.tools.run") as mocked_run:
+ mocked_run.return_value = CommandResult(("ssh", "root@test"), 0, "", "")
+ response = server.handle(
+ {
+ "jsonrpc": "2.0",
+ "id": 8,
+ "method": "tools/call",
+ "params": {
+ "name": "sailfish_device_touch",
+ "arguments": {
+ "action": "discover",
+ "include_evdev_trace": True,
+ },
+ },
+ }
+ )
+ self.assertFalse(response["result"].get("isError", False))
+ remote = mocked_run.call_args.args[0][-1]
+ self.assertIn("evdev_trace -i", remote)
+
+ def test_build_defaults_prefer_configured_local_sdk(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ (root / "rpm").mkdir()
+ (root / "rpm" / "sample.spec").write_text("Name: sample\n", encoding="utf-8")
+ helper = root / "build_sailfishos.py"
+ local_sdk = root / "sdk-chroot"
+ helper.write_text("#!/usr/bin/env python3\n", encoding="utf-8")
+ config = Config(
+ path=None,
+ default_device="phone",
+ devices={
+ "phone": DeviceConfig(
+ name="phone",
+ ssh_target="root@phone",
+ architecture="aarch64",
+ release="live",
+ )
+ },
+ paths=PathConfig(
+ git_root=root,
+ ssh_config=root / "ssh_config",
+ build_sailfishos=helper,
+ local_sdk=local_sdk,
+ ),
+ )
+ server = McpServer(config)
+ with patch("sailfish_devel_mcp.tools.run") as mocked_run:
+ mocked_run.return_value = CommandResult(("python3", str(helper)), 0, "", "")
+ response = server.handle(
+ {
+ "jsonrpc": "2.0",
+ "id": 6,
+ "method": "tools/call",
+ "params": {
+ "name": "sailfish_build_rpm",
+ "arguments": {"project_path": str(root), "device": "phone"},
+ },
+ }
+ )
+ self.assertFalse(response["result"].get("isError", False))
+ argv = list(mocked_run.call_args.args[0])
+ self.assertIn("--local-sdk", argv)
+ self.assertIn(str(local_sdk), argv)
+ self.assertIn("--release", argv)
+ self.assertIn("live", argv)
+ self.assertIn("--arch", argv)
+ self.assertIn("aarch64", argv)
+
+ def write_fake_target(
+ self,
+ root: Path,
+ name: str,
+ *,
+ ssu_release: str,
+ version_id: str,
+ flavour: str = "devel",
+ ) -> None:
+ target = root / "targets" / name
+ (target / "etc" / "ssu").mkdir(parents=True)
+ (target / "etc").mkdir(exist_ok=True)
+ (target / "etc" / "ssu" / "ssu.ini").write_text(
+ f"[rnd]\nrelease={ssu_release}\nflavour={flavour}\n",
+ encoding="utf-8",
+ )
+ (target / "etc" / "sailfish-release").write_text(
+ f'VERSION_ID={version_id}\nSAILFISH_FLAVOUR="{flavour}"\n',
+ encoding="utf-8",
+ )
+
+ def test_build_helper_selects_matching_local_sdk_targets(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ local_sdk = root / "sdks" / "sfossdk" / "sdk-chroot"
+ local_sdk.parent.mkdir(parents=True)
+ local_sdk.touch()
+ sdk_root = local_sdk.parent
+ self.write_fake_target(sdk_root, "aarch64", ssu_release="live", version_id="5.2.0.4")
+ self.write_fake_target(
+ sdk_root,
+ "aarch64-5.0.0",
+ ssu_release="5.0.0",
+ version_id="5.0.0.191",
+ )
+
+ self.assertEqual(build_sailfishos.normalize_release_tag("live"), "live")
+ self.assertEqual(build_sailfishos.normalize_release_tag("devel"), "devel")
+ self.assertEqual(build_sailfishos.normalize_local_release("devel"), "devel")
+ live = build_sailfishos.select_local_sdk_builds(
+ local_sdk,
+ "live",
+ ["aarch64"],
+ False,
+ root,
+ )
+ versioned = build_sailfishos.select_local_sdk_builds(
+ local_sdk,
+ "5.0.0",
+ ["aarch64"],
+ False,
+ root,
+ )
+ missing = build_sailfishos.select_local_sdk_builds(
+ local_sdk,
+ "4.5.0",
+ ["aarch64"],
+ False,
+ root,
+ )
+ exact_mismatch = build_sailfishos.select_local_sdk_builds(
+ local_sdk,
+ "5.0.0.55",
+ ["aarch64"],
+ False,
+ root,
+ )
+
+ self.assertEqual(live, [build_sailfishos.LocalSdkBuild("aarch64", "aarch64")])
+ self.assertEqual(
+ versioned,
+ [build_sailfishos.LocalSdkBuild("aarch64", "aarch64-5.0.0")],
+ )
+ self.assertIsNone(missing)
+ self.assertIsNone(exact_mismatch)
+
+
+if __name__ == "__main__":
+ unittest.main()