feat: implement robust multi-tier text input architecture and ADB diagnostics
- Android Text Input & IME Normalization:
- Implement progressive 3-tier input architecture in AndroidAdbDriver and AndroidDeviceController:
- Tier 1 (Primary): Zero-dependency clipboard injection + KEYCODE_PASTE (immune to IME Pinyin interception, full Unicode/Emoji support, preserves multiline formatting).
- Tier 2 (Broadcast): Base64 broadcast injection when ADBKeyboard is active.
- Tier 3 (Fallback): Native ADB physical keyevent fallback.
- Add literal newline normalization (\r\n, \n, \r) to prevent JSON escape string corruption (e.g. '10086\n' -> '10086、n').
- Add set_clipboard method to UIAutomatorClient without auxiliary APK requirements.
- Diagnostics & System Probes:
- Add adb_keys diagnostics probe for ADB key auth checks.
- Integrate diagnostics into CLI doctor command.
- Admin Console & Showcase UI:
- Improve model service and system router in admin console.
- Enhance workspace stream view and session state in showcase UI.
This commit is contained in:
1 parent
42b8988661
commit
ea3708a98f
19 files changed
+785
-89
No files matched your search
@@ -0,0 +1,53 @@
|
||||
# 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 json
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from apps.admin_console.services.model_service import ModelService
|
||||
|
||||
|
||||
def test_get_active_model_info_pro_architecture():
|
||||
"""Verify that pro profile returns Pro architecture while keeping real LLM model."""
|
||||
info = ModelService.get_active_model_info("pro")
|
||||
assert info["name"] == "Pro"
|
||||
assert info["architecture"] == "ARTEMIS Pro"
|
||||
assert info["provider"] == "google"
|
||||
assert "id" in info
|
||||
|
||||
|
||||
def test_get_active_model_info_flash_architecture():
|
||||
"""Verify that flash profile returns Flash architecture."""
|
||||
info = ModelService.get_active_model_info("flash")
|
||||
assert info["name"] == "Flash"
|
||||
assert info["architecture"] == "ARTEMIS Flash"
|
||||
assert info["provider"] == "google"
|
||||
|
||||
|
||||
def test_resolve_session_profile_from_device_info():
|
||||
"""Verify profile resolution from device_info JSON."""
|
||||
row = {"device_info": json.dumps({"profile": "pro"})}
|
||||
assert ModelService.resolve_session_profile(row) == "pro"
|
||||
|
||||
row_flash = {"device_info": json.dumps({"profile": "flash"})}
|
||||
assert ModelService.resolve_session_profile(row_flash) == "flash"
|
||||
|
||||
|
||||
def test_resolve_session_profile_from_agent_names():
|
||||
"""Verify profile resolution from agent/trace names."""
|
||||
row = {"device_info": None}
|
||||
assert ModelService.resolve_session_profile(row, agent_names=["planner", "operator"]) == "pro"
|
||||
assert ModelService.resolve_session_profile(row, agent_names=["flashrunner"]) == "flash"
|
||||
@@ -0,0 +1,198 @@
|
||||
# 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 ADB RSA key inspection and automated self-healing."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from artemis.core.diagnostics.adb_keys import (
|
||||
AdbKeyStatus,
|
||||
get_adb_key_paths,
|
||||
heal_adb_keys,
|
||||
inspect_adb_keys,
|
||||
)
|
||||
from artemis.core.diagnostics.engine import ReadinessEngine
|
||||
from artemis.core.diagnostics.probes.adb_probe import AdbDeviceProbe
|
||||
from artemis.core.diagnostics.schema import DeviceInfo, ProbeStatus
|
||||
|
||||
|
||||
def test_get_adb_key_paths_default():
|
||||
"""Verify default ADB key paths are under ~/.android."""
|
||||
priv, pub = get_adb_key_paths()
|
||||
assert priv.name == "adbkey"
|
||||
assert pub.name == "adbkey.pub"
|
||||
assert ".android" in str(priv)
|
||||
|
||||
|
||||
def test_get_adb_key_paths_custom_env(monkeypatch, tmp_path):
|
||||
"""Verify custom ADB_VENDOR_KEYS environment variable is respected."""
|
||||
custom_key = tmp_path / "custom_adbkey"
|
||||
monkeypatch.setenv("ADB_VENDOR_KEYS", str(custom_key))
|
||||
|
||||
priv, pub = get_adb_key_paths()
|
||||
assert priv == custom_key
|
||||
assert pub == Path(f"{custom_key}.pub")
|
||||
|
||||
|
||||
def test_inspect_adb_keys_not_exist(monkeypatch, tmp_path):
|
||||
"""When keys do not exist yet, they are considered valid (will auto-generate on first connect)."""
|
||||
priv = tmp_path / "adbkey"
|
||||
pub = tmp_path / "adbkey.pub"
|
||||
monkeypatch.setattr(
|
||||
"artemis.core.diagnostics.adb_keys.get_adb_key_paths", lambda: (priv, pub)
|
||||
)
|
||||
|
||||
status = inspect_adb_keys()
|
||||
assert status.is_valid is True
|
||||
assert status.is_corrupted is False
|
||||
assert status.exists is False
|
||||
|
||||
|
||||
def test_inspect_adb_keys_zero_bytes(monkeypatch, tmp_path):
|
||||
"""When private key is 0-bytes, it must be detected as corrupted."""
|
||||
priv = tmp_path / "adbkey"
|
||||
pub = tmp_path / "adbkey.pub"
|
||||
priv.write_text("")
|
||||
pub.write_text("")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"artemis.core.diagnostics.adb_keys.get_adb_key_paths", lambda: (priv, pub)
|
||||
)
|
||||
|
||||
status = inspect_adb_keys()
|
||||
assert status.is_valid is False
|
||||
assert status.is_corrupted is True
|
||||
assert status.exists is True
|
||||
assert status.key_size_bytes == 0
|
||||
assert "0 bytes" in (status.error_reason or "")
|
||||
|
||||
|
||||
def test_inspect_adb_keys_invalid_pem_header(monkeypatch, tmp_path):
|
||||
"""When private key is non-empty but does not contain PEM headers, it is corrupted."""
|
||||
priv = tmp_path / "adbkey"
|
||||
pub = tmp_path / "adbkey.pub"
|
||||
priv.write_text("GARBAGE DATA WITHOUT PEM HEADER " * 10)
|
||||
pub.write_text("some pubkey")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"artemis.core.diagnostics.adb_keys.get_adb_key_paths", lambda: (priv, pub)
|
||||
)
|
||||
|
||||
status = inspect_adb_keys()
|
||||
assert status.is_valid is False
|
||||
assert status.is_corrupted is True
|
||||
assert "PEM" in (status.error_reason or "")
|
||||
|
||||
|
||||
def test_inspect_adb_keys_valid(monkeypatch, tmp_path):
|
||||
"""When private key has valid PEM structure and size, it is valid."""
|
||||
priv = tmp_path / "adbkey"
|
||||
pub = tmp_path / "adbkey.pub"
|
||||
priv.write_text(
|
||||
"-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...\n-----END PRIVATE KEY-----\n"
|
||||
)
|
||||
pub.write_text("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... user@host")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"artemis.core.diagnostics.adb_keys.get_adb_key_paths", lambda: (priv, pub)
|
||||
)
|
||||
|
||||
status = inspect_adb_keys()
|
||||
assert status.is_valid is True
|
||||
assert status.is_corrupted is False
|
||||
assert status.exists is True
|
||||
assert status.key_size_bytes > 0
|
||||
assert status.error_reason is None
|
||||
|
||||
|
||||
def test_heal_adb_keys_healthy_no_op(monkeypatch, tmp_path):
|
||||
"""When keys are healthy and force is False, heal_adb_keys is a no-op."""
|
||||
priv = tmp_path / "adbkey"
|
||||
pub = tmp_path / "adbkey.pub"
|
||||
priv.write_text(
|
||||
"-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...\n-----END PRIVATE KEY-----\n"
|
||||
)
|
||||
pub.write_text("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... user@host")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"artemis.core.diagnostics.adb_keys.get_adb_key_paths", lambda: (priv, pub)
|
||||
)
|
||||
|
||||
res = heal_adb_keys(force=False)
|
||||
assert res["success"] is True
|
||||
assert res["repaired"] is False
|
||||
|
||||
|
||||
def test_heal_adb_keys_corrupted(monkeypatch, tmp_path):
|
||||
"""When keys are corrupted (0 bytes), heal_adb_keys deletes them and restarts ADB."""
|
||||
priv = tmp_path / "adbkey"
|
||||
pub = tmp_path / "adbkey.pub"
|
||||
priv.write_text("")
|
||||
pub.write_text("")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"artemis.core.diagnostics.adb_keys.get_adb_key_paths", lambda: (priv, pub)
|
||||
)
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value.returncode = 0
|
||||
res = heal_adb_keys()
|
||||
|
||||
assert res["success"] is True
|
||||
assert res["repaired"] is True
|
||||
# The corrupted 0-byte file was removed
|
||||
assert not priv.exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_adb_probe_detects_corrupted_key_with_unauthorized_device(monkeypatch, tmp_path):
|
||||
"""When a device is unauthorized and keys are corrupted, probe reports ADB Key Corrupted."""
|
||||
priv = tmp_path / "adbkey"
|
||||
pub = tmp_path / "adbkey.pub"
|
||||
priv.write_text("")
|
||||
monkeypatch.setattr(
|
||||
"artemis.core.diagnostics.adb_keys.get_adb_key_paths", lambda: (priv, pub)
|
||||
)
|
||||
|
||||
probe = AdbDeviceProbe()
|
||||
monkeypatch.setattr(probe, "_locate_adb", lambda: "/usr/bin/adb")
|
||||
|
||||
async def mock_adb_ver(p):
|
||||
return "Android Debug Bridge version 1.0.41"
|
||||
|
||||
monkeypatch.setattr(probe, "_get_adb_version", mock_adb_ver)
|
||||
|
||||
async def mock_devices(adb_path):
|
||||
return [DeviceInfo(serial="TEST_SERIAL", state="unauthorized")]
|
||||
|
||||
monkeypatch.setattr(probe, "_parse_adb_devices", mock_devices)
|
||||
|
||||
result = await probe.probe()
|
||||
assert result.status == ProbeStatus.WARN
|
||||
assert result.summary == "ADB Key Corrupted"
|
||||
assert "0 bytes" in result.description
|
||||
assert any(a.label == "Auto-Heal ADB Keys" for a in result.actions)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readiness_engine_heal_adb_keys():
|
||||
"""Verify ReadinessEngine exposes heal_adb_keys async method."""
|
||||
engine = ReadinessEngine()
|
||||
with patch("artemis.core.diagnostics.adb_keys.heal_adb_keys") as mock_heal:
|
||||
mock_heal.return_value = {"success": True, "repaired": True, "message": "Repaired"}
|
||||
res = await engine.heal_adb_keys()
|
||||
assert res["success"] is True
|
||||
assert res["repaired"] is True
|
||||
Reference in new issue
Block a user