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:
chenmingxuan committed 2026-10-27 01:13:41 +08:00
1 parent fac4ccb760
commit be6e8c2abd
79 files changed
+2263 -277

No files matched your search

+57 -39
View File
@@ -80,18 +80,14 @@ def test_same_serial_on_different_adb_endpoints_has_independent_locks():
)
first.acquire()
second.acquire()
try:
assert first.path != second.path
owners = DeviceExecutionLock.get_active_owners()
assert len(owners) == 2
assert {owner.lock_scope for owner in owners.values()} == {
"tcp:127.0.0.1:5037",
"tcp:127.0.0.1:5038",
}
with pytest.raises(DeviceBusyError, match="concurrency limit"):
second.acquire(blocking=False)
finally:
second.release()
first.release()
second.acquire(blocking=False)
second.release()
def test_device_lock_release_does_not_remove_replacement_owner():
@@ -185,28 +181,19 @@ def test_submission_reservations_preserve_order_before_workers_start():
thread.join(timeout=2.0)
def test_pending_reservation_at_queue_head_does_not_block_other_devices():
"""An unclaimed pending submission must not gate every device's queue head.
Regression: a reservation whose device is still "pending" used to be
counted into every device's FIFO queue, so one unclaimed head-of-queue
ticket blocked all devices at once. It must now park on at most one idle
candidate device (the first in sorted order) and leave the others
schedulable.
"""
def test_pending_reservation_at_queue_head_preserves_global_submission_order():
"""Unresolved earlier submissions retain their place across target devices."""
pending_ticket = DeviceExecutionLock.reserve("unclaimed submission")
# device-alpha is a known idle device (a queued submission targets it), so
# the pending ticket parks there; device-beta must stay schedulable.
alpha_ticket = DeviceExecutionLock.reserve("task for device alpha", device_id="device-alpha")
try:
beta = DeviceExecutionLock("device-beta", "task for device beta")
# Must acquire immediately even though the older pending ticket is at
# the head of the global queue directory.
beta.acquire(blocking=False)
beta.release()
with pytest.raises(DeviceBusyError, match="earlier queued task"):
beta.acquire(blocking=False)
finally:
DeviceExecutionLock.cancel_reservation(pending_ticket)
DeviceExecutionLock.cancel_reservation(alpha_ticket)
beta.acquire(blocking=False)
beta.release()
def test_pending_reservation_is_served_with_original_fifo_position_after_claim():
@@ -326,27 +313,58 @@ def test_active_owner_is_discoverable_and_can_be_annotated(monkeypatch):
owner_lock.release()
def test_multi_device_locks_can_run_concurrently():
lock_a = DeviceExecutionLock("emulator-5554", "task on device A")
lock_b = DeviceExecutionLock("pixel-9-test", "task on device B")
@pytest.mark.parametrize("targets", [("emulator-5554", "computer"), ("computer", "pixel-9-test")])
@pytest.mark.parametrize("legacy_mode", ["per_device", "parallel", "global"])
def test_all_targets_run_serially_even_with_legacy_parallel_options(
monkeypatch, targets, legacy_mode
):
monkeypatch.setenv("ARTEMIS_CONCURRENCY_MODE", legacy_mode)
monkeypatch.setenv("ARTEMIS_MAX_CONCURRENT_TASKS", "8")
lock_a = DeviceExecutionLock(
targets[0], "task A", concurrency_mode=legacy_mode, max_concurrency=0
)
lock_b = DeviceExecutionLock(
targets[1], "task B", concurrency_mode=legacy_mode, max_concurrency=8
)
lock_a.acquire()
try:
# Device B should be acquired without blocking since it's a different device
lock_b.acquire(blocking=False)
try:
active_owners = DeviceExecutionLock.get_active_owners()
assert "emulator-5554" in active_owners
assert "pixel-9-test" in active_owners
# A second task on device A should be blocked
lock_a_second = DeviceExecutionLock("emulator-5554", "second task on device A")
with pytest.raises(DeviceBusyError, match="task on device A"):
lock_a_second.acquire(blocking=False)
finally:
lock_b.release()
with pytest.raises(DeviceBusyError, match="task A"):
lock_b.acquire(blocking=False)
finally:
lock_a.release()
lock_b.acquire(blocking=False)
lock_b.release()
def test_shared_execution_lease_prevents_racing_admission():
"""Both schedulers can see an idle snapshot; only one may create a lease."""
first = DeviceExecutionLock("computer", "PC")
second = DeviceExecutionLock("android", "Android")
assert first._try_acquire_owner_lock()
try:
assert not second._try_acquire_owner_lock()
assert not second.path.exists()
finally:
first.release()
assert second._try_acquire_owner_lock()
second.release()
def test_same_session_nested_lease_does_not_wait_behind_other_tasks():
parent = DeviceExecutionLock("android", "parent", session_id="same-session")
nested = DeviceExecutionLock("computer", "nested", session_id="same-session")
parent.acquire()
earlier = DeviceExecutionLock.reserve("another task", device_id="computer")
try:
nested.acquire(blocking=False)
nested.release()
# Borrowing the shared resource must not delete the parent's lease.
challenger = DeviceExecutionLock("another-device", "challenger")
assert not challenger._try_acquire_owner_lock()
finally:
DeviceExecutionLock.cancel_reservation(earlier)
parent.release()
def test_global_concurrency_serial_mode_blocks_other_devices(monkeypatch):