From 2c865bfe67bc938b077ecaf15ca4540be925a820 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Thu, 20 Nov 2025 19:57:57 -0800 Subject: [PATCH 1/5] Fix HTTP instance routing and per-project session IDs --- .../Editor/Helpers/ProjectIdentityUtility.cs | 67 +++++++++- Server/plugin_hub.py | 13 +- Server/resources/unity_instances.py | 99 +++++++++++--- .../test_instance_routing_comprehensive.py | 126 ++++++++++++++++++ Server/tools/set_active_instance.py | 46 ++++++- 5 files changed, 319 insertions(+), 32 deletions(-) diff --git a/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs b/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs index 8c1854a3..838bf32c 100644 --- a/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs +++ b/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs @@ -16,6 +16,7 @@ namespace MCPForUnity.Editor.Helpers internal static class ProjectIdentityUtility { private const string SessionPrefKey = EditorPrefKeys.WebSocketSessionId; + private static bool _legacyKeyCleared; private static string _cachedProjectName = "Unknown"; private static string _cachedProjectHash = "default"; private static bool _cacheScheduled; @@ -69,6 +70,7 @@ private static void UpdateIdentityCache() /// public static string GetProjectHash() { + EnsureIdentityCache(); return _cachedProjectHash; } @@ -122,16 +124,21 @@ private static string ComputeProjectName(string dataPath) /// /// Retrieves a persistent session id for the plugin, creating one if absent. + /// The session id is unique per project (scoped by project hash). /// public static string GetOrCreateSessionId() { try { - string sessionId = EditorPrefs.GetString(SessionPrefKey, string.Empty); + // Make the session ID project-specific by including the project hash in the key + string projectHash = GetProjectHash(); + string projectSpecificKey = $"{SessionPrefKey}_{projectHash}"; + + string sessionId = EditorPrefs.GetString(projectSpecificKey, string.Empty); if (string.IsNullOrEmpty(sessionId)) { sessionId = Guid.NewGuid().ToString(); - EditorPrefs.SetString(SessionPrefKey, sessionId); + EditorPrefs.SetString(projectSpecificKey, sessionId); } return sessionId; } @@ -149,9 +156,19 @@ public static void ResetSessionId() { try { - if (EditorPrefs.HasKey(SessionPrefKey)) + // Clear the project-specific session ID + string projectHash = GetProjectHash(); + string projectSpecificKey = $"{SessionPrefKey}_{projectHash}"; + + if (EditorPrefs.HasKey(projectSpecificKey)) + { + EditorPrefs.DeleteKey(projectSpecificKey); + } + + if (!_legacyKeyCleared && EditorPrefs.HasKey(SessionPrefKey)) { EditorPrefs.DeleteKey(SessionPrefKey); + _legacyKeyCleared = true; } } catch @@ -159,5 +176,49 @@ public static void ResetSessionId() // Ignore } } + + private static void EnsureIdentityCache() + { + // When Application.dataPath is unavailable (e.g., batch mode) we fall back to + // hashing the current working directory/Assets path so each project still + // derives a deterministic, per-project session id rather than sharing "default". + if (!string.IsNullOrEmpty(_cachedProjectHash) && _cachedProjectHash != "default") + { + return; + } + + UpdateIdentityCache(); + + if (!string.IsNullOrEmpty(_cachedProjectHash) && _cachedProjectHash != "default") + { + return; + } + + string fallback = TryComputeFallbackProjectHash(); + if (!string.IsNullOrEmpty(fallback)) + { + _cachedProjectHash = fallback; + } + } + + private static string TryComputeFallbackProjectHash() + { + try + { + string workingDirectory = Directory.GetCurrentDirectory(); + if (string.IsNullOrEmpty(workingDirectory)) + { + return "default"; + } + + // Normalise trailing separators so hashes remain stable + workingDirectory = workingDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + return ComputeProjectHash(Path.Combine(workingDirectory, "Assets")); + } + catch + { + return "default"; + } + } } } diff --git a/Server/plugin_hub.py b/Server/plugin_hub.py index 4ca92ae9..a577daf3 100644 --- a/Server/plugin_hub.py +++ b/Server/plugin_hub.py @@ -222,10 +222,19 @@ async def _resolve_session_id(cls, unity_instance: Optional[str]) -> str: retry_ms = float(getattr(config, "reload_retry_ms", 250)) sleep_seconds = max(0.05, retry_ms / 1000.0) + # Allow callers to provide either just the hash or Name@hash + target_hash: Optional[str] = None + if unity_instance: + if "@" in unity_instance: + _, _, suffix = unity_instance.rpartition("@") + target_hash = suffix or None + else: + target_hash = unity_instance + async def _try_once() -> Optional[str]: # Prefer a specific Unity instance if one was requested - if unity_instance: - session_id = await cls._registry.get_session_id_by_hash(unity_instance) + if target_hash: + session_id = await cls._registry.get_session_id_by_hash(target_hash) if session_id: return session_id diff --git a/Server/resources/unity_instances.py b/Server/resources/unity_instances.py index c716ea35..331129d8 100644 --- a/Server/resources/unity_instances.py +++ b/Server/resources/unity_instances.py @@ -6,6 +6,12 @@ from fastmcp import Context from registry import mcp_for_unity_resource from unity_connection import get_unity_connection_pool +from plugin_hub import PluginHub +from unity_transport import _is_http_transport as _core_is_http_transport + + +def _is_http_transport() -> bool: + return _core_is_http_transport() @mcp_for_unity_resource( @@ -20,12 +26,13 @@ async def unity_instances(ctx: Context) -> dict[str, Any]: Returns information about each instance including: - id: Unique identifier (ProjectName@hash) - name: Project name - - path: Full project path + - path: Full project path (stdio only) - hash: 8-character hash of project path - - port: TCP port number - - status: Current status (running, reloading, etc.) - - last_heartbeat: Last heartbeat timestamp + - port: TCP port number (stdio only) + - status: Current status (running, reloading, etc.) (stdio only) + - last_heartbeat: Last heartbeat timestamp (stdio only) - unity_version: Unity version (if available) + - connected_at: Connection timestamp (HTTP only) Returns: Dictionary containing list of instances and metadata @@ -33,29 +40,77 @@ async def unity_instances(ctx: Context) -> dict[str, Any]: await ctx.info("Listing Unity instances") try: - pool = get_unity_connection_pool() - instances = pool.discover_all_instances(force_refresh=False) + if _is_http_transport(): + # HTTP/WebSocket transport: query PluginHub + sessions_data = await PluginHub.get_sessions() + sessions = sessions_data.get("sessions", {}) - # Check for duplicate project names - name_counts = {} - for inst in instances: - name_counts[inst.name] = name_counts.get(inst.name, 0) + 1 + instances = [] + for session_id, session_info in sessions.items(): + project = session_info.get("project") or session_info.get("project_name") + project_hash = session_info.get("hash") - duplicates = [name for name, count in name_counts.items() if count > 1] + if not project or not project_hash: + raise ValueError( + "PluginHub session missing required 'project' or 'hash' fields." + ) - result = { - "success": True, - "instance_count": len(instances), - "instances": [inst.to_dict() for inst in instances], - } + instances.append({ + "id": f"{project}@{project_hash}", + "name": project, + "hash": project_hash, + "unity_version": session_info.get("unity_version"), + "connected_at": session_info.get("connected_at"), + "session_id": session_id, + }) + + # Check for duplicate project names + name_counts = {} + for inst in instances: + name_counts[inst["name"]] = name_counts.get(inst["name"], 0) + 1 + + duplicates = [name for name, count in name_counts.items() if count > 1] + + result = { + "success": True, + "transport": "http", + "instance_count": len(instances), + "instances": instances, + } + + if duplicates: + result["warning"] = ( + f"Multiple instances found with duplicate project names: {duplicates}. " + f"Use full format (e.g., 'ProjectName@hash') to specify which instance." + ) + + return result + else: + # Stdio/TCP transport: query connection pool + pool = get_unity_connection_pool() + instances = pool.discover_all_instances(force_refresh=False) + + # Check for duplicate project names + name_counts = {} + for inst in instances: + name_counts[inst.name] = name_counts.get(inst.name, 0) + 1 + + duplicates = [name for name, count in name_counts.items() if count > 1] + + result = { + "success": True, + "transport": "stdio", + "instance_count": len(instances), + "instances": [inst.to_dict() for inst in instances], + } - if duplicates: - result["warning"] = ( - f"Multiple instances found with duplicate project names: {duplicates}. " - f"Use full format (e.g., 'ProjectName@hash') to specify which instance." - ) + if duplicates: + result["warning"] = ( + f"Multiple instances found with duplicate project names: {duplicates}. " + f"Use full format (e.g., 'ProjectName@hash') to specify which instance." + ) - return result + return result except Exception as e: await ctx.error(f"Error listing Unity instances: {e}") diff --git a/Server/tests/integration/test_instance_routing_comprehensive.py b/Server/tests/integration/test_instance_routing_comprehensive.py index 089770b6..473b00fc 100644 --- a/Server/tests/integration/test_instance_routing_comprehensive.py +++ b/Server/tests/integration/test_instance_routing_comprehensive.py @@ -16,6 +16,7 @@ from unity_instance_middleware import UnityInstanceMiddleware from tools import get_unity_instance_from_context +from tools.set_active_instance import set_active_instance as set_active_instance_tool class TestInstanceRoutingBasics: @@ -168,6 +169,131 @@ def test_tool_category_respects_active_instance(self, tool_category, tool_names) pass # Placeholder for category-level test +class TestInstanceRoutingHTTP: + """Validate HTTP-specific routing helpers.""" + + @pytest.mark.asyncio + async def test_set_active_instance_http_transport(self, monkeypatch): + """set_active_instance should enumerate PluginHub sessions under HTTP.""" + middleware = UnityInstanceMiddleware() + ctx = Mock(spec=Context) + ctx.session_id = "http-session" + state_storage = {} + ctx.set_state = Mock(side_effect=lambda k, v: state_storage.__setitem__(k, v)) + ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) + + monkeypatch.setenv("UNITY_MCP_TRANSPORT", "http") + fake_sessions = { + "sessions": { + "sess-1": { + "project": "Ramble", + "hash": "8e29de57", + "unity_version": "6000.2.10f1", + "connected_at": "2025-11-21T03:30:03.682353+00:00", + } + } + } + monkeypatch.setattr( + "tools.set_active_instance.PluginHub.get_sessions", + AsyncMock(return_value=fake_sessions), + ) + monkeypatch.setattr( + "tools.set_active_instance.get_unity_instance_middleware", + lambda: middleware, + ) + + result = await set_active_instance_tool(ctx, "Ramble@8e29de57") + + assert result["success"] is True + assert middleware.get_active_instance(ctx) == "Ramble@8e29de57" + + @pytest.mark.asyncio + async def test_set_active_instance_http_hash_only(self, monkeypatch): + """Hash-only selection should resolve via PluginHub registry.""" + middleware = UnityInstanceMiddleware() + ctx = Mock(spec=Context) + ctx.session_id = "http-session-2" + state_storage = {} + ctx.set_state = Mock(side_effect=lambda k, v: state_storage.__setitem__(k, v)) + ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) + + monkeypatch.setenv("UNITY_MCP_TRANSPORT", "http") + fake_sessions = { + "sessions": { + "sess-99": { + "project": "UnityMCPTests", + "hash": "cc8756d4", + "unity_version": "2021.3.45f2", + "connected_at": "2025-11-21T03:37:01.501022+00:00", + } + } + } + monkeypatch.setattr( + "tools.set_active_instance.PluginHub.get_sessions", + AsyncMock(return_value=fake_sessions), + ) + monkeypatch.setattr( + "tools.set_active_instance.get_unity_instance_middleware", + lambda: middleware, + ) + + result = await set_active_instance_tool(ctx, "cc8756d4") + + assert result["success"] is True + assert middleware.get_active_instance(ctx) == "UnityMCPTests@cc8756d4" + + @pytest.mark.asyncio + async def test_set_active_instance_http_hash_missing(self, monkeypatch): + """Unknown hashes should surface a clear error.""" + middleware = UnityInstanceMiddleware() + ctx = Mock(spec=Context) + ctx.session_id = "http-session-3" + + monkeypatch.setenv("UNITY_MCP_TRANSPORT", "http") + fake_sessions = {"sessions": {}} + monkeypatch.setattr( + "tools.set_active_instance.PluginHub.get_sessions", + AsyncMock(return_value=fake_sessions), + ) + monkeypatch.setattr( + "tools.set_active_instance.get_unity_instance_middleware", + lambda: middleware, + ) + + result = await set_active_instance_tool(ctx, "deadbeef") + + assert result["success"] is False + assert "No Unity instances" in result["error"] + + @pytest.mark.asyncio + async def test_set_active_instance_http_hash_ambiguous(self, monkeypatch): + """Ambiguous hash prefixes should mirror stdio error messaging.""" + middleware = UnityInstanceMiddleware() + ctx = Mock(spec=Context) + ctx.session_id = "http-session-4" + + monkeypatch.setenv("UNITY_MCP_TRANSPORT", "http") + fake_sessions = { + "sessions": { + "sess-a": {"project": "ProjA", "hash": "abc12345"}, + "sess-b": {"project": "ProjB", "hash": "abc98765"}, + } + } + monkeypatch.setattr( + "tools.set_active_instance.PluginHub.get_sessions", + AsyncMock(return_value=fake_sessions), + ) + monkeypatch.setattr( + "tools.set_active_instance.get_unity_instance_middleware", + lambda: middleware, + ) + + result = await set_active_instance_tool(ctx, "abc") + + assert result["success"] is False + assert "matches multiple instances" in result["error"] + + class TestInstanceRoutingRaceConditions: """Test for race conditions and timing issues.""" diff --git a/Server/tools/set_active_instance.py b/Server/tools/set_active_instance.py index 9086965a..1319641a 100644 --- a/Server/tools/set_active_instance.py +++ b/Server/tools/set_active_instance.py @@ -1,21 +1,52 @@ from typing import Annotated, Any +from types import SimpleNamespace from fastmcp import Context from registry import mcp_for_unity_tool from unity_connection import get_unity_connection_pool from unity_instance_middleware import get_unity_instance_middleware +from plugin_hub import PluginHub +from unity_transport import _is_http_transport as _core_is_http_transport + + +def _is_http_transport() -> bool: + # Delegate to transport helper so detection stays consistent across modules. + return _core_is_http_transport() @mcp_for_unity_tool( description="Set the active Unity instance for this client/session. Accepts Name@hash or hash." ) -def set_active_instance( +async def set_active_instance( ctx: Context, instance: Annotated[str, "Target instance (Name@hash or hash prefix)"] ) -> dict[str, Any]: - # Discover running instances - pool = get_unity_connection_pool() - instances = pool.discover_all_instances(force_refresh=True) + # Discover running instances based on transport + if _is_http_transport(): + sessions_data = await PluginHub.get_sessions() + sessions = sessions_data.get("sessions", {}) if isinstance(sessions_data, dict) else {} + instances = [] + for session_id, session in sessions.items(): + project = session.get("project") or session.get("project_name") or "Unknown" + hash_value = session.get("hash") + if not hash_value: + continue + inst_id = f"{project}@{hash_value}" + instances.append(SimpleNamespace( + id=inst_id, + hash=hash_value, + name=project, + session_id=session_id, + )) + else: + pool = get_unity_connection_pool() + instances = pool.discover_all_instances(force_refresh=True) + + if not instances: + return { + "success": False, + "error": "No Unity instances are currently connected. Start Unity and press 'Start Session'." + } ids = {inst.id: inst for inst in instances} hashes = {} for inst in instances: @@ -23,7 +54,12 @@ def set_active_instance( hashes.setdefault(inst.hash, inst) # Disallow plain names to ensure determinism - value = instance.strip() + value = (instance or "").strip() + if not value: + return { + "success": False, + "error": "Instance identifier must not be empty. Specify Name@hash or a unique hash prefix." + } resolved = None if "@" in value: resolved = ids.get(value) From b179fbbbee3a261809797d4b1e7f03ed88266ddd Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Fri, 21 Nov 2025 01:38:43 -0400 Subject: [PATCH 2/5] Drop confusing log message --- Server/src/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Server/src/main.py b/Server/src/main.py index 56fd2113..00a4c527 100644 --- a/Server/src/main.py +++ b/Server/src/main.py @@ -99,8 +99,6 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: http_port = int(os.environ.get("UNITY_MCP_HTTP_PORT", "8080")) logger.info( f"HTTP tool registry will be available on http://{http_host}:{http_port}") - else: - logger.info("HTTP server disabled - using stdio transport") global _plugin_registry if _plugin_registry is None: From 139d32acab21fc71475abdbbbe92b8cf19e54962 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Fri, 21 Nov 2025 01:44:32 -0400 Subject: [PATCH 3/5] Ensure lock file references later version of uvicorn with key fixes --- Server/uv.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Server/uv.lock b/Server/uv.lock index 9c354b61..26152be7 100644 --- a/Server/uv.lock +++ b/Server/uv.lock @@ -722,7 +722,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, { name = "tomli", specifier = ">=2.3.0" }, - { name = "uvicorn", specifier = ">=0.24.0" }, + { name = "uvicorn", specifier = ">=0.35.0" }, ] provides-extras = ["dev"] @@ -1487,16 +1487,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568, upload-time = "2024-12-15T13:33:30.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, ] [[package]] From cd576197094d566960956635c335f867dcf0bd13 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Fri, 21 Nov 2025 02:28:12 -0400 Subject: [PATCH 4/5] Fix test imports --- Server/src/services/tools/manage_script.py | 16 +++++++------- Server/test_telemetry.py | 6 ++--- .../test_domain_reload_resilience.py | 15 ++++++------- .../test_edit_normalization_and_noop.py | 16 +++++++------- .../test_edit_strict_and_warnings.py | 8 +++---- .../integration/test_find_in_file_minimal.py | 4 ++-- Server/tests/integration/test_get_sha.py | 6 ++--- .../test_improved_anchor_matching.py | 2 +- .../test_instance_routing_comprehensive.py | 22 +++++++++---------- .../test_instance_targeting_resolution.py | 12 +++++----- .../test_manage_asset_json_parsing.py | 14 ++++++------ .../test_manage_asset_param_coercion.py | 2 +- .../test_manage_gameobject_param_coercion.py | 2 +- .../integration/test_manage_script_uri.py | 10 ++++----- .../integration/test_read_console_truncate.py | 12 +++++----- .../integration/test_read_resource_minimal.py | 4 ++-- .../tests/integration/test_resources_api.py | 4 ++-- Server/tests/integration/test_script_tools.py | 18 +++++++-------- .../test_telemetry_endpoint_validation.py | 9 ++++---- .../test_telemetry_queue_worker.py | 2 +- .../integration/test_telemetry_subaction.py | 10 ++++----- .../test_validate_script_summary.py | 6 ++--- 22 files changed, 100 insertions(+), 100 deletions(-) diff --git a/Server/src/services/tools/manage_script.py b/Server/src/services/tools/manage_script.py index 29b6d48f..36baf488 100644 --- a/Server/src/services/tools/manage_script.py +++ b/Server/src/services/tools/manage_script.py @@ -107,7 +107,7 @@ def _needs_normalization(arr: list[dict[str, Any]]) -> bool: if _needs_normalization(edits): # Read file to support index->line/col conversion when needed read_resp = await send_with_unity_instance( - unity_connection.async_send_command_with_retry, + transport.legacy.unity_connection.async_send_command_with_retry, unity_instance, "manage_script", { @@ -313,7 +313,7 @@ def _le(a: tuple[int, int], b: tuple[int, int]) -> bool: } params = {k: v for k, v in params.items() if v is not None} resp = await send_with_unity_instance( - unity_connection.async_send_command_with_retry, + transport.legacy.unity_connection.async_send_command_with_retry, unity_instance, "manage_script", params, @@ -349,7 +349,7 @@ async def _flip_async(): st = _latest_status() if st and st.get("reloading"): return - await unity_connection.async_send_command_with_retry( + await transport.legacy.unity_connection.async_send_command_with_retry( "execute_menu_item", {"menuPath": "MCP/Flip Reload Sentinel"}, max_retries=0, @@ -402,7 +402,7 @@ async def create_script( params["contentsEncoded"] = True params = {k: v for k, v in params.items() if v is not None} resp = await send_with_unity_instance( - unity_connection.async_send_command_with_retry, + transport.legacy.unity_connection.async_send_command_with_retry, unity_instance, "manage_script", params, @@ -423,7 +423,7 @@ async def delete_script( return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} params = {"action": "delete", "name": name, "path": directory} resp = await send_with_unity_instance( - unity_connection.async_send_command_with_retry, + transport.legacy.unity_connection.async_send_command_with_retry, unity_instance, "manage_script", params, @@ -454,7 +454,7 @@ async def validate_script( "level": level, } resp = await send_with_unity_instance( - unity_connection.async_send_command_with_retry, + transport.legacy.unity_connection.async_send_command_with_retry, unity_instance, "manage_script", params, @@ -507,7 +507,7 @@ async def manage_script( params = {k: v for k, v in params.items() if v is not None} response = await send_with_unity_instance( - unity_connection.async_send_command_with_retry, + transport.legacy.unity_connection.async_send_command_with_retry, unity_instance, "manage_script", params, @@ -581,7 +581,7 @@ async def get_sha( name, directory = _split_uri(uri) params = {"action": "get_sha", "name": name, "path": directory} resp = await send_with_unity_instance( - unity_connection.async_send_command_with_retry, + transport.legacy.unity_connection.async_send_command_with_retry, unity_instance, "manage_script", params, diff --git a/Server/test_telemetry.py b/Server/test_telemetry.py index 3e4b7ce7..d6938d33 100644 --- a/Server/test_telemetry.py +++ b/Server/test_telemetry.py @@ -17,7 +17,7 @@ def test_telemetry_basic(): # Avoid stdout noise in tests try: - from telemetry import ( + from core.telemetry import ( get_telemetry, record_telemetry, record_milestone, RecordType, MilestoneType, is_telemetry_enabled ) @@ -73,7 +73,7 @@ def test_telemetry_disabled(): import telemetry importlib.reload(telemetry) - from telemetry import is_telemetry_enabled, record_telemetry, RecordType + core.telemetry import is_telemetry_enabled, record_telemetry, RecordType _ = is_telemetry_enabled() @@ -95,7 +95,7 @@ def test_data_storage(): # Silent for tests try: - from telemetry import get_telemetry + core.telemetry import get_telemetry collector = get_telemetry() data_dir = collector.config.data_dir diff --git a/Server/tests/integration/test_domain_reload_resilience.py b/Server/tests/integration/test_domain_reload_resilience.py index b1258c7c..64fdf8fe 100644 --- a/Server/tests/integration/test_domain_reload_resilience.py +++ b/Server/tests/integration/test_domain_reload_resilience.py @@ -17,7 +17,7 @@ async def test_plugin_hub_waits_for_reconnection_during_reload(): """Test that PluginHub._resolve_session_id waits for plugin reconnection.""" # Import after conftest stubs are set up from transport.plugin_hub import PluginHub - from plugin_registry import PluginRegistry, PluginSession + from transport.plugin_registry import PluginRegistry, PluginSession # Create a mock registry mock_registry = AsyncMock(spec=PluginRegistry) @@ -72,7 +72,7 @@ async def mock_list_sessions(): async def test_plugin_hub_fails_after_timeout(): """Test that PluginHub._resolve_session_id eventually times out if plugin never reconnects.""" from transport.plugin_hub import PluginHub - from plugin_registry import PluginRegistry + from transport.plugin_registry import PluginRegistry # Create a mock registry that never returns sessions mock_registry = AsyncMock(spec=PluginRegistry) @@ -89,7 +89,7 @@ async def mock_list_sessions(): PluginHub._lock = asyncio.Lock() # Temporarily override config for a short timeout - with patch('plugin_hub.config') as mock_config: + with patch('transport.plugin_hub.config') as mock_config: mock_config.reload_max_retries = 3 # Only 3 retries mock_config.reload_retry_ms = 10 # 10ms between retries @@ -114,7 +114,7 @@ async def test_read_console_during_simulated_reload(monkeypatch): 3. The plugin disconnects and reconnects during those calls """ # Setup tools - from tools.read_console import read_console + from services.tools.read_console import read_console call_count = [0] @@ -128,9 +128,9 @@ async def fake_send_command(*args, **kwargs): } # Patch the async_send_command_with_retry directly - import tools.read_console + import services.tools.read_console monkeypatch.setattr( - tools.read_console, + services.tools.read_console, "async_send_command_with_retry", fake_send_command ) @@ -162,7 +162,7 @@ async def fake_send_command(*args, **kwargs): async def test_plugin_hub_respects_unity_instance_preference(): """Test that _resolve_session_id prefers a specific Unity instance if requested.""" from transport.plugin_hub import PluginHub - from plugin_registry import PluginRegistry, PluginSession + from transport.plugin_registry import PluginRegistry, PluginSession # Create a mock registry with two sessions mock_registry = AsyncMock(spec=PluginRegistry) @@ -222,4 +222,3 @@ async def mock_get_session_by_hash(project_hash): # Clean up: restore original PluginHub state PluginHub._registry = original_registry PluginHub._lock = original_lock - diff --git a/Server/tests/integration/test_edit_normalization_and_noop.py b/Server/tests/integration/test_edit_normalization_and_noop.py index 04b51c43..959d8521 100644 --- a/Server/tests/integration/test_edit_normalization_and_noop.py +++ b/Server/tests/integration/test_edit_normalization_and_noop.py @@ -14,9 +14,9 @@ def deco(fn): self.tools[fn.__name__] = fn; return fn def setup_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration - import tools.manage_script + import services.tools.manage_script # Get the registered tools from the registry - from registry import get_registered_tools + from services.registry import get_registered_tools tools = get_registered_tools() # Add all script-related tools to our dummy MCP for tool_info in tools: @@ -39,7 +39,7 @@ async def fake_send(cmd, params, **kwargs): # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) @@ -73,7 +73,7 @@ async def fake_read(cmd, params, **kwargs): # Override unity_connection for this read normalization case monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_read, ) @@ -100,7 +100,7 @@ async def fake_send(cmd, params, **kwargs): # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) @@ -124,9 +124,9 @@ async def test_atomic_multi_span_and_relaxed(monkeypatch): apply_text = tools_text["apply_text_edits"] tools_struct = DummyMCP() # Import the tools module to trigger decorator registration - import tools.script_apply_edits + import services.tools.script_apply_edits # Get the registered tools from the registry - from registry import get_registered_tools + from services.registry import get_registered_tools tools = get_registered_tools() # Add all script-related tools to our dummy MCP for tool_info in tools: @@ -148,7 +148,7 @@ async def fake_send(cmd, params, **kwargs): # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) diff --git a/Server/tests/integration/test_edit_strict_and_warnings.py b/Server/tests/integration/test_edit_strict_and_warnings.py index 061c77b9..1ae3c03a 100644 --- a/Server/tests/integration/test_edit_strict_and_warnings.py +++ b/Server/tests/integration/test_edit_strict_and_warnings.py @@ -14,8 +14,8 @@ def deco(fn): self.tools[fn.__name__] = fn; return fn def setup_tools(): mcp = DummyMCP() # Import tools to trigger decorator-based registration - import tools.manage_script - from registry import get_registered_tools + import services.tools.manage_script + from services.registry import get_registered_tools for tool_info in get_registered_tools(): name = tool_info['name'] if any(k in name for k in ['script', 'apply_text', 'create_script', 'delete_script', 'validate_script', 'get_sha']): @@ -34,7 +34,7 @@ async def fake_send(cmd, params, **kwargs): import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) @@ -68,7 +68,7 @@ async def fake_send(cmd, params, **kwargs): import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) diff --git a/Server/tests/integration/test_find_in_file_minimal.py b/Server/tests/integration/test_find_in_file_minimal.py index 399deef5..30a6710a 100644 --- a/Server/tests/integration/test_find_in_file_minimal.py +++ b/Server/tests/integration/test_find_in_file_minimal.py @@ -19,9 +19,9 @@ def deco(fn): def resource_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration - import tools.resource_tools + import services.tools.resource_tools # Get the registered tools from the registry - from registry import get_registered_tools + from services.registry import get_registered_tools tools = get_registered_tools() # Add all resource-related tools to our dummy MCP for tool_info in tools: diff --git a/Server/tests/integration/test_get_sha.py b/Server/tests/integration/test_get_sha.py index b2e1f261..cb109bd5 100644 --- a/Server/tests/integration/test_get_sha.py +++ b/Server/tests/integration/test_get_sha.py @@ -17,9 +17,9 @@ def deco(fn): def setup_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration - import tools.manage_script + import services.tools.manage_script # Get the registered tools from the registry - from registry import get_registered_tools + from services.registry import get_registered_tools tools = get_registered_tools() # Add all script-related tools to our dummy MCP for tool_info in tools: @@ -44,7 +44,7 @@ async def fake_send(cmd, params, **kwargs): # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) diff --git a/Server/tests/integration/test_improved_anchor_matching.py b/Server/tests/integration/test_improved_anchor_matching.py index 4be48dd7..19a49ae4 100644 --- a/Server/tests/integration/test_improved_anchor_matching.py +++ b/Server/tests/integration/test_improved_anchor_matching.py @@ -6,7 +6,7 @@ import pytest -import tools.script_apply_edits as script_apply_edits_module +import services.tools.script_apply_edits as script_apply_edits_module def test_improved_anchor_matching(): diff --git a/Server/tests/integration/test_instance_routing_comprehensive.py b/Server/tests/integration/test_instance_routing_comprehensive.py index 473b00fc..cdd5157d 100644 --- a/Server/tests/integration/test_instance_routing_comprehensive.py +++ b/Server/tests/integration/test_instance_routing_comprehensive.py @@ -14,9 +14,9 @@ from unittest.mock import AsyncMock, Mock, MagicMock, patch from fastmcp import Context -from unity_instance_middleware import UnityInstanceMiddleware -from tools import get_unity_instance_from_context -from tools.set_active_instance import set_active_instance as set_active_instance_tool +from transport.unity_instance_middleware import UnityInstanceMiddleware +from services.tools import get_unity_instance_from_context +from services.tools.set_active_instance import set_active_instance as set_active_instance_tool class TestInstanceRoutingBasics: @@ -194,11 +194,11 @@ async def test_set_active_instance_http_transport(self, monkeypatch): } } monkeypatch.setattr( - "tools.set_active_instance.PluginHub.get_sessions", + "services.tools.set_active_instance.PluginHub.get_sessions", AsyncMock(return_value=fake_sessions), ) monkeypatch.setattr( - "tools.set_active_instance.get_unity_instance_middleware", + "services.tools.set_active_instance.get_unity_instance_middleware", lambda: middleware, ) @@ -229,11 +229,11 @@ async def test_set_active_instance_http_hash_only(self, monkeypatch): } } monkeypatch.setattr( - "tools.set_active_instance.PluginHub.get_sessions", + "services.tools.set_active_instance.PluginHub.get_sessions", AsyncMock(return_value=fake_sessions), ) monkeypatch.setattr( - "tools.set_active_instance.get_unity_instance_middleware", + "services.tools.set_active_instance.get_unity_instance_middleware", lambda: middleware, ) @@ -252,11 +252,11 @@ async def test_set_active_instance_http_hash_missing(self, monkeypatch): monkeypatch.setenv("UNITY_MCP_TRANSPORT", "http") fake_sessions = {"sessions": {}} monkeypatch.setattr( - "tools.set_active_instance.PluginHub.get_sessions", + "services.tools.set_active_instance.PluginHub.get_sessions", AsyncMock(return_value=fake_sessions), ) monkeypatch.setattr( - "tools.set_active_instance.get_unity_instance_middleware", + "services.tools.set_active_instance.get_unity_instance_middleware", lambda: middleware, ) @@ -280,11 +280,11 @@ async def test_set_active_instance_http_hash_ambiguous(self, monkeypatch): } } monkeypatch.setattr( - "tools.set_active_instance.PluginHub.get_sessions", + "services.tools.set_active_instance.PluginHub.get_sessions", AsyncMock(return_value=fake_sessions), ) monkeypatch.setattr( - "tools.set_active_instance.get_unity_instance_middleware", + "services.tools.set_active_instance.get_unity_instance_middleware", lambda: middleware, ) diff --git a/Server/tests/integration/test_instance_targeting_resolution.py b/Server/tests/integration/test_instance_targeting_resolution.py index 21384118..d61aedbe 100644 --- a/Server/tests/integration/test_instance_targeting_resolution.py +++ b/Server/tests/integration/test_instance_targeting_resolution.py @@ -7,7 +7,7 @@ async def test_manage_gameobject_uses_session_state(monkeypatch): """Test that tools use session-stored active instance via middleware""" - from unity_instance_middleware import UnityInstanceMiddleware, set_unity_instance_middleware + from transport.unity_instance_middleware import UnityInstanceMiddleware, set_unity_instance_middleware # Arrange: Initialize middleware and set a session-scoped active instance middleware = UnityInstanceMiddleware() @@ -29,9 +29,9 @@ async def fake_send(command_type, params, **kwargs): captured["instance_id"] = kwargs.get("instance_id") return {"success": True, "data": {}} - import tools.manage_gameobject as mg + import services.tools.manage_gameobject as mg monkeypatch.setattr( - "tools.manage_gameobject.async_send_command_with_retry", + "services.tools.manage_gameobject.async_send_command_with_retry", fake_send, ) @@ -53,7 +53,7 @@ async def fake_send(command_type, params, **kwargs): async def test_manage_gameobject_without_active_instance(monkeypatch): """Test that tools work with no active instance set (uses None/default)""" - from unity_instance_middleware import UnityInstanceMiddleware, set_unity_instance_middleware + from transport.unity_instance_middleware import UnityInstanceMiddleware, set_unity_instance_middleware # Arrange: Initialize middleware with no active instance set middleware = UnityInstanceMiddleware() @@ -69,9 +69,9 @@ async def fake_send(command_type, params, **kwargs): captured["instance_id"] = kwargs.get("instance_id") return {"success": True, "data": {}} - import tools.manage_gameobject as mg + import services.tools.manage_gameobject as mg monkeypatch.setattr( - "tools.manage_gameobject.async_send_command_with_retry", + "services.tools.manage_gameobject.async_send_command_with_retry", fake_send, ) diff --git a/Server/tests/integration/test_manage_asset_json_parsing.py b/Server/tests/integration/test_manage_asset_json_parsing.py index 4380828a..1b90837c 100644 --- a/Server/tests/integration/test_manage_asset_json_parsing.py +++ b/Server/tests/integration/test_manage_asset_json_parsing.py @@ -5,7 +5,7 @@ import json from .test_helpers import DummyContext -from tools.manage_asset import manage_asset +from services.tools.manage_asset import manage_asset class TestManageAssetJsonParsing: @@ -20,7 +20,7 @@ async def test_properties_json_string_parsing(self, monkeypatch): # Patch Unity transport async def fake_async(cmd, params, **kwargs): return {"success": True, "message": "Asset created successfully", "data": {"path": "Assets/Test.mat"}} - monkeypatch.setattr("tools.manage_asset.async_send_command_with_retry", fake_async) + monkeypatch.setattr("services.tools.manage_asset.async_send_command_with_retry", fake_async) # Test with JSON string properties result = await manage_asset( @@ -45,7 +45,7 @@ async def test_properties_invalid_json_string(self, monkeypatch): async def fake_async(cmd, params, **kwargs): return {"success": True, "message": "Asset created successfully"} - monkeypatch.setattr("tools.manage_asset.async_send_command_with_retry", fake_async) + monkeypatch.setattr("services.tools.manage_asset.async_send_command_with_retry", fake_async) # Test with invalid JSON string result = await manage_asset( @@ -67,7 +67,7 @@ async def test_properties_dict_unchanged(self, monkeypatch): async def fake_async(cmd, params, **kwargs): return {"success": True, "message": "Asset created successfully"} - monkeypatch.setattr("tools.manage_asset.async_send_command_with_retry", fake_async) + monkeypatch.setattr("services.tools.manage_asset.async_send_command_with_retry", fake_async) # Test with dict properties properties_dict = {"shader": "Universal Render Pipeline/Lit", "color": [0, 0, 1, 1]} @@ -91,7 +91,7 @@ async def test_properties_none_handling(self, monkeypatch): async def fake_async(cmd, params, **kwargs): return {"success": True, "message": "Asset created successfully"} - monkeypatch.setattr("tools.manage_asset.async_send_command_with_retry", fake_async) + monkeypatch.setattr("services.tools.manage_asset.async_send_command_with_retry", fake_async) # Test with None properties result = await manage_asset( @@ -113,14 +113,14 @@ class TestManageGameObjectJsonParsing: @pytest.mark.asyncio async def test_component_properties_json_string_parsing(self, monkeypatch): """Test that JSON string component_properties are correctly parsed.""" - from tools.manage_gameobject import manage_gameobject + from services.tools.manage_gameobject import manage_gameobject ctx = DummyContext() async def fake_send(cmd, params, **kwargs): return {"success": True, "message": "GameObject created successfully"} monkeypatch.setattr( - "tools.manage_gameobject.async_send_command_with_retry", + "services.tools.manage_gameobject.async_send_command_with_retry", fake_send, ) diff --git a/Server/tests/integration/test_manage_asset_param_coercion.py b/Server/tests/integration/test_manage_asset_param_coercion.py index f3db0314..2be274ea 100644 --- a/Server/tests/integration/test_manage_asset_param_coercion.py +++ b/Server/tests/integration/test_manage_asset_param_coercion.py @@ -1,7 +1,7 @@ import asyncio from .test_helpers import DummyContext -import tools.manage_asset as manage_asset_mod +import services.tools.manage_asset as manage_asset_mod def test_manage_asset_pagination_coercion(monkeypatch): diff --git a/Server/tests/integration/test_manage_gameobject_param_coercion.py b/Server/tests/integration/test_manage_gameobject_param_coercion.py index b822e1f8..27c836a6 100644 --- a/Server/tests/integration/test_manage_gameobject_param_coercion.py +++ b/Server/tests/integration/test_manage_gameobject_param_coercion.py @@ -1,7 +1,7 @@ import pytest from .test_helpers import DummyContext -import tools.manage_gameobject as manage_go_mod +import services.tools.manage_gameobject as manage_go_mod @pytest.mark.asyncio diff --git a/Server/tests/integration/test_manage_script_uri.py b/Server/tests/integration/test_manage_script_uri.py index 5cac340b..306f57ef 100644 --- a/Server/tests/integration/test_manage_script_uri.py +++ b/Server/tests/integration/test_manage_script_uri.py @@ -17,9 +17,9 @@ def _decorator(fn): def _register_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration - import tools.manage_script # trigger decorator registration + import services.tools.manage_script # trigger decorator registration # Get the registered tools from the registry - from registry import get_registered_tools + from services.registry import get_registered_tools registered_tools = get_registered_tools() # Add all script-related tools to our dummy MCP for tool_info in registered_tools: @@ -42,7 +42,7 @@ async def fake_send(cmd, params, **kwargs): # capture params and return success # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) @@ -82,7 +82,7 @@ async def fake_send(_cmd, params, **kwargs): # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) @@ -107,7 +107,7 @@ async def fake_send(_cmd, params, **kwargs): # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) diff --git a/Server/tests/integration/test_read_console_truncate.py b/Server/tests/integration/test_read_console_truncate.py index bd4bb82d..17e97386 100644 --- a/Server/tests/integration/test_read_console_truncate.py +++ b/Server/tests/integration/test_read_console_truncate.py @@ -17,9 +17,9 @@ def deco(fn): def setup_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration - import tools.read_console + import services.tools.read_console # Get the registered tools from the registry - from registry import get_registered_tools + from services.registry import get_registered_tools registered_tools = get_registered_tools() # Add all console-related tools to our dummy MCP for tool_info in registered_tools: @@ -44,9 +44,9 @@ async def fake_send(cmd, params, **kwargs): } # Patch the send_command_with_retry function in the tools module - import tools.read_console + import services.tools.read_console monkeypatch.setattr( - tools.read_console, + services.tools.read_console, "async_send_command_with_retry", fake_send, ) @@ -75,9 +75,9 @@ async def fake_send(cmd, params, **kwargs): } # Patch the send_command_with_retry function in the tools module - import tools.read_console + import services.tools.read_console monkeypatch.setattr( - tools.read_console, + services.tools.read_console, "async_send_command_with_retry", fake_send, ) diff --git a/Server/tests/integration/test_read_resource_minimal.py b/Server/tests/integration/test_read_resource_minimal.py index cd3fa24a..57a43b21 100644 --- a/Server/tests/integration/test_read_resource_minimal.py +++ b/Server/tests/integration/test_read_resource_minimal.py @@ -19,9 +19,9 @@ def deco(fn): def resource_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration - import tools.resource_tools + import services.tools.resource_tools # Get the registered tools from the registry - from registry import get_registered_tools + from services.registry import get_registered_tools tools = get_registered_tools() # Add all resource-related tools to our dummy MCP for tool_info in tools: diff --git a/Server/tests/integration/test_resources_api.py b/Server/tests/integration/test_resources_api.py index d8bca76b..f9f18888 100644 --- a/Server/tests/integration/test_resources_api.py +++ b/Server/tests/integration/test_resources_api.py @@ -18,9 +18,9 @@ def deco(fn): def resource_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration - import tools.resource_tools + import services.tools.resource_tools # Get the registered tools from the registry - from registry import get_registered_tools + from services.registry import get_registered_tools tools = get_registered_tools() # Add all resource-related tools to our dummy MCP for tool_info in tools: diff --git a/Server/tests/integration/test_script_tools.py b/Server/tests/integration/test_script_tools.py index 5d7942dc..1f6a7308 100644 --- a/Server/tests/integration/test_script_tools.py +++ b/Server/tests/integration/test_script_tools.py @@ -18,9 +18,9 @@ def decorator(func): def setup_manage_script(): mcp = DummyMCP() # Import the tools module to trigger decorator registration - import tools.manage_script + import services.tools.manage_script # Get the registered tools from the registry - from registry import get_registered_tools + from services.registry import get_registered_tools tools = get_registered_tools() # Add all script-related tools to our dummy MCP for tool_info in tools: @@ -33,9 +33,9 @@ def setup_manage_script(): def setup_manage_asset(): mcp = DummyMCP() # Import the tools module to trigger decorator registration - import tools.manage_asset + import services.tools.manage_asset # Get the registered tools from the registry - from registry import get_registered_tools + from services.registry import get_registered_tools tools = get_registered_tools() # Add all asset-related tools to our dummy MCP for tool_info in tools: @@ -59,7 +59,7 @@ async def fake_send(cmd, params, **kwargs): # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) @@ -88,7 +88,7 @@ async def fake_send(cmd, params, **kwargs): # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) @@ -123,7 +123,7 @@ async def fake_send(cmd, params, **kwargs): # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) @@ -152,7 +152,7 @@ async def fake_send(cmd, params, **kwargs): # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection monkeypatch.setattr( - unity_connection, + transport.legacy.unity_connection, "async_send_command_with_retry", fake_send, ) @@ -185,7 +185,7 @@ async def fake_async(cmd, params, loop=None): return {"success": True} # Patch the async function in the tools module - import tools.manage_asset as tools_manage_asset + import services.tools.manage_asset as tools_manage_asset # Patch both at the module and at the function closure location monkeypatch.setattr(tools_manage_asset, "async_send_command_with_retry", fake_async) diff --git a/Server/tests/integration/test_telemetry_endpoint_validation.py b/Server/tests/integration/test_telemetry_endpoint_validation.py index cbcc98a0..283bef8f 100644 --- a/Server/tests/integration/test_telemetry_endpoint_validation.py +++ b/Server/tests/integration/test_telemetry_endpoint_validation.py @@ -1,5 +1,6 @@ import os import importlib +import pytest def test_endpoint_rejects_non_http(tmp_path, monkeypatch): @@ -8,7 +9,7 @@ def test_endpoint_rejects_non_http(tmp_path, monkeypatch): monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", "file:///etc/passwd") # Import the telemetry module - telemetry = importlib.import_module("telemetry") + telemetry = importlib.import_module("core.telemetry") importlib.reload(telemetry) tc = telemetry.TelemetryCollector() @@ -22,11 +23,11 @@ def test_config_preferred_then_env_override(tmp_path, monkeypatch): monkeypatch.delenv("UNITY_MCP_TELEMETRY_ENDPOINT", raising=False) # Patch config.telemetry_endpoint via import mocking - cfg_mod = importlib.import_module("config") + cfg_mod = importlib.import_module("src.core.config") old_endpoint = cfg_mod.config.telemetry_endpoint cfg_mod.config.telemetry_endpoint = "https://example.com/telemetry" try: - telemetry = importlib.import_module("telemetry") + telemetry = importlib.import_module("core.telemetry") importlib.reload(telemetry) tc = telemetry.TelemetryCollector() # When no env override is set, config endpoint is preferred @@ -46,7 +47,7 @@ def test_uuid_preserved_on_malformed_milestones(tmp_path, monkeypatch): monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) # Import the telemetry module - telemetry = importlib.import_module("telemetry") + telemetry = importlib.import_module("core.telemetry") importlib.reload(telemetry) tc1 = telemetry.TelemetryCollector() diff --git a/Server/tests/integration/test_telemetry_queue_worker.py b/Server/tests/integration/test_telemetry_queue_worker.py index 70b558bf..dbdbe12c 100644 --- a/Server/tests/integration/test_telemetry_queue_worker.py +++ b/Server/tests/integration/test_telemetry_queue_worker.py @@ -3,7 +3,7 @@ import time import queue as q -import telemetry +import core.telemetry as telemetry def test_telemetry_queue_backpressure_and_single_worker(monkeypatch, caplog): diff --git a/Server/tests/integration/test_telemetry_subaction.py b/Server/tests/integration/test_telemetry_subaction.py index ca081b22..eb253716 100644 --- a/Server/tests/integration/test_telemetry_subaction.py +++ b/Server/tests/integration/test_telemetry_subaction.py @@ -8,9 +8,9 @@ def _get_decorator_module(): import types # Tests can now import directly from parent package # Remove any previously stubbed module to force real import - sys.modules.pop("telemetry_decorator", None) + sys.modules.pop("core.telemetry_decorator", None) # Preload a minimal telemetry stub to satisfy telemetry_decorator imports - tel = types.ModuleType("telemetry") + tel = types.ModuleType("core.telemetry") class _MilestoneType: FIRST_TOOL_USAGE = "first_tool_usage" FIRST_SCRIPT_CREATION = "first_script_creation" @@ -22,10 +22,10 @@ def _noop(*a, **k): tel.record_tool_usage = _noop tel.record_milestone = _noop tel.get_package_version = lambda: "0.0.0" - sys.modules.setdefault("telemetry", tel) - mod = importlib.import_module("telemetry_decorator") + sys.modules.setdefault("core.telemetry", tel) + mod = importlib.import_module("core.telemetry_decorator") # Drop stub to avoid bleed-through into other tests - sys.modules.pop("telemetry", None) + sys.modules.pop("core.telemetry", None) # Ensure attributes exist for monkeypatch targets even if not exported if not hasattr(mod, "record_tool_usage"): def _noop_record_tool_usage(*a, **k): diff --git a/Server/tests/integration/test_validate_script_summary.py b/Server/tests/integration/test_validate_script_summary.py index 25d6a8dc..d1b161ae 100644 --- a/Server/tests/integration/test_validate_script_summary.py +++ b/Server/tests/integration/test_validate_script_summary.py @@ -17,9 +17,9 @@ def deco(fn): def setup_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration - import tools.manage_script + import services.tools.manage_script # Get the registered tools from the registry - from registry import get_registered_tools + from services.registry import get_registered_tools registered_tools = get_registered_tools() # Add all script-related tools to our dummy MCP for tool_info in registered_tools: @@ -48,7 +48,7 @@ async def fake_send(cmd, params, **kwargs): # Patch the send_command_with_retry function at the module level where it's imported import transport.legacy.unity_connection - monkeypatch.setattr(unity_connection, + monkeypatch.setattr(transport.legacy.unity_connection, "async_send_command_with_retry", fake_send) # No need to patch tools.manage_script; it now calls unity_connection.send_command_with_retry From 8b658e4d94f1fc9107e02581e90ceb0d75d14160 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Fri, 21 Nov 2025 02:41:43 -0400 Subject: [PATCH 5/5] Update refs in docs --- docs/TELEMETRY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/TELEMETRY.md b/docs/TELEMETRY.md index 2539d340..44d64c3a 100644 --- a/docs/TELEMETRY.md +++ b/docs/TELEMETRY.md @@ -113,7 +113,7 @@ python test_telemetry.py ### Custom Telemetry Events ```python -from telemetry import record_telemetry, RecordType +core.telemetry import record_telemetry, RecordType record_telemetry(RecordType.USAGE, { "custom_event": "my_feature_used", @@ -123,7 +123,7 @@ record_telemetry(RecordType.USAGE, { ### Telemetry Status Check ```python -from telemetry import is_telemetry_enabled +core.telemetry import is_telemetry_enabled if is_telemetry_enabled(): print("Telemetry is active")