113 lines
4.4 KiB
Python
113 lines
4.4 KiB
Python
"""Unavailable devices park exploration; unexecuted guidance survives the pause."""
|
|
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock, Mock
|
|
|
|
import pytest
|
|
|
|
from artemis.exploration import runtime
|
|
from artemis.graph.perception import perception_node
|
|
from artemis.graph.state import State
|
|
from artemis.runtime import task_control
|
|
|
|
|
|
class FakeLock:
|
|
device_id = "test-device"
|
|
description = "observation"
|
|
max_concurrency = None
|
|
concurrency_mode = "per_device"
|
|
ingress = "test"
|
|
lock_scope = None
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.released = False
|
|
|
|
def release(self):
|
|
self.released = True
|
|
|
|
def acquire(self, **kwargs):
|
|
pass
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_timeout_parks_without_consuming_guidance_then_fresh_observation_resumes(
|
|
tmp_path, monkeypatch
|
|
):
|
|
monkeypatch.setattr(task_control, "TRACES_PATH", tmp_path)
|
|
monkeypatch.setattr(task_control, "DeviceExecutionLock", FakeLock)
|
|
repository = Mock()
|
|
monkeypatch.setattr(runtime, "explorations", repository)
|
|
control = runtime.ExplorationControl("run", FakeLock())
|
|
ctx = MagicMock()
|
|
ctx.exploration_id = "conversation"
|
|
ctx.exploration_run_id = "run"
|
|
ctx.task_control = control
|
|
ctx.on_task_pause = AsyncMock()
|
|
ctx.on_task_resume = AsyncMock()
|
|
ctx.data_engine.base_dir = tmp_path / "run"
|
|
ctx.data_engine.current_step_id = "step"
|
|
ctx.data_engine.get_image_path.return_value = tmp_path / "frame.jpg"
|
|
controller = SimpleNamespace(
|
|
get_screen_data=AsyncMock(
|
|
side_effect=[
|
|
TimeoutError("设备屏幕观测超时(60 秒)"),
|
|
SimpleNamespace(
|
|
base64="ZHVtbXk=", elements=[], width=100, height=200, platform="android"
|
|
),
|
|
SimpleNamespace(
|
|
base64="ZnJlc2g=", elements=[], width=100, height=200, platform="android"
|
|
),
|
|
]
|
|
)
|
|
)
|
|
monkeypatch.setattr("artemis.graph.perception.UnifiedMobileController", lambda _: controller)
|
|
monkeypatch.setattr("artemis.graph.perception.is_ocr_configured", lambda: False)
|
|
task_control.add_instruction("run", "打开浏览器后等我指导", instruction_id="guidance")
|
|
running = asyncio.create_task(perception_node(State(initial_goal="explore"), ctx))
|
|
|
|
async def wait_for_pause():
|
|
while task_control.read_state("run").get("status") != "task_paused":
|
|
await asyncio.sleep(0.01)
|
|
|
|
try:
|
|
await asyncio.wait_for(wait_for_pause(), 2)
|
|
assert control.pause_reason == "device_observation"
|
|
assert control.device_lock.released
|
|
assert task_control.list_instructions("run")[0]["status"] == "pending"
|
|
await asyncio.sleep(0.05)
|
|
assert controller.get_screen_data.await_count == 1
|
|
assert not running.done()
|
|
repository.add_message.assert_called_once()
|
|
task_control.request_resume("run")
|
|
result = await asyncio.wait_for(running, 2)
|
|
assert result["injected_instruction"] == "打开浏览器后等我指导"
|
|
assert task_control.list_instructions("run")[0]["status"] == "consumed"
|
|
assert controller.get_screen_data.await_count == 3
|
|
assert result["operator_raw_data"]["screenshot_b64"] == "ZnJlc2g="
|
|
ctx.on_task_resume.assert_awaited_once()
|
|
assert task_control.read_state("run")["status"] == "running"
|
|
repository.finish_run.assert_not_called()
|
|
finally:
|
|
running.cancel()
|
|
await asyncio.gather(running, return_exceptions=True)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("error", [TimeoutError("capture"), ValueError("bad implementation")])
|
|
async def test_ordinary_pro_errors_are_not_parked(error):
|
|
ctx = SimpleNamespace(task_control=None)
|
|
controller = SimpleNamespace(get_screen_data=AsyncMock(side_effect=error))
|
|
with pytest.raises(type(error)):
|
|
await runtime.observe_or_pause(ctx, controller)
|
|
controller.get_screen_data.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_exploration_programming_error_is_not_hidden(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(task_control, "TRACES_PATH", tmp_path)
|
|
ctx = SimpleNamespace(task_control=runtime.ExplorationControl("run", FakeLock()))
|
|
controller = SimpleNamespace(get_screen_data=AsyncMock(side_effect=ValueError("bad code")))
|
|
with pytest.raises(ValueError, match="bad code"):
|
|
await runtime.observe_or_pause(ctx, controller)
|