fix: prevent false timeout failures and release 1.2.2

This commit is contained in:
chenmingxuan committed 2026-09-28 19:11:16 +08:00
1 parent 1dc2b0c686
commit e1e26772d9
18 files changed
+189 -22

No files matched your search

@@ -420,6 +420,33 @@ async def test_model_timeout_from_runner_log_reaches_failed_task_and_report(isol
assert "超时" in reports.session_report(job["session_id"])["error"]
def test_optional_timeout_warnings_are_not_reported_as_task_failure(tmp_path):
(tmp_path / "stdout.log").write_text(
"⚠ Could not clean unowned screen wake locks on emulator-5554: "
"Timed out acquiring the Artemis awake-lease mutex.\n"
"⚠ VisualStepSummarizer: Error generating summary for step 3: \n"
"ℹ StepMemoryService: Retrying Step 3 (attempt 4) after 1.0s\n",
encoding="utf-8",
)
assert reports.failure_reason(tmp_path, "failed", None) == (
"任务未完成,运行日志中没有可提取的具体异常;请查看执行步骤。"
)
(tmp_path / "stderr.log").write_text(
"TimeoutError: model request timed out after 300 seconds\n", encoding="utf-8"
)
reason = reports.failure_reason(tmp_path, "failed", None)
assert "model request timed out" in reason
assert "awake-lease" not in reason
(tmp_path / "stderr.log").write_text(
"DeviceBusyError: device queue reservation was cancelled\n", encoding="utf-8"
)
assert reports.failure_reason(tmp_path, "failed", None) == (
"DeviceBusyError: device queue reservation was cancelled"
)
def test_reports_do_not_execute_formulas_or_invent_pass(isolated):
batch = service.create_batch(payload())
job = isolated.jobs(batch["id"])[0]
@@ -14,6 +14,7 @@
"""Unit tests for VisualStepSummarizer and Context Compressor in Flash profile."""
import asyncio
from unittest.mock import ANY, AsyncMock, Mock, call
from uuid import uuid4
import pytest
@@ -106,6 +107,29 @@ async def test_summarizer_retries_independently(mock_context):
assert attempt_count == 2
@pytest.mark.asyncio
async def test_visual_summary_uses_its_own_configured_timeout(mock_context):
summarizer = VisualStepSummarizer(mock_context, retry_limit=0, timeout_seconds=0.01)
async def slow_response(_messages):
await asyncio.sleep(1)
summarizer._llm = Mock()
summarizer._llm.ainvoke = AsyncMock(side_effect=slow_response)
summarizer.dispatch(
step_number=1,
action_name="click",
action_args={"target": [100, 100]},
pre_img_bytes=b"pre",
post_img_bytes=b"post",
exec_outcome="Clicked",
)
await summarizer.flush()
assert summarizer.has_failed(1)
assert summarizer._llm.ainvoke.await_count == 1
@pytest.mark.asyncio
async def test_summarizer_bounded_retry_marks_failed(mock_context):
"""Persistent failures exit after 1 + retry_limit attempts with an explicit failed state."""
+29
View File
@@ -13,6 +13,8 @@
# limitations under the License.
import json
from pathlib import Path
import time
import pytest
@@ -126,3 +128,30 @@ def test_migration_cleanup_never_drains_a_live_legacy_owner(monkeypatch):
cleanup._drain_references = lambda: pytest.fail("must not drain a live owner")
assert cleanup.cleanup_unowned_references() is False
def test_optional_cleanup_skips_a_busy_mutex_without_delaying_task_start(monkeypatch):
owner = ScreenAwakeLease("device-123")
owner.mutex_path.write_text(json.dumps(owner._payload()), encoding="utf-8")
cleanup = ScreenAwakeLease("device-123")
monkeypatch.setattr(cleanup, "_drain_references", lambda: pytest.fail("must not drain"))
started_at = time.monotonic()
assert cleanup.cleanup_unowned_references() is False
assert time.monotonic() - started_at < 2.0
def test_unremovable_stale_mutex_still_obeys_deadline(monkeypatch):
lease = ScreenAwakeLease("device-123")
lease.mutex_path.write_text(json.dumps({"pid": 999999}), encoding="utf-8")
original_unlink = Path.unlink
def refuse_mutex_unlink(path, *args, **kwargs):
if path == lease.mutex_path:
raise PermissionError("file is in use")
return original_unlink(path, *args, **kwargs)
monkeypatch.setattr(Path, "unlink", refuse_mutex_unlink)
with pytest.raises(TimeoutError, match="awake-lease mutex"):
with lease._mutex(wait_timeout_seconds=0.1):
pytest.fail("stale mutex must not be acquired")
+17
View File
@@ -1,3 +1,4 @@
import os
from unittest.mock import MagicMock, patch
from artemis.runtime.awake_service import (
@@ -37,6 +38,22 @@ def test_usb_policy_is_primary_when_android_reports_it_active(mock_run, lease_ty
assert not any("KEYCODE_UNKNOWN" in command for command in commands)
@patch.dict(os.environ, {"ARTEMIS_TASK_WORKER": "1"})
@patch("artemis.runtime.awake_service.ScreenAwakeLease")
@patch("artemis.runtime.awake_service.subprocess.run")
def test_queued_worker_does_not_run_optional_legacy_cleanup(mock_run, lease_type):
mock_run.side_effect = [
completed(),
completed(),
completed(),
completed("2\n"),
completed(" mStayOn=true\n"),
]
assert _configure_usb_stay_awake("device-123") == AWAKE_STRATEGY_USB
lease_type.assert_not_called()
@patch("artemis.runtime.awake_service.ScreenAwakeLease")
@patch("artemis.runtime.awake_service.subprocess.run")
def test_inactive_usb_policy_falls_back_to_effective_host_heartbeat(mock_run, _lease_type):