feat: enhance daemon dispatching, device readiness fallback, and live streaming UI

- Admin Console & Task Queue: add cancellation, session startup progress endpoints, and support ingress metadata
- Diagnostics & Runtime: enhance ADB probe to prefer unlocked devices and synchronize with readiness engine
- MCP Server: dispatch tasks through Artemis Daemon scheduler with fallback to standalone runner, improve action overlays
- Showcase UI: improve session auto-selection with pinning support, render startup progress, and refine stream styling
- CLI & Makefile: expose server lifecycle commands (restart, stop, status)
- Configuration: ignore runtime server state file in .gitignore
This commit is contained in:
somew1nd committed 2026-08-26 17:41:23 -07:00
1 parent 57655266cc
commit 8fdecf23d7
33 files changed
+1315 -293

No files matched your search

@@ -59,6 +59,33 @@ async def test_cancelled_event_stream_reaps_queue_waiter():
assert _pending_queue_get_tasks() == queue_tasks_before
@pytest.mark.asyncio
async def test_stream_events_active_session_replay():
"""Verify stream_events with default 'active' replays running session info and steps."""
state.is_shutting_down = False
state.shutdown_event.clear()
state.active_session_id = "replay-sess-123"
state.current_goal = "Replay Goal"
state.current_profile = "flash"
with patch("apps.admin_console.database.repositories.step_repository.step_repo") as mock_step_repo:
mock_step_repo.get_session_steps.return_value = [
{"step_number": 1, "description": "Step 1", "status": "completed"}
]
response = await stream_events()
iterator = response.body_iterator
first_event = await anext(iterator)
assert "Subscribed to session active" in first_event
second_event = await anext(iterator)
assert "session_started" in second_event
assert "replay-sess-123" in second_event
third_event = await anext(iterator)
assert "step_recorded" in third_event
assert "Step 1" in third_event
await iterator.aclose()
@pytest.mark.asyncio
async def test_windows_sigint_is_ignored_while_task_is_active():
state.current_process = MagicMock(returncode=None)
@@ -74,3 +74,41 @@ async def test_run_task_enqueues_when_device_is_unlocked(monkeypatch):
assert result["status"] == "started"
enqueue_tasks.assert_awaited_once()
@pytest.mark.asyncio
async def test_run_task_switches_to_unlocked_device_and_syncs_engine(monkeypatch):
"""When default device is locked, run_task should enqueue on verified fallback and update engine."""
unlocked_probe = ProbeResult(
id="android_adb",
category=ProbeCategory.DEVICE,
title="Device / Emulator Connected",
status=ProbeStatus.PASS,
is_blocker=True,
summary="Connected",
description="Ready.",
metadata={
"installed": True,
"submission_probe": True,
"active_device": {"serial": "emulator-5556", "is_locked": False},
},
)
run_probe = AsyncMock(return_value=unlocked_probe)
enqueue_tasks = AsyncMock(return_value={"status": "started", "tasks": []})
monkeypatch.setattr(
tasks.readiness_engine, "run_device_submission_probe", run_probe
)
monkeypatch.setattr(tasks.task_queue_service, "enqueue_tasks", enqueue_tasks)
monkeypatch.setattr(tasks.readiness_engine, "get_active_device_serial", lambda: "device-locked-1")
set_active_mock = AsyncMock()
monkeypatch.setattr(tasks.readiness_engine, "set_active_device_serial", set_active_mock)
result = await tasks.run_task(RunRequest(goal="Run on any ready device"))
assert result["status"] == "started"
enqueue_tasks.assert_awaited_once()
# Check that device_serial passed to enqueue_tasks is the verified unlocked device
_, kwargs = enqueue_tasks.call_args
assert kwargs.get("device_serial") == "emulator-5556"
set_active_mock.assert_called_once_with("emulator-5556")
@@ -607,3 +607,198 @@ def test_darwin_terminate_process_tree_preserves_direct_child_for_asyncio():
for call_args in mock_wait_procs.call_args_list:
procs_waited = call_args[0][0]
assert parent not in procs_waited
@pytest.mark.asyncio
async def test_enqueue_tasks_unified_ingress():
"""Verify enqueue_tasks correctly propagates ingress, custom session_id, and conversation_id."""
with patch.object(TaskQueueService, "ensure_worker_running"):
res = await task_queue_service.enqueue_tasks(
["Test unified goal"],
profile="flash",
ingress="mcp",
session_id="custom-mcp-session-123",
conversation_id="conv-456",
)
assert res["status"] in ("started", "queued")
assert len(res["tasks"]) == 1
task = res["tasks"][0]
assert task["session_id"] == "custom-mcp-session-123"
assert task["ingress"] == "mcp"
assert task["conversation_id"] == "conv-456"
assert task["goal"] == "Test unified goal"
@pytest.mark.asyncio
async def test_queue_worker_notifies_conversation():
"""Verify queue_worker calls notify() when conversation_id is attached to task."""
executed_cmds = []
async def fake_subprocess_exec(*args, **kwargs):
executed_cmds.append(list(args))
proc = MagicMock()
proc.pid = 99999
proc.wait = AsyncMock(return_value=0)
proc.returncode = 0
return proc
with (
patch("asyncio.create_subprocess_exec", side_effect=fake_subprocess_exec),
patch("apps.admin_console.services.task_queue_service.media_service"),
patch("mcp_server.notifiers.notify") as mock_notify,
patch("apps.admin_console.services.task_queue_service.session_repo") as mock_repo,
):
mock_repo.get_running_session_id.return_value = None
await task_queue_service.enqueue_tasks(
["Notify goal"],
profile="flash",
ingress="mcp",
conversation_id="conv-789",
device_serial="test-mock-serial",
)
for _ in range(30):
if mock_notify.called and len(state.queue_items) == 0:
break
await asyncio.sleep(0.05)
mock_notify.assert_called_once()
kwargs = mock_notify.call_args[1]
assert kwargs["conversation_id"] == "conv-789"
assert "Notify goal" in kwargs["message"]
task = state.worker_task
if task and not task.done():
task.cancel()
try:
await task
except (asyncio.CancelledError, Exception):
pass
def test_stop_tasks_by_session_id_targets_correct_task_among_multiple():
"""Stopping by session_id terminates only the targeted owner among multiple concurrent devices."""
owner_a = DeviceLockOwner(
pid=11111,
process_created_at=100.0,
token="token-a",
device_id="device-a",
description="Task on device A",
acquired_at="2026-08-24T00:00:00+00:00",
session_id="session-a",
ingress="cli",
)
owner_b = DeviceLockOwner(
pid=22222,
process_created_at=200.0,
token="token-b",
device_id="device-b",
description="Task on device B",
acquired_at="2026-08-24T00:00:00+00:00",
session_id="session-b",
ingress="mcp",
)
with (
patch(
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.get_active_owners",
return_value={"device_a": owner_a, "device_b": owner_b},
),
patch(
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.is_active_owner",
return_value=True,
),
patch(
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.cleanup_stale_locks"
) as cleanup_locks,
patch(
"apps.admin_console.services.task_queue_service.process_supervisor.terminate_tree_verified",
return_value=True,
) as terminate_verified,
patch(
"apps.admin_console.services.task_queue_service.session_repo.update_session_status"
) as update_status,
patch("mcp_server.utils.trace_store.update_trace_status") as update_trace,
):
# Explicitly stop session-b
assert task_queue_service.stop_tasks(clear_all=False, session_id="session-b") is True
# Only session-b's process (22222) should be terminated
terminate_verified.assert_called_once_with(22222, 200.0)
cleanup_locks.assert_called_once_with("device-b")
update_status.assert_called_once()
assert update_status.call_args[0][0] == "session-b"
assert update_status.call_args[0][1] == "cancelled"
update_trace.assert_called_once_with(
"session-b",
"cancelled",
error="Task stopped from the Artemis frontend.",
)
def test_stop_tasks_by_device_id_targets_specific_device():
"""Stopping by device_id targets the active task on that specific device."""
owner_a = DeviceLockOwner(
pid=33333,
process_created_at=300.0,
token="token-a",
device_id="pixel-8",
description="Task on Pixel 8",
acquired_at="2026-08-24T00:00:00+00:00",
session_id="session-pixel8",
ingress="sdk",
)
with (
patch(
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.get_active_owners",
return_value={"pixel_8": owner_a},
),
patch(
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.is_active_owner",
return_value=True,
),
patch("apps.admin_console.services.task_queue_service.DeviceExecutionLock.cleanup_stale_locks"),
patch(
"apps.admin_console.services.task_queue_service.process_supervisor.terminate_tree_verified",
return_value=True,
) as terminate_verified,
patch("apps.admin_console.services.task_queue_service.session_repo.update_session_status"),
):
assert task_queue_service.stop_tasks(clear_all=False, device_id="pixel-8") is True
terminate_verified.assert_called_once_with(33333, 300.0)
def test_stop_tasks_queued_item_by_session_id():
"""Stopping a queued task by session_id cancels its reservation and removes it from queue."""
state.queue_items = [
{"session_id": "queue-1", "goal": "Goal 1", "status": "pending", "queue_ticket": "ticket-1"},
{"session_id": "queue-2", "goal": "Goal 2", "status": "pending", "queue_ticket": "ticket-2"},
]
with (
patch(
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.get_active_owners",
return_value={},
),
patch(
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.get_active_owner",
return_value=None,
),
patch(
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.cancel_reservation",
return_value=True,
) as cancel_res,
patch("apps.admin_console.services.task_queue_service.session_repo.update_session_status") as update_status,
):
assert task_queue_service.stop_tasks(clear_all=False, session_id="queue-1") is True
cancel_res.assert_called_once_with("ticket-1")
# queue-2 should remain in queue
assert len(state.queue_items) == 1
update_status.assert_called_once()
assert update_status.call_args[0][0] == "queue-1"
assert update_status.call_args[0][1] == "cancelled"
+49
View File
@@ -313,6 +313,55 @@ async def test_submission_probe_fails_closed_when_lock_state_is_unknown(monkeypa
assert result.status == ProbeStatus.WARN
@pytest.mark.asyncio
async def test_submission_probe_falls_back_to_unlocked_device(monkeypatch):
"""When the first device is locked but a second device is unlocked, submission probe falls back."""
probe = AdbDeviceProbe()
monkeypatch.setattr(
probe,
"_get_device_states",
AsyncMock(return_value=[("device-locked", "device"), ("device-unlocked", "device")]),
)
async def mock_lock_state(adb_path, serial, timeout_seconds=1.0):
return True if serial == "device-locked" else False
monkeypatch.setattr(probe, "_get_confirmed_device_lock_state", mock_lock_state)
result = await probe.probe_submission_readiness()
assert result.status == ProbeStatus.PASS
assert result.summary == "Connected"
assert result.metadata["active_device"]["serial"] == "device-unlocked"
assert result.metadata["active_device"]["is_locked"] is False
@pytest.mark.asyncio
async def test_adb_probe_prefers_unlocked_device_when_one_is_locked(monkeypatch):
"""When multiple ready devices exist, probe() should pick the unlocked one as active."""
from artemis.core.diagnostics.schema import DeviceInfo
probe = AdbDeviceProbe()
monkeypatch.setattr(probe, "_locate_adb", lambda: "/usr/bin/adb")
monkeypatch.setattr(probe, "_get_adb_version", AsyncMock(return_value="1.0.41"))
monkeypatch.setattr(probe, "_locate_emulator", lambda: "/usr/bin/emulator")
monkeypatch.setattr(probe, "_list_installed_avds", lambda _: [])
devices = [
DeviceInfo(serial="dev-locked-1", state="device", model="Pixel 7", is_locked=True),
DeviceInfo(serial="dev-unlocked-2", state="device", model="Pixel 8", is_locked=False),
]
monkeypatch.setattr(probe, "_parse_adb_devices", AsyncMock(return_value=devices))
result = await probe.probe()
assert result.status == ProbeStatus.PASS
assert result.summary == "Connected"
assert result.metadata["active_device"]["serial"] == "dev-unlocked-2"
assert result.metadata["active_device"]["is_locked"] is False
@pytest.mark.asyncio
async def test_llm_credentials_probe_structure():
"""Verify LLMCredentialsProbe returns correct category and schema."""
+16 -1
View File
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from unittest.mock import MagicMock, patch
import pytest
@@ -92,12 +93,26 @@ def test_submit_task_to_daemon():
mock_resp.read.return_value = b'{"status": "queued", "tasks": [{"session_id": "test-sess"}]}'
mock_resp.__enter__.return_value = mock_resp
with patch("urllib.request.urlopen", return_value=mock_resp):
with patch("urllib.request.urlopen", return_value=mock_resp) as mock_urlopen:
res = submit_task_to_daemon(
"test goal",
profile="flash",
device_serial="pixel-8",
session_id="custom-sess-789",
ingress="mcp",
conversation_id="conv-123",
)
assert res is not None
assert res["status"] == "queued"
assert res["tasks"][0]["session_id"] == "test-sess"
# Verify request payload
req = mock_urlopen.call_args[0][0]
data = json.loads(req.data.decode("utf-8"))
assert data["goal"] == "test goal"
assert data["profile"] == "flash"
assert data["device_serial"] == "pixel-8"
assert data["session_id"] == "custom-sess-789"
assert data["ingress"] == "mcp"
assert data["conversation_id"] == "conv-123"
+19
View File
@@ -436,3 +436,22 @@ def test_cli_restart_command(monkeypatch):
assert stopped["port"] == 8888
assert ui_called["port"] == 8888
assert ui_called["open_browser"] is False
def test_cli_server_lifecycle_aliases(monkeypatch):
"""Verify 'artemis server status/stop/restart' subcommands are accessible."""
from artemis.runtime import server_lifecycle
monkeypatch.setattr(server_lifecycle, "is_port_in_use", lambda port, **kwargs: False)
monkeypatch.setattr(server_lifecycle, "find_server_pids", lambda port: [])
monkeypatch.setattr(server_lifecycle, "read_server_info", lambda: None)
result = runner.invoke(app, ["server", "status", "--port", "59998"])
assert result.exit_code == 0
assert "OFFLINE" in result.output or "STOPPED" in result.output
help_result = runner.invoke(app, ["server", "--help"])
assert help_result.exit_code == 0
assert "restart" in help_result.output
assert "stop" in help_result.output
assert "status" in help_result.output
+11 -6
View File
@@ -16,7 +16,7 @@
from pathlib import Path
import tempfile
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import pytest
from pydantic import SecretStr
@@ -218,8 +218,12 @@ def test_output_config_and_recording():
def test_runtime_state_and_ipc(tmp_path, monkeypatch):
"""Test IPC port and LS address state helpers."""
import urllib.request
monkeypatch.setattr(urllib.request, "urlopen", MagicMock(side_effect=Exception("offline")))
ipc_state_file = tmp_path / ".artemis_ipc_port"
monkeypatch.setattr("artemis.config.runtime.get_ipc_port_file", lambda: ipc_state_file)
monkeypatch.setattr("artemis.config.runtime.ROOT_DIR", tmp_path)
monkeypatch.setattr("artemis.config.runtime.get_app_dir", lambda: tmp_path)
# Test IPC port
write_ipc_port(49152)
assert read_ipc_port() == 49152
@@ -365,8 +369,8 @@ def test_checker_builder_and_context_propagation():
mock_task.request.record_trace = False
mock_task.request.name = "test_task"
mock_task.request.profile = None
mock_task.request.goal = "Test goal"
agent._prepare_tracing(mock_task, ctx)
with patch("artemis.sdk.agent.DataEngine"):
agent._prepare_tracing(mock_task, ctx)
assert ctx.execution_setup is not None
assert ctx.execution_setup.disable_checker is False
@@ -455,8 +459,8 @@ def test_explorer_builder_and_resolution(monkeypatch):
mock_task.request.record_trace = False
mock_task.request.name = "explorer_test_task"
mock_task.request.profile = None
mock_task.request.goal = "Test explorer propagation"
agent._prepare_tracing(mock_task, ctx)
with patch("artemis.sdk.agent.DataEngine"):
agent._prepare_tracing(mock_task, ctx)
assert ctx.execution_setup is not None
assert ctx.execution_setup.explorer_version == "pro"
@@ -505,7 +509,8 @@ def test_outputter_builder_and_context_propagation():
mock_task.request.name = "outputter_test_task"
mock_task.request.profile = None
mock_task.request.goal = "Test outputter propagation"
agent._prepare_tracing(mock_task, ctx)
with patch("artemis.sdk.agent.DataEngine"):
agent._prepare_tracing(mock_task, ctx)
assert ctx.execution_setup is not None
assert ctx.execution_setup.disable_outputter is False