merge(runtime): integrate trace store relocation
This commit is contained in:
commit
8003cf39c2
21 files changed
+134
-64
No files matched your search
@@ -15,6 +15,8 @@
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from artemis.runtime import trace_store
|
||||
|
||||
try:
|
||||
from admin_console.database.connection import db_session
|
||||
except ImportError:
|
||||
@@ -309,8 +311,6 @@ class SessionRepository:
|
||||
)
|
||||
count += cursor.rowcount
|
||||
try:
|
||||
from mcp_server.utils import trace_store
|
||||
|
||||
if trace_store.read_status(str(row["session_id"])):
|
||||
trace_store.update_trace_status(
|
||||
str(row["session_id"]),
|
||||
@@ -410,3 +410,11 @@ class SessionRepository:
|
||||
|
||||
|
||||
session_repo = SessionRepository()
|
||||
|
||||
# Give the base runtime package access to orphan-session reconciliation without
|
||||
# letting it import this application-layer module (inverted dependency): any
|
||||
# process that loads the admin console DB layer -- the server itself, or an
|
||||
# entry point stopping one -- thereby arms stop_server()'s post-kill cleanup.
|
||||
from artemis.runtime.server_lifecycle import register_session_reconciler # noqa: E402
|
||||
|
||||
register_session_reconciler(session_repo.reconcile_orphaned_sessions)
|
||||
@@ -19,6 +19,7 @@ from uuid import UUID
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from artemis.config import DB_PATH, TRACES_PATH
|
||||
from artemis.runtime import trace_store
|
||||
|
||||
try:
|
||||
from admin_console.core.state import state
|
||||
@@ -149,8 +150,6 @@ def _list_sessions_sync():
|
||||
harvested = session_repo.harvest_orphaned_sessions(orphaned_ids)
|
||||
print(f"[list_sessions] Auto-harvested {harvested} orphaned running session(s).")
|
||||
try:
|
||||
from mcp_server.utils import trace_store
|
||||
|
||||
for o_id in orphaned_ids:
|
||||
if trace_store.read_status(str(o_id)):
|
||||
trace_store.update_trace_status(
|
||||
|
||||
@@ -45,6 +45,7 @@ from artemis.runtime import (
|
||||
DeviceExecutionLock,
|
||||
current_adb_endpoint,
|
||||
process_supervisor,
|
||||
trace_store,
|
||||
)
|
||||
|
||||
|
||||
@@ -510,8 +511,6 @@ class TaskQueueService:
|
||||
}
|
||||
if sess_id:
|
||||
try:
|
||||
from mcp_server.utils import trace_store
|
||||
|
||||
st = trace_store.read_status(str(sess_id))
|
||||
if st:
|
||||
st["pid"] = proc.pid
|
||||
@@ -536,8 +535,6 @@ class TaskQueueService:
|
||||
log_path = None
|
||||
if sess_id:
|
||||
try:
|
||||
from mcp_server.utils import trace_store
|
||||
|
||||
log_path = trace_store.get_trace_stdout_log_path(str(sess_id))
|
||||
except Exception:
|
||||
log_path = None
|
||||
@@ -580,8 +577,6 @@ class TaskQueueService:
|
||||
f"status '{new_status}'"
|
||||
)
|
||||
try:
|
||||
from mcp_server.utils import trace_store
|
||||
|
||||
if trace_store.read_status(str(sess_id)):
|
||||
canonical_mcp_status = "completed" if new_status in ("completed", "success") else new_status
|
||||
trace_store.update_trace_status(
|
||||
@@ -870,8 +865,6 @@ class TaskQueueService:
|
||||
if sid:
|
||||
session_repo.update_session_status(str(sid), "cancelled", time.time())
|
||||
try:
|
||||
from mcp_server.utils import trace_store
|
||||
|
||||
is_mcp = bool(dev_owner and dev_owner.ingress == "mcp")
|
||||
if is_mcp or trace_store.read_status(str(sid)):
|
||||
trace_store.update_trace_status(
|
||||
@@ -1133,8 +1126,6 @@ class TaskQueueService:
|
||||
str(stopped_session_id), "cancelled", time.time()
|
||||
)
|
||||
try:
|
||||
from mcp_server.utils import trace_store
|
||||
|
||||
is_mcp = bool(owner and owner.ingress == "mcp")
|
||||
if is_mcp or trace_store.read_status(str(stopped_session_id)):
|
||||
trace_store.update_trace_status(
|
||||
|
||||
@@ -26,7 +26,11 @@ from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
import typer
|
||||
|
||||
from artemis.interfaces.cli.commands.ui import ensure_showcase_built, ui_command
|
||||
from artemis.interfaces.cli.commands.ui import (
|
||||
ensure_showcase_built,
|
||||
load_session_reconciler,
|
||||
ui_command,
|
||||
)
|
||||
from artemis.runtime.daemon_client import daemon_log_path, is_daemon_running, spawn_daemon
|
||||
from artemis.runtime.server_lifecycle import (
|
||||
find_server_pids,
|
||||
@@ -128,6 +132,7 @@ def restart_command(
|
||||
else:
|
||||
console.print(f" [dim]ℹ No active Artemis server found on port {port}.[/dim]")
|
||||
|
||||
load_session_reconciler()
|
||||
success, msg, stopped_pids = stop_server(port=port, force=force, timeout=12.0)
|
||||
if stopped_pids:
|
||||
console.print(f" [green]✓ {msg}[/green]\n")
|
||||
@@ -213,6 +218,7 @@ def stop_command(
|
||||
else:
|
||||
console.print(f" [dim]Scanning port {port}...[/dim]")
|
||||
|
||||
load_session_reconciler()
|
||||
success, msg, stopped_pids = stop_server(port=port, force=force, timeout=12.0)
|
||||
if stopped_pids:
|
||||
console.print(
|
||||
|
||||
@@ -103,6 +103,20 @@ def ensure_showcase_built(console: Console) -> None:
|
||||
console.print(f" [red]⚠ Failed to auto-build Showcase UI: {e}[/red]\n")
|
||||
|
||||
|
||||
def load_session_reconciler() -> None:
|
||||
"""Entry-layer assembly: arm stop_server()'s orphan-session cleanup.
|
||||
|
||||
Importing the admin console session repository registers its reconciler
|
||||
with ``artemis.runtime.server_lifecycle`` (inverted dependency: the base
|
||||
runtime package never imports the application layer). Guarded so a broken
|
||||
or absent admin console install can never block stopping a server.
|
||||
"""
|
||||
try:
|
||||
import apps.admin_console.database.repositories.session_repository # noqa: F401
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _poll_and_open_browser(url: str, stop_event: threading.Event, timeout: float = 15.0) -> None:
|
||||
"""Poll the UI server until it responds, then launch the user's default browser."""
|
||||
start = time.time()
|
||||
@@ -169,6 +183,7 @@ def ui_command(
|
||||
|
||||
if restart:
|
||||
console.print(f" [yellow]🔄 Restart requested. Recycling port {port}...[/yellow]")
|
||||
load_session_reconciler()
|
||||
stop_server(port=port, timeout=12.0)
|
||||
elif is_port_in_use(port):
|
||||
pids = find_server_pids(port)
|
||||
@@ -185,6 +200,7 @@ def ui_command(
|
||||
choice = "1"
|
||||
if choice == "2":
|
||||
console.print(f"\n [yellow]🔄 Terminating previous Artemis instance on port {port}...[/yellow]")
|
||||
load_session_reconciler()
|
||||
stop_server(port=port, timeout=12.0)
|
||||
elif choice == "3":
|
||||
return
|
||||
@@ -244,6 +260,9 @@ def ui_command(
|
||||
t.start()
|
||||
|
||||
try:
|
||||
# Intentional entry-layer lazy load: the CLI assembles the application
|
||||
# here, and importing the FastAPI server at module import time would
|
||||
# drag the whole admin console into every `artemis <cmd>` invocation.
|
||||
from apps.admin_console.server import run_ui_server
|
||||
|
||||
run_ui_server(host=host, port=port, reload=reload)
|
||||
|
||||
@@ -29,12 +29,13 @@ import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from artemis.config.paths import ROOT_DIR, get_server_info_file
|
||||
from artemis.runtime.device_lock import DeviceExecutionLock
|
||||
from artemis.runtime.process_probe import pid_is_alive
|
||||
from artemis.runtime.supervisor import ProcessSupervisor
|
||||
from artemis.utils.logger import get_logger
|
||||
|
||||
@@ -155,12 +156,28 @@ def request_graceful_shutdown(port: int, timeout: float = 1.5) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# Inverted dependency: the session database lives in the application layer
|
||||
# (apps.admin_console), which this base runtime package must not import.
|
||||
# The application layer registers its orphan-session reconciler here
|
||||
# (apps/admin_console/database/repositories/session_repository.py does so on
|
||||
# import); entry points that stop servers without loading the admin console
|
||||
# perform that import as part of their assembly. Without a registration,
|
||||
# reconciliation is skipped -- the admin console re-runs it on next startup.
|
||||
_session_reconciler: Callable[[], int] | None = None
|
||||
|
||||
|
||||
def register_session_reconciler(reconciler: Callable[[], int]) -> None:
|
||||
"""Register the app-layer callback that marks orphaned sessions as failed."""
|
||||
global _session_reconciler
|
||||
_session_reconciler = reconciler
|
||||
|
||||
|
||||
def _reconcile_orphaned_sessions() -> int:
|
||||
"""Immediately mark sessions whose worker PIDs died during forced stop."""
|
||||
if _session_reconciler is None:
|
||||
return 0
|
||||
try:
|
||||
from apps.admin_console.database.repositories.session_repository import session_repo
|
||||
|
||||
return session_repo.reconcile_orphaned_sessions()
|
||||
return _session_reconciler()
|
||||
except Exception as exc:
|
||||
logger.debug(f"Could not reconcile orphaned sessions after server stop: {exc}")
|
||||
return 0
|
||||
@@ -168,8 +185,6 @@ def _reconcile_orphaned_sessions() -> int:
|
||||
|
||||
def _any_pid_alive(pids: list[int]) -> bool:
|
||||
"""Return whether any target process is still running (zombies excluded)."""
|
||||
from artemis.runtime.process_probe import pid_is_alive
|
||||
|
||||
return any(pid_is_alive(pid) for pid in pids)
|
||||
|
||||
|
||||
@@ -188,8 +203,6 @@ def find_server_pids(port: int = 8000) -> list[int]:
|
||||
if info and info.get("port") == port:
|
||||
saved_pid = info.get("pid")
|
||||
if saved_pid and isinstance(saved_pid, int):
|
||||
from artemis.runtime.process_probe import pid_is_alive
|
||||
|
||||
if pid_is_alive(saved_pid):
|
||||
discovered.add(saved_pid)
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ import time
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
from artemis.runtime.process_probe import pid_is_alive
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
@@ -137,8 +139,6 @@ def _lock_is_stale(lock_path: str) -> bool:
|
||||
return time.time() - os.path.getmtime(lock_path) >= _MALFORMED_LOCK_GRACE_SECONDS
|
||||
except OSError:
|
||||
return False
|
||||
from artemis.runtime.process_probe import pid_is_alive
|
||||
|
||||
return not pid_is_alive(pid, created_at if created_at > 0 else None)
|
||||
|
||||
|
||||
@@ -25,12 +25,15 @@ mcp_server/
|
||||
│ ├── task_manager.py # mobile_manage_task
|
||||
│ ├── device_state.py # mobile_get_device_state
|
||||
│ └── inspect_trace.py # mobile_inspect_trace
|
||||
└── utils/ # Environment, device, and trace utilities
|
||||
└── utils/ # Environment and device utilities
|
||||
├── device_utils.py # Cross-platform ADB and emulator resolver
|
||||
├── env_utils.py # Python interpreter and process manager
|
||||
└── trace_store.py # Traces directory and status manager
|
||||
└── env_utils.py # Python interpreter and process manager
|
||||
```
|
||||
|
||||
The trace status store (traces directory layout and `status.json` lifecycle)
|
||||
lives in the neutral base package as `artemis/runtime/trace_store.py`, shared
|
||||
by the MCP server, the admin console, and spawned worker processes.
|
||||
|
||||
## 🔔 Multi-Platform Notification & Wakeup Support
|
||||
|
||||
When you start an asynchronous mobile test via `mobile_run_task`, Artemis MCP runs the job as a detached background process so the AI agent is not blocked. Upon completion or failure, Artemis dispatches alerts across **all available notification channels** so that both human developers and AI agents are notified immediately across different platforms:
|
||||
|
||||
@@ -41,8 +41,9 @@ try:
|
||||
except Exception:
|
||||
load_dotenv(os.path.join(PROJECT_ROOT, ".env"))
|
||||
|
||||
from artemis.runtime import trace_store
|
||||
from mcp_server.notifiers import notify
|
||||
from mcp_server.utils import device_utils, trace_store
|
||||
from mcp_server.utils import device_utils
|
||||
|
||||
|
||||
async def _initialize_agent(
|
||||
@@ -80,6 +81,9 @@ def resolve_profile_file() -> str | None:
|
||||
]
|
||||
)
|
||||
try:
|
||||
# Deliberate guarded import mirroring the module-level bootstrap above:
|
||||
# profile resolution must degrade to repo-relative candidates when the
|
||||
# artemis config package cannot be loaded.
|
||||
from artemis.config.paths import get_app_dir
|
||||
|
||||
app_dir = str(get_app_dir())
|
||||
@@ -146,6 +150,11 @@ async def run_task(
|
||||
target_serial = device_serial
|
||||
|
||||
try:
|
||||
# Deliberate lazy imports: the SDK pulls in the full agent stack
|
||||
# (LLM clients, graph, drivers), and importing it inside this try
|
||||
# converts an import-time failure into a properly recorded task
|
||||
# failure (trace status + wakeup notification) instead of a crash
|
||||
# before status.json is ever updated.
|
||||
from artemis.config import settings
|
||||
from artemis.sdk import Agent
|
||||
from artemis.sdk.builders import Builders
|
||||
@@ -167,6 +176,8 @@ async def run_task(
|
||||
target_serial = settings.ADB_DEVICE_SERIAL or os.environ.get("ADB_DEVICE_SERIAL")
|
||||
if not target_serial:
|
||||
try:
|
||||
# Optional path: pool-based selection falls back to the
|
||||
# first connected device on any import or query failure.
|
||||
from artemis.runtime import device_pool
|
||||
target_serial = device_pool.select_device()
|
||||
except Exception:
|
||||
|
||||
@@ -20,8 +20,8 @@ import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from artemis.runtime import trace_store
|
||||
from mcp_server.notifiers.base import BaseNotifier
|
||||
from mcp_server.utils import trace_store
|
||||
|
||||
logger = logging.getLogger("mcp_server.notifiers.file")
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ from typing import Any
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from mcp_server.base import mcp
|
||||
from mcp_server.utils import env_utils, trace_store
|
||||
from mcp_server.utils import env_utils
|
||||
from artemis.runtime import trace_store
|
||||
from artemis.utils.visualization import draw_action_overlay_on_image
|
||||
from artemis.utils.task_tree import (
|
||||
_render_step_detailed,
|
||||
|
||||
@@ -24,8 +24,15 @@ from typing import Any
|
||||
|
||||
from mcp_server.base import mcp
|
||||
from mcp_server.notifiers import notify
|
||||
from mcp_server.utils import env_utils, trace_store
|
||||
from artemis.runtime import DeviceExecutionLock, process_supervisor
|
||||
from mcp_server.utils import env_utils
|
||||
from artemis.runtime import (
|
||||
DeviceExecutionLock,
|
||||
is_daemon_running,
|
||||
process_supervisor,
|
||||
stop_task_on_daemon,
|
||||
trace_store,
|
||||
)
|
||||
from artemis.runtime.process_probe import pid_is_alive
|
||||
|
||||
_LIVENESS_FAILURE_ERROR = "Task runner process terminated unexpectedly."
|
||||
# Grace window covering the spawn race: the launcher pid may already have exited
|
||||
@@ -48,8 +55,6 @@ def _pid_alive(pid: int | None) -> bool:
|
||||
pid_int = int(pid)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
from artemis.runtime.process_probe import pid_is_alive
|
||||
|
||||
return pid_is_alive(pid_int)
|
||||
|
||||
|
||||
@@ -451,8 +456,6 @@ def mobile_manage_task(
|
||||
stopped_via_daemon = False
|
||||
if os.environ.get("ARTEMIS_STANDALONE") != "1":
|
||||
try:
|
||||
from artemis.runtime import is_daemon_running, stop_task_on_daemon
|
||||
|
||||
if is_daemon_running():
|
||||
stopped_via_daemon = stop_task_on_daemon(trace_id)
|
||||
except Exception:
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
"""MCP Tool: mobile_run_task."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -24,8 +25,15 @@ import uuid
|
||||
|
||||
from mcp_server.base import mcp
|
||||
from mcp_server.notifiers import notify
|
||||
from mcp_server.utils import env_utils, trace_store
|
||||
from artemis.runtime import DeviceExecutionLock
|
||||
from mcp_server.utils import env_utils
|
||||
from artemis.config.runtime import read_ipc_port
|
||||
from artemis.runtime import (
|
||||
DeviceExecutionLock,
|
||||
device_pool,
|
||||
ensure_daemon_running,
|
||||
submit_task_to_daemon,
|
||||
trace_store,
|
||||
)
|
||||
|
||||
# Seconds the spawned runner gets to finish its imports and open its log files
|
||||
# before the spawn is declared dead. Normal startup creates stdout.log within a
|
||||
@@ -136,8 +144,6 @@ def _validate_device_serial(device_serial: str) -> dict[str, Any] | None:
|
||||
validation is skipped rather than blocking task submission.
|
||||
"""
|
||||
try:
|
||||
from artemis.runtime import device_pool
|
||||
|
||||
# The shared validator fails open on an indeterminate/empty enumeration:
|
||||
# the task proceeds and fails downstream with a clear no-device error.
|
||||
detail = device_pool.validate_explicit_serial(device_serial)
|
||||
@@ -249,8 +255,6 @@ def mobile_run_task(
|
||||
# 3. Dispatch via unified Artemis Daemon scheduler if available (unless standalone forced)
|
||||
if os.environ.get("ARTEMIS_STANDALONE") != "1":
|
||||
try:
|
||||
from artemis.runtime import ensure_daemon_running, submit_task_to_daemon
|
||||
|
||||
is_running, base_url = ensure_daemon_running(timeout=2.0, wait_ready=True)
|
||||
if is_running:
|
||||
resp = submit_task_to_daemon(
|
||||
@@ -321,7 +325,6 @@ def mobile_run_task(
|
||||
pass
|
||||
|
||||
# When Daemon is running, refuse to launch a conflicting standalone runner on the same device
|
||||
import logging
|
||||
logging.getLogger("mcp_server").warning(
|
||||
f"Daemon dispatch failed for task '{task_desc}'. Aborting dispatch to prevent runner collision."
|
||||
)
|
||||
@@ -332,7 +335,6 @@ def mobile_run_task(
|
||||
"message": "Task rejected or timed out in Artemis Daemon. Standalone fallback blocked to prevent runner collision.",
|
||||
}
|
||||
except Exception as exc:
|
||||
import logging
|
||||
logging.getLogger("mcp_server").warning(
|
||||
f"Daemon dispatch failed: {exc}"
|
||||
)
|
||||
@@ -387,8 +389,6 @@ def mobile_run_task(
|
||||
env["ARTEMIS_DEVICE_ID"] = device_serial
|
||||
env[DeviceExecutionLock.QUEUE_TICKET_ENV] = queue_ticket
|
||||
try:
|
||||
from artemis.config.runtime import read_ipc_port
|
||||
|
||||
ipc_port = read_ipc_port()
|
||||
if ipc_port:
|
||||
env["ARTEMIS_IPC_PORT"] = str(ipc_port)
|
||||
|
||||
@@ -22,6 +22,9 @@ from typing import Any
|
||||
|
||||
def get_project_root() -> str:
|
||||
"""Returns the absolute path to the project root repository."""
|
||||
# Deliberate lazy import: this helper must keep working when the artemis
|
||||
# package (or its config bootstrap) is unavailable or broken, falling back
|
||||
# to filesystem-relative resolution below.
|
||||
try:
|
||||
from artemis.config.paths import ROOT_DIR
|
||||
|
||||
|
||||
@@ -104,6 +104,15 @@ def main() -> None:
|
||||
|
||||
from artemis.runtime.server_lifecycle import get_server_status, stop_server
|
||||
|
||||
def _arm_session_reconciler() -> None:
|
||||
# Entry-layer assembly: importing the admin console session repository
|
||||
# registers its orphan-session reconciler with the runtime, so a forced
|
||||
# stop below can mark sessions whose worker processes were killed.
|
||||
try:
|
||||
import apps.admin_console.database.repositories.session_repository # noqa: F401
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if args.status:
|
||||
st = get_server_status(args.port)
|
||||
if st["running"]:
|
||||
@@ -116,6 +125,7 @@ def main() -> None:
|
||||
|
||||
if args.stop:
|
||||
print(f"\033[1;33m🛑 Stopping Artemis server on port {args.port}...\033[0m")
|
||||
_arm_session_reconciler()
|
||||
ok, msg, pids = stop_server(args.port)
|
||||
print(f"\033[1;32m✓ {msg}\033[0m")
|
||||
return
|
||||
@@ -130,6 +140,7 @@ def main() -> None:
|
||||
# Check if restart requested
|
||||
if args.restart:
|
||||
print(f"\033[1;33m🔄 Restart requested. Recycling port {args.port}...\033[0m")
|
||||
_arm_session_reconciler()
|
||||
ok, msg, pids = stop_server(args.port)
|
||||
if pids:
|
||||
print(f"\033[1;32m✓ {msg}\033[0m\n")
|
||||
@@ -155,9 +166,11 @@ def main() -> None:
|
||||
|
||||
if choice == "2":
|
||||
print(f"\n\033[1;33m🔄 Restarting Artemis server on port {args.port}...\033[0m")
|
||||
_arm_session_reconciler()
|
||||
stop_server(args.port)
|
||||
elif choice == "3":
|
||||
print(f"\n\033[1;33m🛑 Stopping Artemis server on port {args.port}...\033[0m")
|
||||
_arm_session_reconciler()
|
||||
ok, msg, _ = stop_server(args.port)
|
||||
print(f"\033[1;32m✓ {msg}\033[0m")
|
||||
return
|
||||
|
||||
@@ -55,7 +55,7 @@ from artemis.interfaces.sdk.client import ArtemisClient, ConcurrencyMode
|
||||
from mcp_server.tools.device_state import mobile_get_device_state
|
||||
from mcp_server.tools.task_runner import mobile_run_task
|
||||
from mcp_server.tools.task_manager import mobile_manage_task
|
||||
from mcp_server.utils import trace_store
|
||||
from artemis.runtime import trace_store
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
|
||||
@@ -431,7 +431,7 @@ def test_stop_tasks_terminates_external_global_owner_and_preserves_local_waiter(
|
||||
"apps.admin_console.services.task_queue_service.session_repo.update_session_status"
|
||||
) as update_status,
|
||||
patch(
|
||||
"mcp_server.utils.trace_store.update_trace_status"
|
||||
"artemis.runtime.trace_store.update_trace_status"
|
||||
) as update_trace_status,
|
||||
):
|
||||
assert task_queue_service.stop_tasks(clear_all=False) is True
|
||||
@@ -845,7 +845,7 @@ def test_stop_tasks_by_session_id_targets_correct_task_among_multiple():
|
||||
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,
|
||||
patch("artemis.runtime.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
|
||||
|
||||
@@ -52,7 +52,7 @@ def test_legacy_jetski_tools_imports():
|
||||
|
||||
def test_legacy_jetski_utils_imports():
|
||||
from jetski.utils.trace_store import init_trace
|
||||
from mcp_server.utils.trace_store import init_trace as a_init
|
||||
from artemis.runtime.trace_store import init_trace as a_init
|
||||
|
||||
assert init_trace is a_init
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ from mcp_server.tools import (
|
||||
mobile_manage_task,
|
||||
mobile_run_task,
|
||||
)
|
||||
from mcp_server.utils import trace_store
|
||||
from artemis.runtime import trace_store
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -162,11 +162,11 @@ def test_mobile_run_task_dispatched_to_daemon(temp_trace_env, monkeypatch):
|
||||
monkeypatch.delenv("ARTEMIS_STANDALONE", raising=False)
|
||||
with (
|
||||
patch(
|
||||
"artemis.runtime.ensure_daemon_running",
|
||||
"mcp_server.tools.task_runner.ensure_daemon_running",
|
||||
return_value=(True, "http://127.0.0.1:8000"),
|
||||
),
|
||||
patch(
|
||||
"artemis.runtime.submit_task_to_daemon",
|
||||
"mcp_server.tools.task_runner.submit_task_to_daemon",
|
||||
return_value={"status": "started", "tasks": [{"session_id": "daemon-sid-1"}]},
|
||||
),
|
||||
patch("mcp_server.tools.task_runner.subprocess.Popen") as popen,
|
||||
@@ -399,8 +399,8 @@ def test_mobile_manage_task_stop_via_daemon(temp_trace_env, monkeypatch):
|
||||
trace_store.init_trace(trace_id, "Daemon task to stop", "Flash", "conv-stop")
|
||||
|
||||
with (
|
||||
patch("artemis.runtime.is_daemon_running", return_value=True),
|
||||
patch("artemis.runtime.stop_task_on_daemon", return_value=True) as mock_stop,
|
||||
patch("mcp_server.tools.task_manager.is_daemon_running", return_value=True),
|
||||
patch("mcp_server.tools.task_manager.stop_task_on_daemon", return_value=True) as mock_stop,
|
||||
):
|
||||
res = mobile_manage_task(action="stop", trace_id=trace_id)
|
||||
mock_stop.assert_called_once_with(trace_id)
|
||||
|
||||
@@ -28,7 +28,7 @@ from mcp_server.notifiers import (
|
||||
WebhookNotifier,
|
||||
notify,
|
||||
)
|
||||
from mcp_server.utils import trace_store
|
||||
from artemis.runtime import trace_store
|
||||
|
||||
|
||||
class DummyNotifier(BaseNotifier):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2026 Google LLC
|
||||
# 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.
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Unit tests for MCP trace store."""
|
||||
"""Unit tests for the shared trace store (artemis.runtime.trace_store)."""
|
||||
|
||||
import glob
|
||||
import logging
|
||||
@@ -23,7 +23,7 @@ import threading
|
||||
import uuid
|
||||
import pytest
|
||||
|
||||
from mcp_server.utils import trace_store
|
||||
from artemis.runtime import trace_store
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -139,7 +139,7 @@ def test_corrupt_status_is_quarantined_and_read_returns_none(temp_trace_env, cap
|
||||
with open(status_path, "w", encoding="utf-8") as f:
|
||||
f.write('{"status": "running", "trace_id"') # torn write
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="mcp_server.utils.trace_store"):
|
||||
with caplog.at_level(logging.WARNING, logger="artemis.runtime.trace_store"):
|
||||
assert trace_store.read_status(trace_id) is None
|
||||
|
||||
assert any("Corrupt status.json" in rec.message for rec in caplog.records)
|
||||
@@ -148,7 +148,7 @@ def test_corrupt_status_is_quarantined_and_read_returns_none(temp_trace_env, cap
|
||||
|
||||
|
||||
def test_missing_status_stays_silent(temp_trace_env, caplog):
|
||||
with caplog.at_level(logging.WARNING, logger="mcp_server.utils.trace_store"):
|
||||
with caplog.at_level(logging.WARNING, logger="artemis.runtime.trace_store"):
|
||||
assert trace_store.read_status(str(uuid.uuid4())) is None
|
||||
assert caplog.records == []
|
||||
|
||||
@@ -161,13 +161,13 @@ def test_update_on_corrupt_status_logs_and_drops(temp_trace_env, caplog):
|
||||
with open(status_path, "w", encoding="utf-8") as f:
|
||||
f.write("not json at all")
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="mcp_server.utils.trace_store"):
|
||||
with caplog.at_level(logging.WARNING, logger="artemis.runtime.trace_store"):
|
||||
assert trace_store.update_trace_status(trace_id, "completed") is None
|
||||
|
||||
assert any("Dropping status update" in rec.message for rec in caplog.records)
|
||||
# Updating a trace that never existed stays a silent no-op.
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.WARNING, logger="mcp_server.utils.trace_store"):
|
||||
with caplog.at_level(logging.WARNING, logger="artemis.runtime.trace_store"):
|
||||
assert trace_store.update_trace_status(str(uuid.uuid4()), "completed") is None
|
||||
assert not any("Dropping status update" in rec.message for rec in caplog.records)
|
||||
|
||||
Reference in new issue
Block a user