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

@@ -86,6 +86,28 @@ def test_healthy_helper_never_constructs_uiautomator(composite):
factory.assert_not_called()
def test_clear_reply_loss_never_replays_on_fallback_backend(composite):
client, helper, u2, factory, _ = composite
client.connect()
helper.clear_text.side_effect = HelperUnavailable("reply lost after clear")
with pytest.raises(HelperUnavailable):
client.clear_text()
helper.clear_text.assert_called_once_with()
factory.assert_not_called()
u2.clear_text.assert_not_called()
def test_clear_uses_the_backend_that_observed_the_field(composite):
client, helper, u2, _, clock = composite
helper.connect.side_effect = HelperUnavailable("offline")
client.connect()
clock["now"] = 100.0 # Helper recovery is due, but must wait for observation.
u2.clear_text.return_value = True
assert client.clear_text() is True
helper.clear_text.assert_not_called()
u2.clear_text.assert_called_once_with()
def test_connect_failure_degrades_to_uiautomator(composite):
client, helper, u2, factory, _ = composite
helper.connect.side_effect = HelperUnavailable("not installed")
@@ -12,6 +12,7 @@ from uiautomator2 import base, core
from artemis.clients import ui_automator_client
from artemis.clients.accessibility_client import HelperUnavailable
from artemis.clients.screen_client_factory import FallbackScreenClient
from artemis.runtime import observation_budget
@dataclass
@@ -128,6 +129,53 @@ def test_each_failed_launch_is_closed_before_retry_and_final_failure(server):
assert "3" in str(caught.value)
@pytest.mark.parametrize("helper_seconds", [0, 18, 59])
def test_helper_and_uiautomator_share_observation_deadline(server, monkeypatch, helper_seconds):
monkeypatch.setattr(observation_budget, "monotonic", server.clock.time)
helper = MagicMock()
def unavailable():
server.clock.sleep(helper_seconds)
raise HelperUnavailable("helper snapshot timed out")
helper.get_screen_data.side_effect = unavailable
client = FallbackScreenClient(
"isolated-startup-device",
helper=helper,
state_probe=lambda _serial: "device",
)
budget = observation_budget.ObservationBudget(deadline=60)
token = observation_budget.current_observation.set(budget)
try:
with pytest.raises(TimeoutError, match="预算耗尽"):
client.get_screen_data()
finally:
observation_budget.current_observation.reset(token)
client.disconnect()
assert server.clock.now == 60
assert len(server.connections) <= 2
assert all(c.close_calls == 1 for c in server.connections)
assert client.active_backend is None
def test_disconnect_during_connect_discards_and_stops_late_device(server, monkeypatch):
client = ui_automator_client.UIAutomatorClient("isolated-startup-device")
candidate = MagicMock()
def late_connection(_device_id):
client.disconnect(stop_server=True)
return candidate
factory = MagicMock(side_effect=late_connection)
monkeypatch.setattr(ui_automator_client, "_ManagedUIAutomatorDevice", factory)
with pytest.raises(RuntimeError, match="连接已取消"):
client.connect()
factory.assert_called_once()
candidate.stop_uiautomator.assert_called_once()
assert client._device is None
assert client._awake_strategy is None
def test_retry_can_connect_after_failed_launch_was_stopped(server):
server.modes = ["not_ready", "ready"]
client = ui_automator_client.UIAutomatorClient("isolated-startup-device")