Unify task scheduling and queue state, fix Android input and observation recovery, preserve screenshot paths, and match final review criteria accurately. Add collapsible copyable results and publish the complete 2.1.0 upgrade notes. Validation: 3189 backend tests, 326 frontend tests, 24 client tests, production build and quality gates.
97 lines
3.3 KiB
Python
97 lines
3.3 KiB
Python
"""Pro and Flash must judge clear versus fill against fresh input evidence."""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import pytest
|
|
|
|
from artemis.agents.validator.action_execution import exec_action
|
|
from artemis.mcp.action_executor import McpActionExecutor
|
|
from artemis.mcp.action_types import ActionResult, ObserveResult
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("mode", ["pro", "flash"])
|
|
@pytest.mark.parametrize(
|
|
("text", "clear", "observed", "success"),
|
|
[
|
|
("", True, "", True),
|
|
("", True, "AR", False),
|
|
("", True, None, False),
|
|
("new", True, "new", True),
|
|
("new", True, "", False),
|
|
("new", False, "oldnew", True),
|
|
("new", False, None, False),
|
|
("", False, "", True),
|
|
("", False, "old", True),
|
|
],
|
|
)
|
|
async def test_input_verification_matches_intent(mode, text, clear, observed, success):
|
|
ctx = SimpleNamespace(
|
|
data_engine=None, device=SimpleNamespace(device_width=100, device_height=100)
|
|
)
|
|
element = {
|
|
"class": "android.widget.EditText",
|
|
"text": observed,
|
|
"focused": True,
|
|
"bounds": "[0,0][100,100]",
|
|
"center": [50, 50],
|
|
}
|
|
session = Mock()
|
|
session.call = AsyncMock(return_value=ActionResult.success("input_text", "Dispatched"))
|
|
session.ui_hierarchy = AsyncMock(return_value=[element])
|
|
if mode == "pro":
|
|
action = {
|
|
"action": "focus_and_input_text",
|
|
"coordinates": [50, 50],
|
|
"text": text,
|
|
"clear_before_input": clear,
|
|
}
|
|
ok, _ = await exec_action(ctx, session, action)
|
|
else:
|
|
executor = McpActionExecutor(ctx, actuator=Mock())
|
|
executor._session_or_start = AsyncMock(return_value=session)
|
|
executor._observe_after_action = AsyncMock(
|
|
return_value=(ObserveResult(ok=True, elements=[element]), True)
|
|
)
|
|
result = await executor._execute_device_action(
|
|
"input_text",
|
|
"input_text",
|
|
{"text": text, "clear_exist": clear},
|
|
"input",
|
|
SimpleNamespace(latest_screenshot=None, indexed_elements=[]),
|
|
)
|
|
ok = result.status == "success"
|
|
assert ok is success
|
|
session.call.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("observation_ok", [True, False])
|
|
async def test_flash_cannot_confirm_clear_without_fresh_hierarchy(observation_ok):
|
|
ctx = SimpleNamespace(data_engine=None, device=None)
|
|
session = Mock()
|
|
session.call = AsyncMock(return_value=ActionResult.success("input_text", "Dispatched"))
|
|
executor = McpActionExecutor(ctx, actuator=Mock())
|
|
executor._session_or_start = AsyncMock(return_value=session)
|
|
executor._observe_after_action = AsyncMock(
|
|
return_value=(
|
|
ObserveResult(
|
|
ok=observation_ok,
|
|
hierarchy_ok=False,
|
|
elements=[{"focused": True, "input_value_state": "empty", "center": [1, 1]}],
|
|
),
|
|
True,
|
|
)
|
|
)
|
|
result = await executor._execute_device_action(
|
|
"input_text",
|
|
"input_text",
|
|
{"text": "", "clear_exist": True},
|
|
"clear",
|
|
SimpleNamespace(latest_screenshot=None, indexed_elements=[]),
|
|
)
|
|
assert result.status == "error"
|
|
assert result.metadata["input_verification"] == "unknown"
|
|
session.call.assert_awaited_once()
|