From eb92561bd685961889cc553cd96db4a569eefec5 Mon Sep 17 00:00:00 2001 From: Andrew Branson Date: Mon, 29 Jun 2026 16:40:30 +0200 Subject: Add remote Android build tools and request logging Add Android build host config and MCP tools for remote AppSupport builds. Extend device RPM installs to copy dependency sets and install them together. Log MCP request and tool-call lifecycle events, and document the config. --- tests/test_server.py | 221 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) (limited to 'tests') diff --git a/tests/test_server.py b/tests/test_server.py index c7f93d5..3a80e80 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -10,8 +10,11 @@ import unittest from unittest.mock import patch from sailfish_devel_mcp.config import ( + AndroidBuildHostConfig, BUNDLED_BUILD_HELPER, Config, + DEFAULT_ANDROID_BUILD_PROJECT_DIR, + DEFAULT_ANDROID_BUILD_SSH_TARGET, DeviceConfig, PathConfig, load_config, @@ -69,6 +72,9 @@ class McpServerTests(unittest.TestCase): self.assertIn("sailfish_device_user_session_command", names) self.assertIn("sailfish_device_browser_launch", names) self.assertIn("sailfish_sdk_refresh_metadata", names) + self.assertIn("sailfish_android_build_hosts", names) + self.assertIn("sailfish_android_build", names) + self.assertIn("sailfish_android_build_status", names) self.assertIn("sailfish_qml_check_translator_ternaries", names) def test_qml_ternary_checker_reports_inline_ternary_qstrid(self) -> None: @@ -235,6 +241,15 @@ class McpServerTests(unittest.TestCase): self.assertIsNone(config.paths.local_sdk) self.assertTrue(config.paths.build_sailfishos.exists()) self.assertNotIn("build-sailfishos-skill", str(config.paths.build_sailfishos)) + self.assertEqual(config.default_android_build_host, "android-builder") + self.assertEqual( + config.android_build_hosts["android-builder"].ssh_target, + DEFAULT_ANDROID_BUILD_SSH_TARGET, + ) + self.assertEqual( + config.android_build_hosts["android-builder"].project_dir, + DEFAULT_ANDROID_BUILD_PROJECT_DIR, + ) def test_config_loads_local_sdk_from_paths(self) -> None: with tempfile.TemporaryDirectory() as tmp: @@ -459,6 +474,87 @@ class McpServerTests(unittest.TestCase): self.assertIn("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/100000/dbus/user_bus_socket", remote) self.assertIn("systemctl --user status app.service", remote) + def test_device_install_rpm_keeps_single_remote_path_override(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + rpm = root / "sample.rpm" + rpm.write_text("rpm", encoding="utf-8") + server = self.make_server(root) + with patch("sailfish_devel_mcp.tools.run") as mocked_run: + mocked_run.side_effect = [ + CommandResult(("scp",), 0, "", ""), + CommandResult(("ssh",), 0, "installed\n", ""), + ] + response = server.handle( + { + "jsonrpc": "2.0", + "id": 18, + "method": "tools/call", + "params": { + "name": "sailfish_device_install_rpm", + "arguments": { + "rpm_path": str(rpm), + "remote_path": "/tmp/custom.rpm", + }, + }, + } + ) + + self.assertFalse(response["result"].get("isError", False)) + calls = [list(call.args[0]) for call in mocked_run.call_args_list] + self.assertEqual(len(calls), 2) + self.assertEqual(calls[0][-1], "root@test:/tmp/custom.rpm") + self.assertEqual(calls[1][-1], "pkcon install-local -y /tmp/custom.rpm") + self.assertEqual( + response["result"]["structuredContent"]["remote_paths"], + ["/tmp/custom.rpm"], + ) + + def test_device_install_rpm_installs_multiple_rpms_together(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + rpm_a = root / "sample.rpm" + rpm_b = root / "sample-deps.rpm" + rpm_a.write_text("rpm", encoding="utf-8") + rpm_b.write_text("rpm", encoding="utf-8") + server = self.make_server(root) + with patch("sailfish_devel_mcp.tools.run") as mocked_run: + mocked_run.side_effect = [ + CommandResult(("ssh",), 0, "", ""), + CommandResult(("scp",), 0, "", ""), + CommandResult(("scp",), 0, "", ""), + CommandResult(("ssh",), 0, "installed\n", ""), + ] + response = server.handle( + { + "jsonrpc": "2.0", + "id": 19, + "method": "tools/call", + "params": { + "name": "sailfish_device_install_rpm", + "arguments": { + "rpm_paths": [str(rpm_a), str(rpm_b)], + "remote_dir": "/tmp/test-rpms", + "installer": "rpm", + }, + }, + } + ) + + self.assertFalse(response["result"].get("isError", False)) + calls = [list(call.args[0]) for call in mocked_run.call_args_list] + self.assertEqual(calls[0][-1], "mkdir -p /tmp/test-rpms") + self.assertEqual(calls[1][-1], "root@test:/tmp/test-rpms/sample.rpm") + self.assertEqual(calls[2][-1], "root@test:/tmp/test-rpms/sample-deps.rpm") + self.assertEqual( + calls[3][-1], + "rpm -Uvh --replacepkgs /tmp/test-rpms/sample.rpm /tmp/test-rpms/sample-deps.rpm", + ) + self.assertEqual( + response["result"]["structuredContent"]["remote_paths"], + ["/tmp/test-rpms/sample.rpm", "/tmp/test-rpms/sample-deps.rpm"], + ) + def test_browser_launch_uses_display_env_and_reports_topmost_pid(self) -> None: with tempfile.TemporaryDirectory() as tmp: server = self.make_server(Path(tmp)) @@ -713,6 +809,131 @@ class McpServerTests(unittest.TestCase): self.assertIn("--arch", argv) self.assertIn("aarch64", argv) + def test_android_build_starts_remote_nohup_job(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + config = Config( + path=None, + default_device="phone", + devices={"phone": DeviceConfig(name="phone", ssh_target="root@phone")}, + paths=PathConfig(git_root=root, ssh_config=root / "ssh_config"), + default_android_build_host="android-builder", + android_build_hosts={ + "android-builder": AndroidBuildHostConfig( + name="android-builder", + ssh_target="builder@example.invalid", + project_dir="/remote/a15", + state_dir="/remote/state/android-builds", + ) + }, + ) + server = McpServer(config) + with patch("sailfish_devel_mcp.tools.run") as mocked_run: + mocked_run.return_value = CommandResult( + ("ssh",), + 0, + "job_id: android-test\npid: 1234\n", + "", + ) + response = server.handle( + { + "jsonrpc": "2.0", + "id": 16, + "method": "tools/call", + "params": { + "name": "sailfish_android_build", + "arguments": { + "job_id": "android-test", + "shell_command": "source build/envsetup.sh && m services", + }, + }, + } + ) + + self.assertFalse(response["result"].get("isError", False)) + self.assertEqual( + response["result"]["structuredContent"]["job_id"], + "android-test", + ) + argv = list(mocked_run.call_args.args[0]) + self.assertEqual(argv[:3], ["ssh", "-F", str(root / "ssh_config")]) + self.assertIn("builder@example.invalid", argv) + remote = argv[-1] + self.assertIn("nohup", remote) + self.assertIn("/remote/a15", remote) + self.assertIn("/remote/state/android-builds/android-test", remote) + self.assertIn("base64 -d", remote) + + def test_android_build_requires_configured_project_dir(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + server = self.make_server(Path(tmp)) + response = server.handle( + { + "jsonrpc": "2.0", + "id": 16, + "method": "tools/call", + "params": { + "name": "sailfish_android_build", + "arguments": { + "job_id": "android-test", + "shell_command": "source build/envsetup.sh && m services", + }, + }, + } + ) + + self.assertTrue(response["result"].get("isError", False)) + self.assertIn( + "project_dir is required", + response["result"]["content"][0]["text"], + ) + + def test_android_build_status_tails_remote_job_log(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + server = self.make_server(root) + with patch("sailfish_devel_mcp.tools.run") as mocked_run: + mocked_run.return_value = CommandResult( + ("ssh",), + 0, + "\n".join( + [ + "job_id: android-test", + "state: finished", + "returncode: 7", + "pid: 1234", + "log_path: /remote/state/android-test/build.log", + "", + "build failed", + ] + ), + "", + ) + response = server.handle( + { + "jsonrpc": "2.0", + "id": 17, + "method": "tools/call", + "params": { + "name": "sailfish_android_build_status", + "arguments": { + "job_id": "android-test", + "state_dir": "/remote/state", + "lines": 25, + }, + }, + } + ) + + self.assertTrue(response["result"].get("isError", False)) + self.assertEqual(response["result"]["structuredContent"]["returncode"], 7) + argv = list(mocked_run.call_args.args[0]) + self.assertEqual(argv[:3], ["ssh", "-F", str(root / "ssh_config")]) + self.assertIn("builder@example.invalid", argv) + remote = argv[-1] + self.assertIn("tail -n \"$lines\" \"$log_path\"", remote) + self.assertIn("lines=25", remote) + def write_fake_target( self, root: Path, -- cgit v1.2.3