fix: prevent false timeout failures and release 1.2.2
This commit is contained in:
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."""
|
||||
|
||||
@@ -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")
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user