Files
Artemis-Eapil/tests/unit/mcp/test_action_observation_settling.py
T

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)