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
@@ -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