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:
1 parent
78e478f502
commit
9a5aaf99b7
249 files changed
+37226
-9339
No files matched your search
@@ -0,0 +1,42 @@
|
||||
# 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 CascadingConfigEngine and variable interpolation."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from artemis.config.core import CascadingConfigEngine, interpolate_config_value
|
||||
|
||||
|
||||
def test_interpolate_env_variables():
|
||||
"""Verify ${env:KEY} expressions are properly interpolated."""
|
||||
with patch.dict("os.environ", {"TEST_KEY": "secret_123"}):
|
||||
result = interpolate_config_value("Bearer ${env:TEST_KEY}")
|
||||
assert result == "Bearer secret_123"
|
||||
|
||||
default_result = interpolate_config_value("${env:MISSING_KEY:-fallback_val}")
|
||||
assert default_result == "fallback_val"
|
||||
|
||||
|
||||
def test_interpolate_path_variables():
|
||||
"""Verify ${path:KEY} expressions resolve system standard paths."""
|
||||
result = interpolate_config_value("${path:temp_dir}/screenshots")
|
||||
assert "/screenshots" in result or "\\screenshots" in result
|
||||
|
||||
|
||||
def test_cascading_config_merge():
|
||||
"""Verify CLI overrides take precedence in CascadingConfigEngine."""
|
||||
engine = CascadingConfigEngine()
|
||||
merged = engine.load_raw_config(cli_overrides={"default": {"model": "gemini-test-custom"}})
|
||||
assert merged.get("default", {}).get("model") == "gemini-test-custom"
|
||||
@@ -0,0 +1,187 @@
|
||||
# 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 Artemis System Diagnostics & Readiness Engine."""
|
||||
|
||||
import pytest
|
||||
from artemis.core.diagnostics.engine import ReadinessEngine
|
||||
from artemis.core.diagnostics.probes.adb_probe import AdbDeviceProbe
|
||||
from artemis.core.diagnostics.probes.credentials_probe import (
|
||||
LLMCredentialsProbe,
|
||||
VisionOCRProbe,
|
||||
)
|
||||
from artemis.core.diagnostics.probes.runtime_probe import (
|
||||
PythonRuntimeProbe,
|
||||
SystemConfigProbe,
|
||||
)
|
||||
from artemis.core.diagnostics.probes.toolchain_probe import ToolchainProbe
|
||||
from artemis.core.diagnostics.schema import (
|
||||
ProbeCategory,
|
||||
ProbeResult,
|
||||
ProbeStatus,
|
||||
SystemReadinessReport,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readiness_engine_run_all():
|
||||
"""Verify that ReadinessEngine aggregates probes and builds structured report."""
|
||||
engine = ReadinessEngine()
|
||||
report: SystemReadinessReport = await engine.run_all()
|
||||
|
||||
assert isinstance(report, SystemReadinessReport)
|
||||
assert report.blocker_count >= 3
|
||||
assert isinstance(report.probes, list)
|
||||
assert len(report.probes) >= 5
|
||||
|
||||
probe_ids = [p.id for p in report.probes]
|
||||
assert "python_runtime" in probe_ids
|
||||
assert "system_config" in probe_ids
|
||||
assert "android_adb" in probe_ids
|
||||
assert "gemini_api_key" in probe_ids
|
||||
assert "vision_ocr_key" in probe_ids
|
||||
assert "toolchain" in probe_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_python_runtime_probe_structure():
|
||||
"""Verify PythonRuntimeProbe returns valid runtime inspection result."""
|
||||
probe = PythonRuntimeProbe()
|
||||
assert probe.probe_id == "python_runtime"
|
||||
assert probe.category == ProbeCategory.RUNTIME
|
||||
assert probe.is_blocker is True
|
||||
|
||||
result: ProbeResult = await probe.probe()
|
||||
assert isinstance(result, ProbeResult)
|
||||
assert result.status in (ProbeStatus.PASS, ProbeStatus.FAIL)
|
||||
assert "version" in result.metadata
|
||||
assert "executable" in result.metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_system_config_probe_structure():
|
||||
"""Verify SystemConfigProbe checks configuration file health."""
|
||||
probe = SystemConfigProbe()
|
||||
assert probe.probe_id == "system_config"
|
||||
assert probe.category == ProbeCategory.RUNTIME
|
||||
assert probe.is_blocker is True
|
||||
|
||||
result: ProbeResult = await probe.probe()
|
||||
assert isinstance(result, ProbeResult)
|
||||
assert result.status in (ProbeStatus.PASS, ProbeStatus.FAIL)
|
||||
assert "valid" in result.metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vision_ocr_probe_structure():
|
||||
"""Verify VisionOCRProbe returns optional non-blocker status."""
|
||||
probe = VisionOCRProbe()
|
||||
assert probe.probe_id == "vision_ocr_key"
|
||||
assert probe.category == ProbeCategory.CREDENTIALS
|
||||
assert probe.is_blocker is False
|
||||
|
||||
result: ProbeResult = await probe.probe()
|
||||
assert isinstance(result, ProbeResult)
|
||||
assert result.status == ProbeStatus.PASS
|
||||
assert "configured" in result.metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_adb_probe_structure():
|
||||
"""Verify AdbDeviceProbe returns correct category, blocker status, and schema."""
|
||||
probe = AdbDeviceProbe()
|
||||
assert probe.probe_id == "android_adb"
|
||||
assert probe.category == ProbeCategory.DEVICE
|
||||
assert probe.is_blocker is True
|
||||
|
||||
result: ProbeResult = await probe.probe()
|
||||
assert isinstance(result, ProbeResult)
|
||||
assert result.status in (ProbeStatus.PASS, ProbeStatus.WARN, ProbeStatus.FAIL)
|
||||
assert "installed" in result.metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_credentials_probe_structure():
|
||||
"""Verify LLMCredentialsProbe returns correct category and schema."""
|
||||
probe = LLMCredentialsProbe()
|
||||
assert probe.probe_id == "gemini_api_key"
|
||||
assert probe.category == ProbeCategory.CREDENTIALS
|
||||
assert probe.is_blocker is True
|
||||
|
||||
result: ProbeResult = await probe.probe()
|
||||
assert isinstance(result, ProbeResult)
|
||||
assert result.status in (ProbeStatus.PASS, ProbeStatus.FAIL)
|
||||
assert "configured_count" in result.metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_toolchain_probe_structure():
|
||||
"""Verify ToolchainProbe returns valid probe category, metadata, and schema."""
|
||||
probe = ToolchainProbe()
|
||||
assert probe.probe_id == "toolchain"
|
||||
assert probe.category == ProbeCategory.TOOLCHAIN
|
||||
assert probe.is_blocker is False
|
||||
|
||||
result: ProbeResult = await probe.probe()
|
||||
assert isinstance(result, ProbeResult)
|
||||
assert result.status in (ProbeStatus.PASS, ProbeStatus.FAIL)
|
||||
assert "ffmpeg" in result.metadata
|
||||
assert "scrcpy" in result.metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_active_device_selection():
|
||||
"""Verify selecting a device updates engine state."""
|
||||
engine = ReadinessEngine()
|
||||
engine.set_active_device_serial("test-emulator-1234")
|
||||
assert engine.get_active_device_serial() == "test-emulator-1234"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_credentials_probe_and_dynamic_update():
|
||||
"""Verify dynamic API key updates and metadata reflection."""
|
||||
from artemis.config import settings
|
||||
|
||||
settings.set_api_key("google", "test_gemini_key_1234567890", persist_to_env=False)
|
||||
|
||||
probe = LLMCredentialsProbe()
|
||||
result = await probe.probe()
|
||||
assert result.status == ProbeStatus.PASS
|
||||
assert "current_key" in result.metadata
|
||||
assert result.metadata["current_key"] == "test_gemini_key_1234567890"
|
||||
assert "api_keys" in result.metadata
|
||||
assert result.metadata["api_keys"]["google"] == "test_gemini_key_1234567890"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emulator_manager_lifecycle():
|
||||
"""Verify EmulatorManager status querying, validation, and dismissal."""
|
||||
from artemis.core.diagnostics.emulator_manager import (
|
||||
EmulatorLaunchStage,
|
||||
EmulatorManager,
|
||||
)
|
||||
|
||||
manager = EmulatorManager()
|
||||
status = manager.get_status()
|
||||
assert status.status == EmulatorLaunchStage.IDLE
|
||||
|
||||
# Invalid empty AVD name
|
||||
empty_res = await manager.launch(" ")
|
||||
assert empty_res.status == EmulatorLaunchStage.FAILED
|
||||
assert "empty" in (empty_res.error or "").lower()
|
||||
|
||||
# Dismiss state
|
||||
dismiss_res = manager.dismiss()
|
||||
assert dismiss_res["success"] is True
|
||||
assert manager.get_status().status == EmulatorLaunchStage.IDLE
|
||||
@@ -0,0 +1,78 @@
|
||||
# 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 Platform Abstraction Layer (PAL)."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from artemis.platform import (
|
||||
DarwinPlatform,
|
||||
LinuxPlatform,
|
||||
OSType,
|
||||
WindowsPlatform,
|
||||
get_platform,
|
||||
)
|
||||
|
||||
|
||||
def test_platform_singleton():
|
||||
"""Verify global platform singleton is initialized and conforms to protocol."""
|
||||
p = get_platform()
|
||||
assert p is not None
|
||||
assert p.os_type in (OSType.LINUX, OSType.MACOS, OSType.WINDOWS)
|
||||
assert p.paths.config_dir is not None
|
||||
assert p.paths.data_dir is not None
|
||||
assert p.paths.temp_dir() is not None
|
||||
assert p.process.path_separator in (":", ";")
|
||||
|
||||
|
||||
def test_linux_platform_paths():
|
||||
"""Verify Linux platform paths conform to XDG Base Directory specification."""
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{"XDG_CONFIG_HOME": "/tmp/test_xdg_config", "XDG_DATA_HOME": "/tmp/test_xdg_data"},
|
||||
clear=False,
|
||||
):
|
||||
linux_p = LinuxPlatform()
|
||||
assert linux_p.os_type == OSType.LINUX
|
||||
assert linux_p.paths.config_dir == Path("/tmp/test_xdg_config/artemis")
|
||||
assert linux_p.paths.data_dir == Path("/tmp/test_xdg_data/artemis")
|
||||
assert linux_p.process.path_separator == ":"
|
||||
|
||||
|
||||
def test_darwin_platform_paths():
|
||||
"""Verify macOS platform paths adhere to Apple Library standards."""
|
||||
darwin_p = DarwinPlatform()
|
||||
assert darwin_p.os_type == OSType.MACOS
|
||||
assert "Library/Application Support" in str(darwin_p.paths.config_dir)
|
||||
assert darwin_p.process.path_separator == ":"
|
||||
|
||||
|
||||
def test_windows_platform_paths():
|
||||
"""Verify Windows platform paths adhere to AppData standards."""
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"APPDATA": "C:\\Users\\Test\\AppData\\Roaming",
|
||||
"LOCALAPPDATA": "C:\\Users\\Test\\AppData\\Local",
|
||||
},
|
||||
clear=False,
|
||||
):
|
||||
with patch.object(Path, "mkdir", return_value=None):
|
||||
win_p = WindowsPlatform()
|
||||
assert win_p.os_type == OSType.WINDOWS
|
||||
assert str(win_p.paths.config_dir).startswith("C:\\Users\\Test\\AppData\\Roaming")
|
||||
assert str(win_p.paths.data_dir).startswith("C:\\Users\\Test\\AppData\\Local")
|
||||
assert win_p.process.path_separator == ";"
|
||||
assert win_p.get_install_command("adb") == "winget install Google.PlatformTools"
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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 ToolchainResolver and binary discovery."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from artemis.toolchain import ToolchainResolver, find_adb, find_ffmpeg, find_scrcpy
|
||||
|
||||
|
||||
def test_toolchain_env_override():
|
||||
"""Verify explicit environment variables override tool path resolution."""
|
||||
resolver = ToolchainResolver()
|
||||
fake_adb = Path("/tmp/fake_adb_binary")
|
||||
fake_adb.touch(exist_ok=True)
|
||||
|
||||
with patch.dict("os.environ", {"ARTEMIS_ADB_PATH": str(fake_adb)}):
|
||||
resolved = resolver.resolve("adb", force_refresh=True)
|
||||
assert resolved == str(fake_adb.resolve())
|
||||
|
||||
|
||||
def test_toolchain_helper_functions():
|
||||
"""Verify toolchain discovery helper functions return strings."""
|
||||
adb = find_adb()
|
||||
ffmpeg = find_ffmpeg()
|
||||
scrcpy = find_scrcpy()
|
||||
|
||||
assert isinstance(adb, str) and len(adb) > 0
|
||||
assert isinstance(ffmpeg, str) and len(ffmpeg) > 0
|
||||
assert isinstance(scrcpy, str) and len(scrcpy) > 0
|
||||
|
||||
|
||||
def test_toolchain_is_installed_lookup():
|
||||
"""Verify is_installed check on known and unknown tools."""
|
||||
resolver = ToolchainResolver()
|
||||
assert resolver.is_installed("non_existent_tool_xyz123") is False
|
||||
Reference in new issue
Block a user