- 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.
199 lines
6.8 KiB
Python
199 lines
6.8 KiB
Python
# 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
|