diff --git a/apps/admin_console/database/repositories/session_repository.py b/apps/admin_console/database/repositories/session_repository.py index a0ae1d1..b51d512 100644 --- a/apps/admin_console/database/repositories/session_repository.py +++ b/apps/admin_console/database/repositories/session_repository.py @@ -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) diff --git a/apps/admin_console/routers/sessions.py b/apps/admin_console/routers/sessions.py index 9f9aae1..3ad3e4b 100644 --- a/apps/admin_console/routers/sessions.py +++ b/apps/admin_console/routers/sessions.py @@ -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( diff --git a/apps/admin_console/services/task_queue_service.py b/apps/admin_console/services/task_queue_service.py index 4978f99..b10bf08 100644 --- a/apps/admin_console/services/task_queue_service.py +++ b/apps/admin_console/services/task_queue_service.py @@ -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( diff --git a/artemis/interfaces/cli/commands/server_lifecycle.py b/artemis/interfaces/cli/commands/server_lifecycle.py index e745861..7a1ab59 100644 --- a/artemis/interfaces/cli/commands/server_lifecycle.py +++ b/artemis/interfaces/cli/commands/server_lifecycle.py @@ -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( diff --git a/artemis/interfaces/cli/commands/ui.py b/artemis/interfaces/cli/commands/ui.py index b9780c3..86c6e1b 100644 --- a/artemis/interfaces/cli/commands/ui.py +++ b/artemis/interfaces/cli/commands/ui.py @@ -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 ` invocation. from apps.admin_console.server import run_ui_server run_ui_server(host=host, port=port, reload=reload) diff --git a/artemis/runtime/server_lifecycle.py b/artemis/runtime/server_lifecycle.py index 2ad40db..c8ce575 100644 --- a/artemis/runtime/server_lifecycle.py +++ b/artemis/runtime/server_lifecycle.py @@ -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) diff --git a/mcp_server/utils/trace_store.py b/artemis/runtime/trace_store.py similarity index 99% rename from mcp_server/utils/trace_store.py rename to artemis/runtime/trace_store.py index 3938983..d3d41cc 100644 --- a/mcp_server/utils/trace_store.py +++ b/artemis/runtime/trace_store.py @@ -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) diff --git a/mcp_server/README.md b/mcp_server/README.md index e994757..7e12622 100644 --- a/mcp_server/README.md +++ b/mcp_server/README.md @@ -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: diff --git a/mcp_server/background/task_runner.py b/mcp_server/background/task_runner.py index a50f14c..13f6665 100644 --- a/mcp_server/background/task_runner.py +++ b/mcp_server/background/task_runner.py @@ -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: diff --git a/mcp_server/notifiers/file.py b/mcp_server/notifiers/file.py index 9e4a635..3781ce5 100644 --- a/mcp_server/notifiers/file.py +++ b/mcp_server/notifiers/file.py @@ -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") diff --git a/mcp_server/tools/inspect_trace.py b/mcp_server/tools/inspect_trace.py index 6809d8c..6ee5d88 100644 --- a/mcp_server/tools/inspect_trace.py +++ b/mcp_server/tools/inspect_trace.py @@ -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, diff --git a/mcp_server/tools/task_manager.py b/mcp_server/tools/task_manager.py index d5f83b7..41ac4c9 100644 --- a/mcp_server/tools/task_manager.py +++ b/mcp_server/tools/task_manager.py @@ -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: diff --git a/mcp_server/tools/task_runner.py b/mcp_server/tools/task_runner.py index 2b42141..195163e 100644 --- a/mcp_server/tools/task_runner.py +++ b/mcp_server/tools/task_runner.py @@ -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) diff --git a/mcp_server/utils/env_utils.py b/mcp_server/utils/env_utils.py index 941c512..099e50a 100644 --- a/mcp_server/utils/env_utils.py +++ b/mcp_server/utils/env_utils.py @@ -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 diff --git a/scripts/run_ui.py b/scripts/run_ui.py index 7018415..4c25949 100755 --- a/scripts/run_ui.py +++ b/scripts/run_ui.py @@ -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 diff --git a/tests/e2e/test_multi_port_end_to_end.py b/tests/e2e/test_multi_port_end_to_end.py index a76a441..aaf0a1c 100644 --- a/tests/e2e/test_multi_port_end_to_end.py +++ b/tests/e2e/test_multi_port_end_to_end.py @@ -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) diff --git a/tests/unit/admin_console/test_task_queue_service.py b/tests/unit/admin_console/test_task_queue_service.py index dd64ee4..aa390ca 100644 --- a/tests/unit/admin_console/test_task_queue_service.py +++ b/tests/unit/admin_console/test_task_queue_service.py @@ -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 diff --git a/tests/unit/mcp/test_backward_compatibility.py b/tests/unit/mcp/test_backward_compatibility.py index 507a8b1..7991840 100644 --- a/tests/unit/mcp/test_backward_compatibility.py +++ b/tests/unit/mcp/test_backward_compatibility.py @@ -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 diff --git a/tests/unit/mcp/test_mcp_tools.py b/tests/unit/mcp/test_mcp_tools.py index a31cffd..74c98c8 100644 --- a/tests/unit/mcp/test_mcp_tools.py +++ b/tests/unit/mcp/test_mcp_tools.py @@ -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) diff --git a/tests/unit/mcp/test_notifiers.py b/tests/unit/mcp/test_notifiers.py index a43633f..4993ccc 100644 --- a/tests/unit/mcp/test_notifiers.py +++ b/tests/unit/mcp/test_notifiers.py @@ -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): diff --git a/tests/unit/mcp/test_trace_store.py b/tests/unit/runtime/test_trace_store.py similarity index 94% rename from tests/unit/mcp/test_trace_store.py rename to tests/unit/runtime/test_trace_store.py index eaff9ab..a048571 100644 --- a/tests/unit/mcp/test_trace_store.py +++ b/tests/unit/runtime/test_trace_store.py @@ -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)