119 lines
4.0 KiB
Python
119 lines
4.0 KiB
Python
"""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_sequence_preserves_immediate_popup_evidence_without_claiming_stability(tmp_path):
|
|
popup = _observation(_screen(tmp_path, "popup.jpg"), "缩放面板")
|
|
session = Mock()
|
|
session.observe = AsyncMock(return_value=popup)
|
|
executor = McpActionExecutor(Mock(), actuator=Mock())
|
|
|
|
observed, settled = await executor._observe_after_action(session, "action_sequence", None)
|
|
|
|
assert observed is popup
|
|
assert settled is None
|
|
session.observe.assert_awaited_once_with(settle_ms=0)
|
|
|
|
|
|
@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)
|