feat: settle mobile observations before summarizing steps
This commit is contained in:
1 parent
c818a09a49
commit
4465a2e199
12 files changed
+271
-9
No files matched your search
@@ -70,6 +70,12 @@ def test_new_install_uses_playones_endpoint_without_overriding_saved_url(monkeyp
|
||||
assert service.get_model_settings()["base_url"] == "https://saved.example/v1"
|
||||
|
||||
|
||||
def test_model_presets_include_flash_pro_and_luna_without_restricting_custom_names():
|
||||
presets = service.get_model_settings()["model_presets"]
|
||||
assert presets == ["gpt-5.6-luna", "gpt-6-luna", "mimo-v2.6-flash", "mimo-v2.6-pro"]
|
||||
assert service.ModelSettingsUpdate(model="custom/model-v2").model == "custom/model-v2"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url",
|
||||
[
|
||||
|
||||
@@ -60,20 +60,21 @@ REFRESHED = [_el(1, "Back", 100, 100), _el(2, "Wi-Fi toggle", 900, 300)]
|
||||
|
||||
|
||||
class _FakeSession:
|
||||
"""An action session whose observations replay a scripted list sequence."""
|
||||
"""An action session whose screen advances after each device action."""
|
||||
|
||||
started = True
|
||||
|
||||
def __init__(self, observations: list[list[dict]]):
|
||||
self.calls: list[tuple[str, dict]] = []
|
||||
self._observations = list(observations)
|
||||
self._observations = observations
|
||||
|
||||
async def call(self, name, args):
|
||||
self.calls.append((name, dict(args)))
|
||||
return ActionResult.success(name, f"{name} dispatched")
|
||||
|
||||
async def observe(self, settle_ms=0):
|
||||
elements = self._observations.pop(0) if self._observations else []
|
||||
index = min(len(self.calls) - 1, len(self._observations) - 1)
|
||||
elements = self._observations[index] if self._observations else []
|
||||
return ObserveResult(
|
||||
ok=True,
|
||||
elements_text="\n".join(f"[{e['index']}] {e['text']}" for e in elements),
|
||||
|
||||
@@ -468,6 +468,8 @@ async def test_dual_frame_dispatch_keeps_transition_prompt(mock_context):
|
||||
pre_img_bytes=b"pre",
|
||||
post_img_bytes=b"post",
|
||||
exec_outcome="executed",
|
||||
post_ui_elements_text="[1] Text: '存储空间' | Bounds: [0,0][100,100]",
|
||||
observation_settled=False,
|
||||
)
|
||||
await summarizer.flush()
|
||||
|
||||
@@ -477,6 +479,10 @@ async def test_dual_frame_dispatch_keeps_transition_prompt(mock_context):
|
||||
assert "Before/After screenshots" in system_text
|
||||
assert "--- [1] BEFORE ACTION SCREEN (Action Marked Visually in Red) ---" in human_text
|
||||
assert "--- [2] AFTER ACTION SCREEN ---" in human_text
|
||||
assert "--- [3] AFTER ACTION UI ELEMENTS ---" in human_text
|
||||
assert "Text: '存储空间'" in human_text
|
||||
assert "screen still changing" in human_text
|
||||
assert "hierarchy names the new page" in system_text
|
||||
assert "DECISION FRAME" not in human_text
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Page transitions are recaptured until screenshot and hierarchy agree."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from artemis.mcp.action_executor import McpActionExecutor
|
||||
from artemis.mcp.action_types import ObserveResult
|
||||
|
||||
|
||||
def _screen(tmp_path, name: str, *, reverse: bool = False) -> str:
|
||||
image = Image.new("L", (64, 64))
|
||||
image.putdata([255 - x * 4 if reverse else x * 4 for _y in range(64) for x in range(64)])
|
||||
path = tmp_path / name
|
||||
image.save(path, format="JPEG")
|
||||
return str(path)
|
||||
|
||||
|
||||
def _observation(path: str, page: str) -> ObserveResult:
|
||||
return ObserveResult(
|
||||
ok=True,
|
||||
screenshot_path=path,
|
||||
elements_text=f"[1] {page}",
|
||||
elements=[{"text": page, "resource_id": "page/title", "bounds": [0, 0, 64, 64]}],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_navigation_recaptures_when_image_lags_behind_hierarchy(tmp_path):
|
||||
home = _screen(tmp_path, "home.jpg")
|
||||
detail = _screen(tmp_path, "detail.jpg", reverse=True)
|
||||
session = Mock()
|
||||
session.observe = AsyncMock(
|
||||
side_effect=[
|
||||
_observation(home, "存储空间"),
|
||||
_observation(detail, "存储空间"),
|
||||
_observation(detail, "存储空间"),
|
||||
]
|
||||
)
|
||||
executor = McpActionExecutor(Mock(), actuator=Mock())
|
||||
|
||||
observed, settled = await executor._observe_after_action(
|
||||
session, "click", home, _observation(home, "设置首页").elements
|
||||
)
|
||||
|
||||
assert observed.screenshot_path == detail
|
||||
assert observed.elements_text == "[1] 存储空间"
|
||||
assert settled is True
|
||||
assert session.observe.await_count == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_navigation_waits_until_new_page_has_two_stable_frames(tmp_path):
|
||||
home = _screen(tmp_path, "home.jpg")
|
||||
detail = _screen(tmp_path, "detail.jpg", reverse=True)
|
||||
session = Mock()
|
||||
session.observe = AsyncMock(
|
||||
side_effect=[
|
||||
_observation(home, "设置首页"),
|
||||
_observation(home, "设置首页"),
|
||||
_observation(detail, "存储空间"),
|
||||
_observation(detail, "存储空间"),
|
||||
]
|
||||
)
|
||||
executor = McpActionExecutor(Mock(), actuator=Mock())
|
||||
|
||||
observed, settled = await executor._observe_after_action(
|
||||
session, "click", home, _observation(home, "设置首页").elements
|
||||
)
|
||||
|
||||
assert observed.screenshot_path == detail
|
||||
assert settled is True
|
||||
assert session.observe.await_count == 4
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_old_bitmap_with_new_hierarchy_is_never_marked_settled(tmp_path):
|
||||
home = _screen(tmp_path, "home.jpg")
|
||||
session = Mock()
|
||||
session.observe = AsyncMock(return_value=_observation(home, "存储空间"))
|
||||
executor = McpActionExecutor(Mock(), actuator=Mock())
|
||||
|
||||
observed, settled = await executor._observe_after_action(
|
||||
session, "click", home, _observation(home, "设置首页").elements
|
||||
)
|
||||
|
||||
assert observed.elements_text == "[1] 存储空间"
|
||||
assert settled is False
|
||||
assert session.observe.await_count == 4
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_only_action_keeps_single_observation(tmp_path):
|
||||
home = _screen(tmp_path, "home.jpg")
|
||||
session = Mock()
|
||||
session.observe = AsyncMock(return_value=_observation(home, "设置首页"))
|
||||
executor = McpActionExecutor(Mock(), actuator=Mock())
|
||||
|
||||
observed, settled = await executor._observe_after_action(session, "wait_for_delay", home)
|
||||
|
||||
assert observed.screenshot_path == home
|
||||
assert settled is True
|
||||
session.observe.assert_awaited_once_with(settle_ms=400)
|
||||
Reference in new issue
Block a user