feat: improve task execution, verification and packaging

Add execution incidents, action bursts and shared Flash history.

Expose verification and Explorer settings with streamed check results.

Support graceful cancellation, recording recovery and bundled UI resources.

Add CI quality checks and clean up comments and documentation.
This commit is contained in:
somew1nd committed 2026-09-02 21:45:51 -07:00
1 parent 5ccf06d677
commit 67248c887e
289 files changed
+15883 -6919

No files matched your search

@@ -0,0 +1,273 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Cancelling a task must let the worker finalize its recording, or recover it."""
import asyncio
from contextlib import ExitStack
from unittest.mock import MagicMock, patch
import pytest
from apps.admin_console.core.state import state
from apps.admin_console.services.task_queue_service import TaskQueueService, task_queue_service
from artemis.runtime import cancel_requests
from artemis.runtime.device_lock import DeviceLockOwner
TQS = "apps.admin_console.services.task_queue_service"
def _reset_state() -> None:
state.clear_queue()
state.queue_items.clear()
state.active_connections.clear()
state.cancelled_session_ids.clear()
state.manually_stopped_run_ids.clear()
state.current_process = None
state.active_session_id = None
state.current_goal = None
state.current_profile = None
@pytest.fixture(autouse=True)
def isolated(tmp_path, monkeypatch):
marker_dir = tmp_path / "cancel-requests"
lock_dir = tmp_path / "device-locks"
lock_dir.mkdir()
monkeypatch.setattr(cancel_requests, "get_temp_dir", lambda _subfolder=None: marker_dir)
monkeypatch.setattr(
"artemis.runtime.device_lock.get_temp_dir", lambda _subfolder=None: lock_dir
)
monkeypatch.setenv("ARTEMIS_CANCEL_GRACE_SECONDS", "5")
for task in list(TaskQueueService._forced_stop_tasks):
task.cancel()
TaskQueueService._forced_stop_tasks.clear()
_reset_state()
yield marker_dir
for task in list(TaskQueueService._forced_stop_tasks):
task.cancel()
TaskQueueService._forced_stop_tasks.clear()
_reset_state()
def _owner(pid: int = 24680, session_id: str = "sess-1") -> DeviceLockOwner:
return DeviceLockOwner(
pid=pid,
process_created_at=1234.5,
token="owner-token",
device_id="emulator-5554",
description="frontend task: demo",
acquired_at="2026-08-24T00:00:00+00:00",
session_id=session_id,
ingress="frontend",
)
def _stop_patches(stack: ExitStack, owner: DeviceLockOwner, alive: bool = True):
stack.enter_context(patch.object(TaskQueueService, "ensure_worker_running"))
stack.enter_context(
patch(f"{TQS}.DeviceExecutionLock.get_active_owners", return_value={"dev": owner})
)
stack.enter_context(patch(f"{TQS}.DeviceExecutionLock.is_active_owner", return_value=True))
stack.enter_context(patch(f"{TQS}.DeviceExecutionLock.cleanup_stale_locks"))
stack.enter_context(patch(f"{TQS}.session_repo.update_session_status"))
stack.enter_context(patch("artemis.runtime.trace_store.update_trace_status"))
pid_alive = stack.enter_context(patch(f"{TQS}.pid_is_alive", return_value=alive))
terminate = stack.enter_context(patch(f"{TQS}.process_supervisor.terminate_tree_verified"))
return pid_alive, terminate
@pytest.mark.asyncio
async def test_stop_requests_graceful_cancel_and_skips_kill_when_worker_exits():
owner = _owner()
with ExitStack() as stack:
pid_alive, terminate = _stop_patches(stack, owner)
assert task_queue_service.stop_tasks(session_id="sess-1") is True
# The worker is asked to cancel itself; nothing is killed yet.
terminate.assert_not_called()
assert cancel_requests.is_cancel_requested(session_id="sess-1")
assert cancel_requests.is_cancel_requested(pid=24680, process_created_at=1234.5)
assert len(TaskQueueService._forced_stop_tasks) == 1
enforcer = next(iter(TaskQueueService._forced_stop_tasks))
# The worker finalizes and exits on its own within the grace period.
pid_alive.return_value = False
await asyncio.wait_for(enforcer, timeout=5)
terminate.assert_not_called()
assert not cancel_requests.is_cancel_requested(session_id="sess-1")
@pytest.mark.asyncio
async def test_stop_kills_worker_that_ignores_the_cancel_request(monkeypatch):
monkeypatch.setenv("ARTEMIS_CANCEL_GRACE_SECONDS", "0.6")
owner = _owner()
with ExitStack() as stack:
_pid_alive, terminate = _stop_patches(stack, owner, alive=True)
terminate.return_value = True
assert task_queue_service.stop_tasks(session_id="sess-1") is True
terminate.assert_not_called()
enforcer = next(iter(TaskQueueService._forced_stop_tasks))
await asyncio.wait_for(enforcer, timeout=5)
terminate.assert_called_once_with(24680, 1234.5)
assert not cancel_requests.is_cancel_requested(session_id="sess-1")
def test_zero_grace_keeps_the_immediate_kill(monkeypatch):
monkeypatch.setenv("ARTEMIS_CANCEL_GRACE_SECONDS", "0")
owner = _owner()
with ExitStack() as stack:
_pid_alive, terminate = _stop_patches(stack, owner)
terminate.return_value = True
assert task_queue_service.stop_tasks(session_id="sess-1") is True
terminate.assert_called_once_with(24680, 1234.5)
assert not cancel_requests.is_cancel_requested(session_id="sess-1")
assert not TaskQueueService._forced_stop_tasks
def test_stop_of_dead_worker_writes_no_marker():
owner = _owner()
with ExitStack() as stack:
_pid_alive, terminate = _stop_patches(stack, owner, alive=False)
assert task_queue_service.stop_tasks(session_id="sess-1") is True
terminate.assert_not_called()
assert not cancel_requests.is_cancel_requested(session_id="sess-1")
assert not TaskQueueService._forced_stop_tasks
@pytest.mark.asyncio
async def test_recover_or_fail_recording_finalizes_orphaned_raw_file(tmp_path):
final = tmp_path / "web_x_FAIL_2026-09-02T16-31-32" / "recording.mp4"
final.parent.mkdir()
final.write_bytes(b"video")
raw_path = str(tmp_path / "web_x" / "recording.mkv")
repo = MagicMock()
repo.get_video_recording_for_session.return_value = {
"status": "recording",
"local_video_path": raw_path,
"start_time": 1788391895.4,
}
media = MagicMock()
media.recover_orphaned_recording.return_value = final
media.path_to_video_url.return_value = "/videos/traces/web_x_FAIL/recording.mp4"
events: list[tuple[str, dict]] = []
with (
patch(f"{TQS}.session_repo", repo),
patch(f"{TQS}.media_service", media),
patch.object(
TaskQueueService, "_broadcast_event", side_effect=lambda t, d: events.append((t, d))
),
):
await TaskQueueService._recover_or_fail_recording("sess-1")
media.recover_orphaned_recording.assert_called_once_with(raw_path, 1788391895.4)
repo.mark_recording_ready.assert_called_once_with("sess-1", str(final))
repo.mark_recording_failed_if_pending.assert_not_called()
assert events == [
(
"recording_ready",
{"session_id": "sess-1", "video_url": "/videos/traces/web_x_FAIL/recording.mp4"},
)
]
@pytest.mark.asyncio
async def test_recover_or_fail_recording_marks_failed_when_nothing_usable():
repo = MagicMock()
repo.get_video_recording_for_session.return_value = {
"status": "recording",
"local_video_path": "/nowhere/recording.mkv",
"start_time": 1.0,
}
repo.get_video_recordings_map.return_value = {}
repo.mark_recording_failed_if_pending.return_value = True
media = MagicMock()
media.recover_orphaned_recording.return_value = None
media.build_video_index.return_value = {}
media.resolve_video_url.return_value = None
events: list[tuple[str, dict]] = []
with (
patch(f"{TQS}.session_repo", repo),
patch(f"{TQS}.media_service", media),
patch.object(
TaskQueueService, "_broadcast_event", side_effect=lambda t, d: events.append((t, d))
),
):
await TaskQueueService._recover_or_fail_recording("sess-2")
repo.mark_recording_ready.assert_not_called()
repo.mark_recording_failed_if_pending.assert_called_once()
assert events[0][0] == "recording_failed"
def test_startup_sweep_publishes_orphaned_recordings(tmp_path):
final = tmp_path / "web_a_FAIL_2026-09-02T16-31-32" / "recording.mp4"
final.parent.mkdir()
final.write_bytes(b"video")
repo = MagicMock()
repo.get_unfinalized_video_recordings.return_value = [
{
"session_id": "sess-a",
"local_video_path": str(tmp_path / "web_a" / "recording.mkv"),
"start_time": 10.0,
"session_status": "cancelled",
},
{
"session_id": "sess-b",
"local_video_path": str(tmp_path / "web_b" / "recording.mkv"),
"start_time": None,
"session_start_time": 20.0,
"session_status": "failed",
},
]
repo.mark_recording_ready.return_value = True
media = MagicMock()
media.recover_orphaned_recording.side_effect = [final, None]
with patch(f"{TQS}.session_repo", repo), patch(f"{TQS}.media_service", media):
assert TaskQueueService.recover_orphaned_recordings_on_launch() == 1
assert media.recover_orphaned_recording.call_args_list[0].args == (
str(tmp_path / "web_a" / "recording.mkv"),
10.0,
)
assert media.recover_orphaned_recording.call_args_list[1].args == (
str(tmp_path / "web_b" / "recording.mkv"),
20.0,
)
repo.mark_recording_ready.assert_called_once_with("sess-a", str(final))
@pytest.mark.asyncio
async def test_recover_or_fail_recording_skips_rows_the_worker_finalized():
repo = MagicMock()
repo.get_video_recording_for_session.return_value = {"status": "ready"}
media = MagicMock()
with patch(f"{TQS}.session_repo", repo), patch(f"{TQS}.media_service", media):
await TaskQueueService._recover_or_fail_recording("sess-3")
media.recover_orphaned_recording.assert_not_called()
repo.mark_recording_failed_if_pending.assert_not_called()
@@ -51,7 +51,13 @@ async def test_session_video_publishes_only_finalized_versioned_media(monkeypatc
"resolve_video_segments",
MagicMock(
return_value=[
{"url": "/videos/recording.mp4", "start": 0, "duration": 4, "width": 1080, "height": 1920}
{
"url": "/videos/recording.mp4",
"start": 0,
"duration": 4,
"width": 1080,
"height": 1920,
}
]
),
)
@@ -0,0 +1,173 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Recording files of cancelled / killed workers must survive and be served whole."""
import os
from pathlib import Path
import time
from unittest.mock import patch
import pytest
from apps.admin_console.services import media_service as media_module
MediaService = media_module.MediaService
@pytest.fixture(autouse=True)
def clean_cache():
MediaService._playable_cache.clear()
yield
MediaService._playable_cache.clear()
def _fake_convert(src: Path, dst: Path) -> Path:
dst.write_bytes(b"mp4:" + src.read_bytes())
return dst
def _age(path: Path, seconds: float) -> None:
stamp = time.time() - seconds
os.utime(path, (stamp, stamp))
def test_live_recording_is_not_converted(tmp_path):
mkv = tmp_path / "recording.mkv"
mkv.write_bytes(b"x" * 10) # fresh mtime: scrcpy is still writing
with patch.object(MediaService, "_convert_to_mp4", side_effect=_fake_convert) as convert:
assert MediaService.ensure_browser_playable_video(mkv) == mkv
convert.assert_not_called()
assert not (tmp_path / "recording.mp4").exists()
def test_stale_partial_mp4_is_reconverted_from_newer_mkv(tmp_path):
mkv = tmp_path / "recording.mkv"
mkv.write_bytes(b"full")
partial = tmp_path / "recording.mp4"
partial.write_bytes(b"partial")
_age(partial, 120)
_age(mkv, 60)
with patch.object(MediaService, "_convert_to_mp4", side_effect=_fake_convert) as convert:
result = MediaService.ensure_browser_playable_video(mkv)
convert.assert_called_once()
assert result == partial
assert partial.read_bytes() == b"mp4:full"
def test_finished_mp4_newer_than_mkv_is_reused(tmp_path):
mkv = tmp_path / "recording.mkv"
mkv.write_bytes(b"full")
mp4 = tmp_path / "recording.mp4"
mp4.write_bytes(b"final")
_age(mkv, 120)
_age(mp4, 60)
with patch.object(MediaService, "_convert_to_mp4", side_effect=_fake_convert) as convert:
result = MediaService.ensure_browser_playable_video(mkv)
convert.assert_not_called()
assert result == mp4
def test_build_video_index_skips_live_recordings(tmp_path, monkeypatch):
traces = tmp_path / "traces"
live = traces / "web_1_live"
live.mkdir(parents=True)
(live / "recording.mkv").write_bytes(b"live")
done = traces / "web_2_done_PASS_2026-09-02T12-00-00"
done.mkdir()
(done / "recording.mp4").write_bytes(b"done")
monkeypatch.setattr(media_module, "TRACES_PATH", traces)
monkeypatch.setattr(media_module, "WORKSPACE_ROOT", tmp_path)
with patch.object(MediaService, "_convert_to_mp4", side_effect=_fake_convert) as convert:
idx = MediaService.build_video_index()
convert.assert_not_called()
assert "web_1_live" not in idx
assert idx["web_2_done"] == idx["web_2_done_PASS_2026-09-02T12-00-00"]
assert idx["web_2_done"].endswith("recording.mp4")
def test_strip_trace_status_suffix_handles_every_terminal_marker():
assert MediaService.strip_trace_status_suffix("web_1_PASS_2026") == "web_1"
assert MediaService.strip_trace_status_suffix("web_1_FAIL_2026") == "web_1"
assert MediaService.strip_trace_status_suffix("web_1_TESTFAIL_2026") == "web_1"
assert MediaService.strip_trace_status_suffix("web_1") == "web_1"
def test_recover_orphaned_recording_remuxes_and_renames(tmp_path, monkeypatch):
traces = tmp_path / "traces"
folder = traces / "web_1788391889_5d5287b9"
folder.mkdir(parents=True)
mkv = folder / "recording.mkv"
mkv.write_bytes(b"full")
partial = folder / "recording.mp4"
partial.write_bytes(b"partial")
_age(partial, 120)
_age(mkv, 60)
monkeypatch.setattr(media_module, "TRACES_PATH", traces)
start = time.mktime(time.strptime("2026-09-02 16:31:32", "%Y-%m-%d %H:%M:%S"))
with patch.object(MediaService, "_convert_to_mp4", side_effect=_fake_convert):
final = MediaService.recover_orphaned_recording(str(mkv), start)
assert final is not None
assert final.parent.name == "web_1788391889_5d5287b9_FAIL_2026-09-02T16-31-32"
assert final.name == "recording.mp4"
assert final.read_bytes() == b"mp4:full"
assert not folder.exists()
assert not (final.parent / "recording.mkv").exists()
def test_recover_orphaned_recording_leaves_live_file_alone(tmp_path, monkeypatch):
traces = tmp_path / "traces"
folder = traces / "web_live"
folder.mkdir(parents=True)
mkv = folder / "recording.mkv"
mkv.write_bytes(b"live")
monkeypatch.setattr(media_module, "TRACES_PATH", traces)
with patch.object(MediaService, "_convert_to_mp4", side_effect=_fake_convert) as convert:
assert MediaService.recover_orphaned_recording(str(mkv), None) is None
convert.assert_not_called()
assert folder.exists()
def test_recover_orphaned_recording_keeps_raw_when_remux_fails(tmp_path, monkeypatch):
traces = tmp_path / "traces"
folder = traces / "web_broken"
folder.mkdir(parents=True)
mkv = folder / "recording.mkv"
mkv.write_bytes(b"corrupt")
_age(mkv, 60)
monkeypatch.setattr(media_module, "TRACES_PATH", traces)
with patch.object(MediaService, "_convert_to_mp4", side_effect=lambda src, dst: src):
assert MediaService.recover_orphaned_recording(str(mkv), None) is None
assert mkv.exists()
assert folder.exists()
def test_recover_orphaned_recording_returns_none_without_files(tmp_path):
missing = tmp_path / "missing" / "recording.mkv"
assert MediaService.recover_orphaned_recording(str(missing)) is None
assert MediaService.recover_orphaned_recording(None) is None
@@ -49,9 +49,7 @@ async def test_credentials_endpoint_never_returns_key_material(monkeypatch):
from artemis.config import settings
honeytoken = f"sk-honeytoken-{py_secrets.token_hex(16)}"
monkeypatch.setattr(
type(settings), "get_api_key", lambda self, provider: SecretStr(honeytoken)
)
monkeypatch.setattr(type(settings), "get_api_key", lambda self, provider: SecretStr(honeytoken))
async with _client() as ac:
res = await ac.get("/api/system/credentials")
@@ -168,9 +166,7 @@ async def test_cross_origin_browser_request_is_rejected_without_cors_grant():
@pytest.mark.asyncio
async def test_same_origin_browser_request_passes():
async with _client() as ac:
res = await ac.get(
"/api/system/emulator/status", headers={"Origin": "http://localhost"}
)
res = await ac.get("/api/system/emulator/status", headers={"Origin": "http://localhost"})
assert res.status_code == 200
@@ -68,7 +68,9 @@ async def test_stream_events_active_session_replay():
state.current_goal = "Replay Goal"
state.current_profile = "flash"
with patch("apps.admin_console.database.repositories.step_repository.step_repo") as mock_step_repo:
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"}
]
@@ -85,7 +87,6 @@ async def test_stream_events_active_session_replay():
await iterator.aclose()
@pytest.mark.asyncio
async def test_windows_sigint_is_ignored_while_task_is_active():
state.current_process = MagicMock(returncode=None)
@@ -0,0 +1,68 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""``/api/sessions/{id}/checks``: backfill source for the Checker panel."""
import json
from apps.admin_console.services import media_service as media_service_module
from apps.admin_console.services.media_service import MediaService
def test_get_session_checks_reads_ledger_and_outcome(tmp_path, monkeypatch):
monkeypatch.setattr(media_service_module, "TRACES_PATH", tmp_path)
session_dir = tmp_path / "sess-1"
session_dir.mkdir()
records = [
{
"attempt_id": "abc#1",
"checkpoint_id": "abc",
"subgoal_text": "Create the alarm",
"item_text": "alarm exists",
"kind": "verify",
"when": "on_complete",
"status": "passed",
"evidence": "seen",
"ts": 1.0,
},
{"attempt_id": "final#1", "checkpoint_id": "final", "status": "passed", "ts": 2.0},
]
(session_dir / "check_ledger.jsonl").write_text(
"\n".join(json.dumps(r) for r in records) + "\nnot json\n\n", encoding="utf-8"
)
(session_dir / "run_outcome.json").write_text(
json.dumps({"task_status": "completed", "tests": {"passed": 2, "failed": 0}}),
encoding="utf-8",
)
result = MediaService.get_session_checks("sess-1")
assert [r["attempt_id"] for r in result["records"]] == ["abc#1", "final#1"]
assert result["records"][0]["subgoal_text"] == "Create the alarm"
assert result["run_outcome"]["task_status"] == "completed"
def test_get_session_checks_without_material(tmp_path, monkeypatch):
monkeypatch.setattr(media_service_module, "TRACES_PATH", tmp_path)
(tmp_path / "sess-2").mkdir()
assert MediaService.get_session_checks("sess-2") == {"records": [], "run_outcome": None}
assert MediaService.get_session_checks("missing") == {"records": [], "run_outcome": None}
def test_checks_route_is_registered():
from apps.admin_console.routers import media
paths = {getattr(r, "path", None) for r in media.router.routes}
assert "/api/sessions/{session_id}/checks" in paths
@@ -40,9 +40,7 @@ async def test_run_task_rejects_locked_device(monkeypatch):
)
run_probe = AsyncMock(return_value=locked_probe)
enqueue_tasks = AsyncMock()
monkeypatch.setattr(
tasks.readiness_engine, "run_device_submission_probe", run_probe
)
monkeypatch.setattr(tasks.readiness_engine, "run_device_submission_probe", run_probe)
monkeypatch.setattr(tasks.task_queue_service, "enqueue_tasks", enqueue_tasks)
with pytest.raises(HTTPException) as exc_info:
@@ -66,9 +64,7 @@ async def test_run_task_enqueues_when_device_is_unlocked(monkeypatch):
)
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.readiness_engine, "run_device_submission_probe", run_probe)
monkeypatch.setattr(tasks.task_queue_service, "enqueue_tasks", enqueue_tasks)
result = await tasks.run_task(RunRequest(goal="Open Settings"))
@@ -96,9 +92,7 @@ async def test_run_task_binds_probe_verified_device_when_no_serial_requested(mon
)
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.readiness_engine, "run_device_submission_probe", run_probe)
monkeypatch.setattr(tasks.task_queue_service, "enqueue_tasks", enqueue_tasks)
result = await tasks.run_task(RunRequest(goal="Run on any ready device"))
@@ -113,9 +107,7 @@ async def test_run_task_binds_probe_verified_device_when_no_serial_requested(mon
@pytest.mark.asyncio
async def test_idempotent_retry_skips_device_probe_for_active_session(monkeypatch):
run_probe = AsyncMock()
monkeypatch.setattr(
tasks.readiness_engine, "run_device_submission_probe", run_probe
)
monkeypatch.setattr(tasks.readiness_engine, "run_device_submission_probe", run_probe)
monkeypatch.setattr(tasks.state, "active_session_id", "sdk-task-1")
monkeypatch.setattr(tasks.state, "active_connections", {})
monkeypatch.setattr(
@@ -204,9 +196,7 @@ async def test_explicit_device_is_rejected_when_attached_but_not_ready(monkeypat
@pytest.mark.parametrize("enumeration", [None, []])
@pytest.mark.asyncio
async def test_explicit_device_proceeds_when_enumeration_is_indeterminate(
monkeypatch, enumeration
):
async def test_explicit_device_proceeds_when_enumeration_is_indeterminate(monkeypatch, enumeration):
"""A failed (None) or empty enumeration must never hard-reject an explicit
serial: the submission queues and fails downstream if the device is truly
absent. This is the startup-storm regression guard."""
@@ -236,4 +226,3 @@ async def test_explicit_device_proceeds_when_enumeration_is_indeterminate(
enqueue_tasks.assert_awaited_once()
_, kwargs = enqueue_tasks.call_args
assert kwargs.get("device_serial") == "pixel-10"
@@ -45,6 +45,13 @@ def clean_state(tmp_path, monkeypatch):
queue_module = importlib.import_module("apps.admin_console.services.task_queue_service")
monkeypatch.setattr(state_module, "PAUSE_FILE", isolated_pause_file)
monkeypatch.setattr(queue_module, "PAUSE_FILE", isolated_pause_file)
# The stop assertions below describe the legacy immediate kill; the graceful
# cancel path has its own tests in test_graceful_stop_and_recovery.py.
monkeypatch.setenv("ARTEMIS_CANCEL_GRACE_SECONDS", "0")
monkeypatch.setattr(
"artemis.runtime.cancel_requests.get_temp_dir",
lambda _subfolder=None: isolated_lock_dir,
)
state.clear_queue()
state.queue_items.clear()
state.current_process = None
@@ -283,6 +290,8 @@ async def test_queue_worker_cmd_construction():
enable_outputter=True,
locked_app_package="com.google.android.apps.maps",
app_path="/path/to/app.apk",
verification_level=" Checkpoints ",
explorer_mode="ULTRA",
)
for _ in range(30):
@@ -301,6 +310,12 @@ async def test_queue_worker_cmd_construction():
assert "com.google.android.apps.maps" in cmd
assert "--app-path" in cmd
assert "/path/to/app.apk" in cmd
# Pro tuning knobs are normalised (trimmed, lower-cased) and forwarded
# to the worker as the CLI's existing spelling.
assert cmd[cmd.index("--verification-level") + 1] == "checkpoints"
assert cmd[cmd.index("--explorer-pro-mode") + 1] == "ultra"
assert enqueue_result["tasks"][0]["verification_level"] == "checkpoints"
assert enqueue_result["tasks"][0]["explorer_mode"] == "ultra"
assert (
executed_kwargs[0]["env"]["ARTEMIS_DEVICE_QUEUE_TICKET"]
== (enqueue_result["tasks"][0]["queue_ticket"])
@@ -430,9 +445,7 @@ def test_stop_tasks_terminates_external_global_owner_and_preserves_local_waiter(
patch(
"apps.admin_console.services.task_queue_service.session_repo.update_session_status"
) as update_status,
patch(
"artemis.runtime.trace_store.update_trace_status"
) as update_trace_status,
patch("artemis.runtime.trace_store.update_trace_status") as update_trace_status,
):
assert task_queue_service.stop_tasks(clear_all=False) is True
@@ -510,9 +523,7 @@ def test_stop_tasks_session_target_never_adopts_mismatched_fallback_owner():
patch(
"apps.admin_console.services.task_queue_service.process_supervisor.terminate_tree_verified"
) as terminate_verified,
patch(
"apps.admin_console.services.task_queue_service.session_repo.update_session_status"
),
patch("apps.admin_console.services.task_queue_service.session_repo.update_session_status"),
patch(
"apps.admin_console.services.task_queue_service.session_repo.get_session_by_id",
return_value=None,
@@ -733,7 +744,10 @@ 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"),
patch("apps.admin_console.services.task_queue_service.DeviceExecutionLock.reserve", return_value="mock-ticket-unified"),
patch(
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.reserve",
return_value="mock-ticket-unified",
),
):
res = await task_queue_service.enqueue_tasks(
["Test unified goal"],
@@ -885,7 +899,9 @@ def test_stop_tasks_by_device_id_targets_specific_device():
"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.DeviceExecutionLock.cleanup_stale_locks"
),
patch(
"apps.admin_console.services.task_queue_service.process_supervisor.terminate_tree_verified",
return_value=True,
@@ -899,8 +915,18 @@ def test_stop_tasks_by_device_id_targets_specific_device():
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"},
{
"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 (
@@ -916,7 +942,9 @@ def test_stop_tasks_queued_item_by_session_id():
"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,
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")
@@ -934,7 +962,10 @@ async def test_enqueue_tasks_deduplicates_by_session_id():
with (
patch("apps.admin_console.services.task_queue_service.session_repo"),
patch("apps.admin_console.services.task_queue_service.DeviceExecutionLock.reserve", return_value="ticket-123"),
patch(
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.reserve",
return_value="ticket-123",
),
):
res1 = await task_queue_service.enqueue_tasks(["Task goal"], session_id="sid-dedup-1")
assert len(state.queue_items) == 1
@@ -1041,8 +1072,7 @@ async def test_manual_stop_of_one_run_does_not_pollute_concurrent_run():
assert ended_payloads["run-b"]["was_stopped_manually"] is False
persisted = {
call.args[0]: call.args[1]
for call in mock_repo.update_session_status.call_args_list
call.args[0]: call.args[1] for call in mock_repo.update_session_status.call_args_list
}
assert persisted.get("run-a") == "cancelled"
assert persisted.get("run-b") == "completed"
@@ -1059,22 +1089,25 @@ async def test_enqueue_tasks_debounces_rapid_identical_submissions():
with (
patch("apps.admin_console.services.task_queue_service.session_repo"),
patch("apps.admin_console.services.task_queue_service.DeviceExecutionLock.reserve", return_value="ticket-456"),
patch(
"apps.admin_console.services.task_queue_service.DeviceExecutionLock.reserve",
return_value="ticket-456",
),
patch(
"artemis.runtime.device_pool.device_pool.try_list_devices_async",
new=AsyncMock(return_value=[]),
),
patch.object(TaskQueueService, "ensure_worker_running"),
):
res1 = await task_queue_service.enqueue_tasks(["Rapid duplicate goal"], device_serial="dev-1")
res1 = await task_queue_service.enqueue_tasks(
["Rapid duplicate goal"], device_serial="dev-1"
)
assert len(state.queue_items) == 1
assert res1["enqueued_count"] == 1
# Immediate second submission (within 1s) must debounce
res2 = await task_queue_service.enqueue_tasks(["Rapid duplicate goal"], device_serial="dev-1")
res2 = await task_queue_service.enqueue_tasks(
["Rapid duplicate goal"], device_serial="dev-1"
)
assert len(state.queue_items) == 1
assert res2["enqueued_count"] == 0