"""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)