feat: add multi-device concurrency, server lifecycle CLI, and unified execution stream
- Implement DevicePool and DeviceExecutionLock concurrency modes (per-device FIFO vs global serialization) - Extend ArtemisClient SDK and CLI with explicit device targeting and concurrency strategy configuration - Add server lifecycle management (artemis restart, stop, status) with cross-platform PID discovery - Update MCP server tools to support multi-device dispatch, trace tagging, and per-device execution - Enhance Showcase UI with step-by-step visual replay, playback speed controls, and device serial badges - Add comprehensive unit test coverage and edge-case verification scripts
This commit is contained in:
1 parent
a36a66ce6d
commit
c2c90a6378
84 files changed
+5612
-645
No files matched your search
@@ -64,3 +64,131 @@ async def test_artemis_client_stream_run():
|
||||
assert events[0].event_type == StreamEventType.STATUS
|
||||
assert events[0].payload.get("status") == "starting"
|
||||
assert len(collected_events) == len(events)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_device_specification():
|
||||
"""Verify target device can be set via init, setters, or per-task run parameters."""
|
||||
client = ArtemisClient(device_serial="emulator-5554", default_profile="flash")
|
||||
assert client.device_id == "emulator-5554"
|
||||
assert client.device_serial == "emulator-5554"
|
||||
|
||||
# Mutate via property setter
|
||||
client.device_serial = "pixel-9-pro"
|
||||
assert client.device_id == "pixel-9-pro"
|
||||
assert client.device_serial == "pixel-9-pro"
|
||||
|
||||
# Mutate via chaining method
|
||||
client.set_device("pixel-8")
|
||||
assert client.device_id == "pixel-8"
|
||||
|
||||
# Override device per run call using device_serial
|
||||
res1 = await client.run("Task on dev 1", device_serial="target-device-alpha")
|
||||
assert res1.device_id == "target-device-alpha"
|
||||
|
||||
# Override device per run call using device_id
|
||||
res2 = await client.run("Task on dev 2", device_id="target-device-beta")
|
||||
assert res2.device_id == "target-device-beta"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_stream_run_device_specification():
|
||||
"""Verify stream_run propagates custom target device."""
|
||||
client = ArtemisClient(device_id="default-stream-dev", default_profile="flash")
|
||||
events = []
|
||||
async for ev in client.stream_run("Test goal", device_serial="explicit-stream-dev"):
|
||||
events.append(ev)
|
||||
|
||||
assert len(events) >= 1
|
||||
assert events[0].payload.get("device_id") == "explicit-stream-dev"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_concurrency_mode_configuration():
|
||||
"""Verify concurrency_mode can be configured as 'global' or 'per_device'."""
|
||||
from artemis import ConcurrencyMode
|
||||
|
||||
client = ArtemisClient(concurrency_mode=ConcurrencyMode.GLOBAL)
|
||||
assert client.concurrency_mode == "global"
|
||||
|
||||
client.set_concurrency_mode(ConcurrencyMode.PER_DEVICE)
|
||||
assert client.concurrency_mode == "per_device"
|
||||
|
||||
client.concurrency_mode = "global"
|
||||
assert client.concurrency_mode == "global"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sdk_multi_device_parallel_execution():
|
||||
"""Verify per_device concurrency allows tasks on different devices to run concurrently."""
|
||||
import asyncio
|
||||
|
||||
client_a = ArtemisClient(device_id="mock-dev-parallel-1", concurrency_mode="per_device")
|
||||
client_b = ArtemisClient(device_id="mock-dev-parallel-2", concurrency_mode="per_device")
|
||||
|
||||
# Run tasks on two different devices concurrently
|
||||
task_a = asyncio.create_task(client_a.run("Task on device 1"))
|
||||
task_b = asyncio.create_task(client_b.run("Task on device 2"))
|
||||
|
||||
res_a, res_b = await asyncio.gather(task_a, task_b)
|
||||
assert res_a.status in ("completed", "running", "success")
|
||||
assert res_b.status in ("completed", "running", "success")
|
||||
assert res_a.device_id == "mock-dev-parallel-1"
|
||||
assert res_b.device_id == "mock-dev-parallel-2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sdk_global_concurrency_serializes_across_devices():
|
||||
"""Verify global concurrency restricts execution to 1 task across all devices."""
|
||||
import asyncio
|
||||
from artemis.runtime import DeviceBusyError, DeviceExecutionLock
|
||||
|
||||
# Pre-acquire a lock on device A with global concurrency mode
|
||||
lock_a = DeviceExecutionLock("mock-global-dev-1", "active global task", concurrency_mode="global")
|
||||
lock_a.acquire()
|
||||
|
||||
try:
|
||||
client_b = ArtemisClient(device_id="mock-global-dev-2", concurrency_mode="global")
|
||||
# Attempting non-blocking execution on device B should fail because global concurrency limit (1) is held by device A
|
||||
with pytest.raises(DeviceBusyError, match="Global task concurrency limit"):
|
||||
await client_b.run("Task on device 2", blocking=False)
|
||||
finally:
|
||||
lock_a.release()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sdk_run_task_with_device_and_concurrency():
|
||||
"""Verify Task object with device_serial and concurrency_mode executes via run_task."""
|
||||
client = ArtemisClient()
|
||||
task = Task(
|
||||
goal="Verify search functionality",
|
||||
profile="flash",
|
||||
device_serial="device-from-task-obj",
|
||||
concurrency_mode="per_device",
|
||||
)
|
||||
assert task.device_id == "device-from-task-obj"
|
||||
assert task.concurrency_mode == "per_device"
|
||||
|
||||
res = await client.run_task(task)
|
||||
assert res.device_id == "device-from-task-obj"
|
||||
assert res.status in ("completed", "running", "success")
|
||||
|
||||
|
||||
def test_builder_and_agent_concurrency_and_device_targeting():
|
||||
"""Verify AgentConfigBuilder and Agent support device targeting and concurrency configuration."""
|
||||
from artemis import Agent, Builders
|
||||
|
||||
# 1. Builder supports string for_device directly
|
||||
cfg1 = Builders.AgentConfig.for_device("emulator-9999").with_concurrency_mode("global").build()
|
||||
assert cfg1.device_id == "emulator-9999"
|
||||
assert cfg1.concurrency_mode == "global"
|
||||
|
||||
# 2. Builder supports for_device_serial
|
||||
cfg2 = Builders.AgentConfig.for_device_serial("pixel-test-serial").with_concurrency_mode("per_device").build()
|
||||
assert cfg2.device_id == "pixel-test-serial"
|
||||
assert cfg2.concurrency_mode == "per_device"
|
||||
|
||||
# 3. Agent supports device_serial and concurrency_mode directly in constructor
|
||||
agent = Agent(device_serial="pixel-constructor-dev", concurrency_mode="global")
|
||||
assert agent._config.device_id == "pixel-constructor-dev"
|
||||
assert agent._config.concurrency_mode == "global"
|
||||
Reference in new issue
Block a user