Files
chenmingxuan 2ddec53954
CI / Python 3.12 / ubuntu-latest (push) Canceled after 0s
CI / Python 3.12 / windows-latest (push) Canceled after 0s
CI / Frontend tests and production build (push) Canceled after 0s
CI / Build and smoke-test Python packages (push) Canceled after 0s
feat: add Chinese UI, Responses models, and mirrored Windows setup
Localize the web console and persist custom model and capability settings.
Use OpenAI Responses exclusively and move runtime limits into environment configuration.
Bootstrap uv and the virtual environment on Windows, support optional China mirrors,
and package source distributions without environments, build output, or credentials.

Update regression tests and repository quality checks for the new behavior.
2026-09-22 19:16:17 +08:00

208 lines
7.6 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.
import os
os.environ["ARTEMIS_USE_FILE_API"] = "false"
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
import json
from artemis.agents.explorer.explorer import Explorer
from artemis.context import ArtemisContext
from artemis.graph.state import State
MOCK_PROMPT_JSON = """{
"IDENTITY": "You are the agentic UI Explorer designed to identify objects, scan text, and extract UI attributes on a phone screen.",
"OPERATING PRINCIPLES": [
"Confidence-Driven Cost-Benefit Strategy: Maximize localization accuracy...",
"Tool Capabilities: You have three classes of tools...",
"Parallel vs Sequential Invocations: You are fully supported..."
]
}"""
@pytest.mark.asyncio
async def test_explorer_inspect_region():
import pathlib
import numpy as np
# Mock context
mock_ctx = MagicMock(spec=ArtemisContext)
mock_ctx.device = MagicMock()
mock_ctx.device.device_width = 1080
mock_ctx.device.device_height = 2400
mock_ctx.data_engine = MagicMock()
explorer = Explorer(mock_ctx)
explorer.width = 1080
explorer.height = 2400
explorer.screenshot_path = "/tmp/test_screenshot.jpg"
explorer.image_name = "test_image"
dummy_img = np.zeros((2400, 1080, 3), dtype=np.uint8)
with (
patch("cv2.imread", return_value=dummy_img) as mock_imread,
patch("cv2.resize", return_value=dummy_img) as mock_resize,
patch("cv2.imwrite", return_value=True) as mock_imwrite,
patch("pathlib.Path.mkdir") as mock_mkdir,
patch("glob.glob", return_value=[]),
patch("artemis.agents.explorer.perception_tools.settings") as mock_settings,
):
mock_settings.TRACES_PATH = pathlib.Path("/tmp/traces")
res = await explorer.exec_inspect_region(
x_min=100, y_min=200, x_max=300, y_max=400, zoom_factor=2.0
)
assert res["image_path"] is not None
assert "Inspected region coordinates" in res["text"]
mock_imread.assert_called_once_with("/tmp/test_screenshot.jpg")
# Verify resize calculations:
# px_start = 100 * 1080 / 1000 = 108
# py_start = 200 * 2400 / 1000 = 480
# px_end = 300 * 1080 / 1000 = 324
# py_end = 400 * 2400 / 1000 = 960
# width = 324 - 108 = 216
# height = 960 - 480 = 480
# new_width = 216 * 2.0 = 432
# new_height = 480 * 2.0 = 960
mock_resize.assert_called_once()
resize_args = mock_resize.call_args[0]
assert resize_args[1] == (432, 960)
mock_imwrite.assert_called_once()
@pytest.mark.asyncio
async def test_explorer_ask_perception_tool():
"""The perception bundle answers from the in-memory screen index.
No Data Engine lookup is involved: the text search, the coordinate
audit and the OCR inventory all read ``explorer.screen_index``.
"""
import numpy as np
from unittest.mock import mock_open
from artemis.agents.explorer.screen_index import ScreenIndex
mock_ctx = MagicMock(spec=ArtemisContext)
mock_ctx.data_engine = MagicMock()
mock_ctx.data_engine.base_dir = "/tmp/test_session"
explorer = Explorer(mock_ctx)
explorer.image_name = "dummy_hash"
explorer.screenshot_path = "/tmp/test_screenshot.jpg"
explorer.width = 1080
explorer.height = 2400
explorer.global_label_idx = 1
explorer.image_pool = {
"img_0": {
"path": "/tmp/test_screenshot.jpg",
"transform": {
"offset_x": 0.0,
"offset_y": 0.0,
"scale_x": 1.0,
"scale_y": 1.0,
},
"description": "Original complete screenshot",
}
}
explorer.screen_index = ScreenIndex.from_hierarchy(
[
{
"bounds": "[500,400][600,560]",
"text": "Settings",
"resource-id": "com.android.settings:id/btn_settings",
"class": "android.widget.Button",
"clickable": "true",
},
{
"bounds": "[100,100][200,200]",
"class": "android.widget.FrameLayout [OCR]",
"ocr_elements": [{"text": "Settings page", "bounds": "[100,100][200,200]"}],
},
],
1080,
2400,
)
# Mock _run_object_detection
mock_detector_res = {
"detected": [{"label": "profile icon", "point": [900, 100]}],
"failed": [],
}
# Mock draw_dots
mock_draw_dots = MagicMock()
with (
patch("artemis.agents.explorer.run_setup.StorageManager") as storage_cls,
patch(
"artemis.agents.explorer.perception_tools._run_object_detection",
return_value=mock_detector_res,
),
patch("artemis.agents.explorer.perception_tools.draw_dots", mock_draw_dots),
patch("artemis.utils.visualization.draw_dots", mock_draw_dots),
patch("pathlib.Path.mkdir"),
patch("glob.glob", return_value=[]),
patch("builtins.open", mock_open(read_data=b'{"templates": []}')),
patch("cv2.imread", return_value=np.zeros((2400, 1080, 3), dtype=np.uint8)),
):
result = await explorer.exec_ask_perception_tool(
search_query="Settings",
nx=550,
ny=200,
detect_queries=["profile icon"],
)
storage_cls.assert_not_called()
assert "Text Search Results are:" in result["text"]
assert "Coordinate Search Results are:" in result["text"]
assert "Object Detection Results are:" in result["text"]
# UI-tree text match -> X1, OCR text match -> O2 (both from the index)
assert "[X1] 'Settings' at [509,200]" in result["text"]
assert "[O2] 'Settings page' at [138,62]" in result["text"]
# Coordinate audit -> X3 with the innermost element's description
assert (
"Matched element at [550,200]: [X3] | 'Settings' class=android.widget.Button"
" id=com.android.settings:id/btn_settings source=ui-tree interactive"
) in result["text"]
# Object detection -> D4 (the label counter is shared and contiguous)
assert "[D4]" in result["text"]
# Element-backed labels are registered; detection points are not.
assert explorer.label_registry["X1"].text == "Settings"
assert explorer.label_registry["O2"].source == "ocr"
assert explorer.label_registry["X3"] is explorer.label_registry["X1"]
assert "D4" not in explorer.label_registry
assert len(result["image_paths"]) == 3
# Verify call arguments of draw_dots
assert mock_draw_dots.call_count == 3
# First call (XML/OCR Text search): green color
first_call = mock_draw_dots.call_args_list[0]
assert first_call.kwargs.get("color") == "green"
assert first_call.args[2] == ["X1", "O2"]
# Second call (XML Coordinate audit): blue color
second_call = mock_draw_dots.call_args_list[1]
assert second_call.kwargs.get("color") == "blue"
assert second_call.args[2] == ["X3"]
# Third call (Object detection): red color
third_call = mock_draw_dots.call_args_list[2]
assert third_call.kwargs.get("color") == "red"
assert third_call.args[2] == ["D4"]