feat: release 2.2.0 with PC-only tests and execution reliability fixes
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.
This commit is contained in:
1 parent
fac4ccb760
commit
be6e8c2abd
79 files changed
+2263
-277
No files matched your search
@@ -113,8 +113,9 @@ async def test_input_text_clear_erases_then_types_without_reclearing(actuator, m
|
||||
res = await actuator.input_text("hello", (500, 300), clear_exist=True)
|
||||
assert res.ok
|
||||
actions = [h["action"] for h in actuator.action_history]
|
||||
# The controller's full clear runs as a shell keyevent batch on this driver.
|
||||
assert actions == ["tap", "execute_shell", "input_text"]
|
||||
assert actions == ["tap", "input_text", "input_text"]
|
||||
assert actuator.action_history[-2]["text"] == ""
|
||||
assert actuator.action_history[-2]["clear_existing"] is True
|
||||
assert actuator.action_history[-1]["clear_existing"] is False
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""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()
|
||||
@@ -180,6 +180,41 @@ def test_mobile_run_task_with_device_serial(temp_trace_env):
|
||||
assert status["device_serial"] == "pixel-11-pro-001"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["flash", "pro"])
|
||||
def test_pc_run_task_uses_daemon_without_android_validation(temp_trace_env, monkeypatch, model):
|
||||
monkeypatch.delenv("ARTEMIS_STANDALONE", raising=False)
|
||||
with (
|
||||
patch(
|
||||
"mcp_server.tools.task_runner.ensure_daemon_running",
|
||||
return_value=(True, "http://127.0.0.1:8000"),
|
||||
),
|
||||
patch(
|
||||
"mcp_server.tools.task_runner._validate_device_serial",
|
||||
side_effect=AssertionError("ADB must not be used"),
|
||||
) as validate,
|
||||
patch(
|
||||
"mcp_server.tools.task_runner.submit_task_to_daemon",
|
||||
side_effect=lambda **kwargs: {
|
||||
"status": "started",
|
||||
"tasks": [{"session_id": kwargs["session_id"]}],
|
||||
},
|
||||
) as submit,
|
||||
patch("mcp_server.tools.task_runner.subprocess.Popen") as spawn,
|
||||
):
|
||||
result = mobile_run_task(
|
||||
"Check desktop",
|
||||
model=model,
|
||||
device_serial="offline-phone",
|
||||
runtime_overrides={"execution_target": "computer"},
|
||||
)
|
||||
assert result["status"] == "pending"
|
||||
assert submit.call_args.kwargs["runtime_overrides"]["execution_target"] == "computer"
|
||||
assert submit.call_args.kwargs["device_serial"] == "computer:local"
|
||||
assert trace_store.read_status(result["trace_id"])["device_serial"] == "computer:local"
|
||||
validate.assert_not_called()
|
||||
spawn.assert_not_called()
|
||||
|
||||
|
||||
def test_mobile_run_task_dispatched_to_daemon(temp_trace_env, monkeypatch):
|
||||
monkeypatch.delenv("ARTEMIS_STANDALONE", raising=False)
|
||||
with (
|
||||
@@ -201,12 +236,46 @@ def test_mobile_run_task_dispatched_to_daemon(temp_trace_env, monkeypatch):
|
||||
|
||||
popen.assert_not_called()
|
||||
assert result["trace_id"] == "daemon-sid-1"
|
||||
assert result["status"] == "running"
|
||||
assert result["status"] == "pending"
|
||||
assert "enqueued via unified Artemis Daemon" in result["message"]
|
||||
assert "stdout_log" in result
|
||||
assert "stderr_log" in result
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lost_reply", [False, True])
|
||||
@pytest.mark.parametrize("worker_status", ["pending", "running", "completed", "failed"])
|
||||
def test_daemon_receipt_preserves_queue_and_worker_status(
|
||||
temp_trace_env, monkeypatch, lost_reply, worker_status
|
||||
):
|
||||
monkeypatch.delenv("ARTEMIS_STANDALONE", raising=False)
|
||||
queued = []
|
||||
|
||||
def submit(**kwargs):
|
||||
sid = kwargs["session_id"]
|
||||
assert trace_store.read_status(sid)["status"] == "pending"
|
||||
queued.append({"session_id": sid, "status": "pending"})
|
||||
if worker_status != "pending":
|
||||
trace_store.update_trace_status(sid, worker_status)
|
||||
return None if lost_reply else {"status": "started", "tasks": queued}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"mcp_server.tools.task_runner.ensure_daemon_running",
|
||||
return_value=(True, "http://127.0.0.1:8000"),
|
||||
),
|
||||
patch("mcp_server.tools.task_runner.submit_task_to_daemon", side_effect=submit),
|
||||
patch(
|
||||
"mcp_server.tools.task_runner.DeviceExecutionLock.get_queued_tasks",
|
||||
side_effect=lambda: queued,
|
||||
),
|
||||
patch("mcp_server.tools.task_runner.subprocess.Popen") as spawn,
|
||||
):
|
||||
result = mobile_run_task("Queue state regression", model="Pro")
|
||||
assert result["status"] == worker_status
|
||||
assert trace_store.read_status(result["trace_id"])["status"] == worker_status
|
||||
spawn.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("kwargs", "match"),
|
||||
[
|
||||
|
||||
Reference in new issue
Block a user