feat: introduce MCP server, cross-platform runtime, and Showcase UI reorganization

- Showcase UI Reorganization:
  * Move web UI dashboard from root directory into apps/showcase_ui/ for cleaner project structure
  * Add 'artemis ui' CLI command (artemis/interfaces/cli/commands/ui.py) and launcher scripts for the interactive showcase console
- Standalone MCP Server & Notifiers (mcp_server/):
  * Implement dedicated MCP server package with mobile automation tools (device_state, inspect_trace, task_manager, task_runner)
  * Add multi-channel notifiers (AgentAPI, Desktop notifications, Webhooks, File output, and Composite routing)
  * Introduce background task runner and trace store for autonomous mobile execution monitoring
- Cross-Platform Runtime & Toolchain Support:
  * Add artemis/platform/ with OS-specific implementations for Linux, macOS (Darwin), and Windows
  * Add artemis/runtime/supervisor.py for resilient process and agent runtime supervision
  * Add artemis/toolchain/ with dependency resolution and SDK descriptors
  * Add cross-platform launcher scripts (start.sh, start.bat) and installer scripts (scripts/install_deps.*)
- Core Enhancements & Diagnostics:
  * Enhance coordinate normalization, OCR API fallback, and perception pipeline
  * Add credential validation (credentials_validator.py) and system diagnostics
  * Update bilingual documentation (README.md, README_CN.md) and architecture diagrams
- Test Suite Expansion:
  * Add integration tests for cloud gateway, remote ADB, and Artemis login
  * Add unit tests covering core diagnostics, platform detection, toolchain, credentials validator, optional OCR, and MCP tools
