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
@@ -132,6 +132,39 @@ async def test_enqueued_task_keeps_its_adb_endpoint_snapshot():
|
||||
assert target.lock_key == f"{original.identity}/emulator-5554"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pc_queue_uses_desktop_resources_without_android_discovery():
|
||||
from artemis.plugins.models import PluginSelection
|
||||
|
||||
pinned = [PluginSelection(id="cua", version="1.0.0")]
|
||||
with (
|
||||
patch.object(TaskQueueService, "ensure_worker_running"),
|
||||
patch("artemis.plugins.store.PluginStore.pin", return_value=pinned),
|
||||
patch(
|
||||
"artemis.plugins.store.PluginStore.reserve", return_value=["desktop:local"]
|
||||
) as reserve,
|
||||
patch(
|
||||
"artemis.runtime.device_pool.device_pool.select_device_async", new_callable=AsyncMock
|
||||
) as select,
|
||||
patch(
|
||||
"artemis.runtime.device_pool.device_pool.validate_explicit_serial_async",
|
||||
new_callable=AsyncMock,
|
||||
) as validate,
|
||||
):
|
||||
result = await TaskQueueService.enqueue_tasks(
|
||||
["Check PC"],
|
||||
device_serial="offline-phone",
|
||||
runtime_overrides={"execution_target": "computer"},
|
||||
)
|
||||
item = result["tasks"][0]
|
||||
assert item["device_serial"] == "computer:local"
|
||||
assert item["plugin_resources"] == ["desktop:local"]
|
||||
assert item["runtime_overrides"]["plugins"][0]["version"] == "1.0.0"
|
||||
reserve.assert_called_once_with(item["session_id"], pinned, "computer")
|
||||
select.assert_not_awaited()
|
||||
validate.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("current_status", "returncode", "stopped", "expected"),
|
||||
[
|
||||
@@ -1008,6 +1041,63 @@ async def test_enqueue_tasks_deduplicates_by_session_id():
|
||||
assert res2["tasks"][0]["session_id"] == "sid-dedup-1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("targets", [("computer", "android"), ("android", "computer")])
|
||||
async def test_pc_and_android_dispatch_in_submission_order(monkeypatch, targets):
|
||||
monkeypatch.setenv("ARTEMIS_CONCURRENCY_MODE", "per_device")
|
||||
monkeypatch.setenv("ARTEMIS_MAX_CONCURRENT_TASKS", "8")
|
||||
monkeypatch.setattr(state, "active_runs", {})
|
||||
monkeypatch.setattr(TaskQueueService, "_run_tasks", set())
|
||||
monkeypatch.setattr(TaskQueueService, "_sync_control_states", lambda: None)
|
||||
execute = AsyncMock()
|
||||
monkeypatch.setattr(TaskQueueService, "_execute_task_item", execute)
|
||||
state.queue_items = [
|
||||
{
|
||||
"session_id": target,
|
||||
"status": "pending",
|
||||
"device_serial": target,
|
||||
"execution_target": "computer" if target == "computer" else "android",
|
||||
}
|
||||
for target in targets
|
||||
]
|
||||
first = state.queue_items[0]
|
||||
TaskQueueService._dispatch_pending_tasks()
|
||||
await asyncio.gather(*TaskQueueService._run_tasks)
|
||||
assert [item["status"] for item in state.queue_items] == ["running", "pending"]
|
||||
# A launched worker occupies the slot before process registration.
|
||||
TaskQueueService._dispatch_pending_tasks()
|
||||
assert execute.await_count == 1
|
||||
state.queue_items.remove(first)
|
||||
TaskQueueService._dispatch_pending_tasks()
|
||||
await asyncio.gather(*TaskQueueService._run_tasks)
|
||||
assert [call.args[0]["session_id"] for call in execute.await_args_list] == list(targets)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_serial_dispatch_does_not_bypass_blocked_queue_head(monkeypatch):
|
||||
monkeypatch.setattr(state, "active_runs", {})
|
||||
monkeypatch.setattr(TaskQueueService, "_run_tasks", set())
|
||||
monkeypatch.setattr(TaskQueueService, "_sync_control_states", lambda: None)
|
||||
monkeypatch.setattr(
|
||||
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.get_active_owners",
|
||||
lambda: {"external": MagicMock(resource_ids=("shared-desktop",))},
|
||||
)
|
||||
execute = AsyncMock()
|
||||
monkeypatch.setattr(TaskQueueService, "_execute_task_item", execute)
|
||||
state.queue_items = [
|
||||
{
|
||||
"session_id": "first",
|
||||
"status": "pending",
|
||||
"device_serial": "android",
|
||||
"plugin_resources": ["shared-desktop"],
|
||||
},
|
||||
{"session_id": "second", "status": "pending", "device_serial": "computer"},
|
||||
]
|
||||
TaskQueueService._dispatch_pending_tasks()
|
||||
assert [item["status"] for item in state.queue_items] == ["pending", "pending"]
|
||||
execute.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_manual_stop_of_one_run_does_not_pollute_concurrent_run():
|
||||
"""Stopping run A must not flip run B's terminal status or its payload.
|
||||
|
||||
Reference in new issue
Block a user