This commit is contained in:
somew1nd committed 2026-08-18 13:59:27 -07:00
1 parent 78e478f502
commit 9a5aaf99b7
249 files changed
+37226 -9339

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import pty
import select
import sys
import time
import pytest
@pytest.mark.skip(reason="Manual integration test requiring remote host")
def test_artemis_ssh_login():
"""Manual test to verify SSH login to remote Artemis instance."""
run_artemis_ssh_login()
def run_artemis_ssh_login():
host = os.getenv("ARTEMIS_SSH_TEST_HOST", "127.0.0.1")
user = os.getenv("ARTEMIS_SSH_TEST_USER", "artemis")
password = os.getenv("ARTEMIS_SSH_TEST_PASSWORD", "password")
cmd = [
"ssh",
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/dev/null",
"-o",
"PreferredAuthentications=password,keyboard-interactive",
f"{user}@{host}",
(
"echo '=================================================='; echo '🚀 BINGO!! PURE"
" PASSWORD LOGIN VICTORY 🚀'; echo"
" '=================================================='; whoami; hostname; uptime"
),
]
master, slave = pty.openpty()
pid = os.fork()
if pid == 0:
os.close(master)
os.execvp(cmd[0], cmd)
else:
os.close(slave)
output = b""
start = time.time()
password_sent = False
while time.time() - start < 15:
r, _, _ = select.select([master], [], [], 0.3)
if master in r:
try:
data = os.read(master, 2048)
if not data:
break
output += data
sys.stdout.buffer.write(data)
sys.stdout.flush()
if (b"Password:" in data or b"password:" in data) and not password_sent:
time.sleep(0.2)
os.write(master, f"{password}\n".encode())
password_sent = True
except OSError:
break
os.close(master)
os.waitpid(pid, 0)
if __name__ == "__main__":
run_artemis_ssh_login()
+126
View File
@@ -0,0 +1,126 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Unit and Integration Tests for GCP Cloud Brain Outer Server Gateway.
Tests multi-tenant authentication, device registration, device mutex locking,
workspace path resolution, RESTful /api/v1/* endpoints, and SSE event streaming.
"""
import pytest
from fastapi.testclient import TestClient
from cloud_service.gateway.auth import TokenAuthProvider
from cloud_service.gateway.manager import ConnectionManager
try:
from cloud_service.server import app
except ImportError:
from apps.cloud_service.server import app
client = TestClient(app)
def test_token_auth_provider():
auth = TokenAuthProvider()
# Test valid tenant name as Bearer token
assert auth.authenticate("Bearer alice") == "alice"
assert auth.authenticate("Bearer bob") == "bob"
assert auth.authenticate("alice-token") == "alice"
# Test invalid token
assert auth.authenticate("Bearer invalid-token-1234") is None
assert auth.authenticate("invalid-token-1234") is None
def test_connection_manager_device_lifecycle():
mgr = ConnectionManager()
device = mgr.register_device("alice", "pixel8-test-001", model="Pixel 8")
assert device.tenant == "alice"
assert device.device_serial == "pixel8-test-001"
assert device.status == "ONLINE"
devices = mgr.list_tenant_devices("alice")
assert len(devices) == 1
assert devices[0].device_serial == "pixel8-test-001"
# Test unregister
assert mgr.unregister_device("alice", "pixel8-test-001") is True
assert len(mgr.list_tenant_devices("alice")) == 0
assert mgr.unregister_device("alice", "non-existent") is False
def test_workspace_dir_resolution():
mgr = ConnectionManager()
ws_path = mgr.get_workspace_dir("alice", "session_test_001")
assert "alice" in str(ws_path)
assert "session_test_001" in str(ws_path)
assert ws_path.exists()
def test_api_v1_tenant_profile():
headers = {"Authorization": "Bearer alice"}
resp = client.get("/api/v1/tenants/me", headers=headers)
assert resp.status_code == 200
data = resp.json()
assert data["tenant_id"] == "alice"
assert "max_concurrent_sessions" in data
def test_api_v1_unauthorized():
resp = client.get("/api/v1/tenants/me")
assert resp.status_code == 401
resp2 = client.get("/api/v1/tenants/me", headers={"Authorization": "Bearer bad-token"})
assert resp2.status_code in (401, 403)
def test_api_v1_device_register_and_list():
headers = {"Authorization": "Bearer alice"}
reg_payload = {
"device_serial": "pixel8-api-001",
"model": "Pixel 8 Pro",
"android_version": "14",
}
resp = client.post("/api/v1/devices/register", json=reg_payload, headers=headers)
assert resp.status_code == 200
data = resp.json().get("device", resp.json())
assert data["device_serial"] == "pixel8-api-001"
assert data["model"] == "Pixel 8 Pro"
# List devices
list_resp = client.get("/api/v1/devices", headers=headers)
assert list_resp.status_code == 200
devices = list_resp.json()["devices"]
assert any(d["device_serial"] == "pixel8-api-001" for d in devices)
# Unregister device
del_resp = client.delete("/api/v1/devices/pixel8-api-001", headers=headers)
assert del_resp.status_code == 200
assert del_resp.json()["status"] == "unregistered"
@pytest.mark.asyncio
async def test_device_mutex_lock():
mgr = ConnectionManager()
mgr.register_device("alice", "pixel8-mutex-001")
lock = mgr.get_device_lock("pixel8-mutex-001")
# Simulate device busy by acquiring lock
await lock.acquire()
try:
with pytest.raises(RuntimeError, match="currently BUSY"):
await mgr.spawn_user_session("alice", "pixel8-mutex-001", "test goal")
finally:
lock.release()
+167
View File
@@ -0,0 +1,167 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Unit tests for RemoteAdbDevice, RemoteAdbClient, and platform specific commands parity.
"""
from unittest.mock import MagicMock
from cloud_service.virtualization.remote_adb import (
RemoteAdbDevice,
RemoteAdbClient,
)
from artemis.context import ArtemisContext, DeviceContext, DevicePlatform
from artemis.controllers.platform_specific_commands_controller import (
get_adb_device,
list_packages,
get_current_foreground_package,
get_device_date,
)
def test_remote_adb_device_shell():
mock_bridge = MagicMock()
mock_bridge.execute_sync.return_value = {
"status": "SUCCESS",
"output": "package:/data/app/com.android.chrome.apk=com.android.chrome\npackage:/data/app/com.android.settings.apk=com.android.settings",
}
device = RemoteAdbDevice(serial="emulator-5554", bridge=mock_bridge)
output = device.shell("pm list packages -f")
assert "com.android.chrome" in output
mock_bridge.execute_sync.assert_called_once_with("shell", {"command": "pm list packages -f"})
def test_remote_adb_device_current_app():
mock_bridge = MagicMock()
mock_bridge.execute_sync.return_value = {
"status": "SUCCESS",
"package": "com.android.chrome",
"activity": "com.google.android.apps.chrome.Main",
}
device = RemoteAdbDevice(serial="emulator-5554", bridge=mock_bridge)
app = device.current_app()
assert app is not None
assert app.package == "com.android.chrome"
assert app.activity == "com.google.android.apps.chrome.Main"
def test_remote_adb_device_window_size():
mock_bridge = MagicMock()
mock_bridge.execute_sync.return_value = {"status": "SUCCESS", "width": 1440, "height": 3120}
device = RemoteAdbDevice(serial="emulator-5554", bridge=mock_bridge)
size = device.window_size()
assert size == (1440, 3120)
def test_platform_commands_parity_with_remote_device(monkeypatch):
monkeypatch.setenv("ARTEMIS_CLOUD_MODE", "1")
monkeypatch.setenv("ARTEMIS_CLOUD_SESSION_ID", "session_parity_001")
monkeypatch.setenv("ADB_DEVICE_SERIAL", "emulator-5554")
mock_bridge = MagicMock()
mock_bridge.execute_sync.side_effect = [
# 1. date
{"status": "SUCCESS", "output": "Thu Aug 6 12:00:00 UTC 2026\n"},
# 2. pm list packages
{
"status": "SUCCESS",
"output": "package:/data/app/com.example.app1.apk=com.example.app1\npackage:/data/app/com.example.app2.apk=com.example.app2",
},
# 3. current_app
{"status": "SUCCESS", "package": "com.example.app1", "activity": ".MainActivity"},
]
client = RemoteAdbClient(bridge=mock_bridge)
device = client.device("emulator-5554")
ctx = ArtemisContext(
device=DeviceContext(
host_platform="LINUX",
mobile_platform=DevicePlatform.ANDROID,
device_id="emulator-5554",
device_width=1080,
device_height=2400,
),
adb_client=client,
)
dev = get_adb_device(ctx)
assert dev.serial == "emulator-5554"
# Verify real dynamic date
date_str = get_device_date(ctx)
assert "2026" in date_str
# Verify real dynamic package listing (Zero hardcoding)
pkgs = list_packages(ctx)
assert pkgs == "com.example.app1\ncom.example.app2"
# Verify real dynamic foreground package detection (Zero retry loop)
fg_pkg = get_current_foreground_package(ctx)
assert fg_pkg == "com.example.app1"
def test_remote_uiautomator_client_parity():
"""Verify RemoteUIAutomatorClient correctly returns structured UIAutomatorScreenData."""
from cloud_service.virtualization.remote_adb import RemoteUIAutomatorClient
mock_bridge = MagicMock()
mock_bridge.execute_sync.return_value = {
"status": "SUCCESS",
"base64": "fake_b64",
"hierarchy_xml": "<hierarchy><node text='Search' bounds='[100,200][300,400]'/></hierarchy>",
"elements": [{"text": "Search", "bounds": "[100,200][300,400]"}],
"width": 1080,
"height": 2400,
}
client = RemoteUIAutomatorClient(bridge=mock_bridge)
b64, xml, elements = client.get_screen_data()
assert b64 == "fake_b64"
assert "<node text='Search'" in xml
assert len(elements) == 1
assert elements[0]["text"] == "Search"
def test_controller_factory_cloud_mode_uses_unified_controller(monkeypatch):
"""Verify controller_factory creates UnifiedMobileController in Cloud Mode."""
monkeypatch.setenv("ARTEMIS_CLOUD_MODE", "1")
monkeypatch.setenv("ARTEMIS_CLOUD_SESSION_ID", "test_cloud_session")
monkeypatch.setenv("ADB_DEVICE_SERIAL", "emulator-5554")
from artemis.controllers.controller_factory import create_device_controller
from artemis.controllers.unified_controller import UnifiedMobileController
ctx = ArtemisContext(
device=DeviceContext(
host_platform="LINUX",
mobile_platform=DevicePlatform.ANDROID,
device_id="emulator-5554",
device_width=1080,
device_height=2400,
)
)
controller = create_device_controller(ctx)
assert isinstance(controller, UnifiedMobileController)
assert controller.driver.device_id == "emulator-5554"