Skip to content

Conversation

@dsarno
Copy link
Owner

@dsarno dsarno commented Jan 3, 2026

Summary by CodeRabbit

  • New Features

    • Refresh Unity tool (optional wait-for-ready); Editor State v2 with server-side readiness/staleness and external-changes detection; async test jobs (start/poll) and in-editor test-job query; no-throttle test-run mode.
  • Bug Fixes

    • Stronger preflight gating, retry/recovery and error handling for tools/tests; asset refreshes made synchronous to reduce race conditions.
  • Tests

    • New integration tests covering editor-state v2, external-changes scanning, refresh recovery, and async test jobs.
  • Chores

    • Version bumped to 8.6.0; docs and README updated.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 3, 2026

Warning

Rate limit exceeded

@dsarno has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 46 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between a946be9 and e5774bb.

📒 Files selected for processing (1)
  • MCPForUnity/Editor/Services/TestJobManager.cs

Walkthrough

Adds a v2 editor-state pipeline (Unity cache + server enrichment), external-changes scanning, preflight gating and refresh tooling with readiness/polling, async test-job orchestration (start/poll), TestJobManager/TestRun tooling, many synchronous AssetDatabase refreshes, and version/docs bump to 8.6.0.

Changes

Cohort / File(s) Summary
Unity meta files
MCPForUnity/Editor/Dependencies/Models.meta, MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs.meta, MCPForUnity/Editor/Services/EditorStateCache.cs.meta, MCPForUnity/Editor/Services/TestRunStatus.cs.meta, MCPForUnity/Editor/Tools/RefreshUnity.cs.meta, MCPForUnity/Editor/Services/TestJobManager.cs.meta, MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs.meta, MCPForUnity/Editor/Tools/RunTestsAsync.cs.meta, MCPForUnity/Editor/Tools/GetTestJob.cs.meta
Added/updated Unity .meta importer files (GUIDs and MonoImporter blocks) for new/changed C# sources.
Editor snapshot & cache (Unity)
MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs, MCPForUnity/Editor/Services/EditorStateCache.cs
New EditorStateV2 resource and EditorStateCache providing versioned, throttled, defensive editor-state snapshots wired to editor lifecycle events.
Server v2 resource & enrichment
Server/src/services/resources/editor_state_v2.py
New server resource that retrieves v2 or legacy state, infers instance-id, computes readiness advice and staleness, and merges external-changes scanner results.
External-changes scanner (server)
Server/src/services/state/external_changes_scanner.py
New per-instance scanner tracking project roots, mtimes, manifest file:local deps, dirty/last_cleared timestamps, and exposing update/get/set APIs plus a singleton.
Preflight guard + tool integrations (server)
Server/src/services/tools/preflight.py, Server/src/services/tools/manage_asset.py, Server/src/services/tools/manage_gameobject.py, Server/src/services/tools/manage_scene.py, Server/src/services/tools/manage_gameobject.py
New preflight(ctx, ...) used by multiple tools to gate execution when compiling, domain-reloading, tests running, or external changes pending; may trigger refresh_unity and return busy MCPResponse.
Refresh Unity (server + Unity)
Server/src/services/tools/refresh_unity.py, MCPForUnity/Editor/Tools/RefreshUnity.cs
New server refresh_unity with retry/readiness polling and external-dirty clearing; Unity-side RefreshUnity tool accepts mode/scope/compile/wait_for_ready.
Test-run orchestration (Unity + server)
MCPForUnity/Editor/Services/TestRunStatus.cs, MCPForUnity/Editor/Services/TestRunnerService.cs, MCPForUnity/Editor/Services/TestJobManager.cs, MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs, MCPForUnity/Editor/Tools/RunTestsAsync.cs, Server/src/services/tools/test_jobs.py, Server/src/services/tools/run_tests.py
Adds TestRunStatus, TestJobManager (job lifecycle + persistence), TestRunnerNoThrottle, RunTestsAsync/GetTestJob tools, server-side run_tests_async/get_test_job, and run_tests preflight/busy semantics.
Manage tools: synchronous refresh (Unity)
MCPForUnity/Editor/Tools/* (ManageAsset/ManageGameObject/ManageScene/ManageScript/ManageShader/Prefabs/...) and test projects
Replaced AssetDatabase.Refresh() with AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport) across many tool paths and tests to force synchronous imports.
Async test jobs (server)
Server/src/services/tools/test_jobs.py
New run_tests_async and get_test_job server helpers that forward params to Unity and return job-oriented responses.
Version bump & docs
MCPForUnity/package.json, Server/pyproject.toml, Server/README.md, README.md, docs/README-DEV.md, .github/workflows/unity-tests.yml
Bumped versions to 8.6.0; updated README/docs, workflow and domain-reload docs; adjusted transport/Start Server wording.
Integration tests
Server/tests/integration/* (editor_state_v2_contract, external_changes_scanner, refresh_unity_*, run_tests_busy_semantics, test_jobs_async, refresh registration)
Added tests validating editor_state_v2 contract, external-changes scanning, refresh_unity registration & retry recovery, run_tests busy semantics, and async test-job forwarding.
Unity editor tools: GetTestJob / RunTestsAsync
MCPForUnity/Editor/Tools/GetTestJob.cs, MCPForUnity/Editor/Tools/RunTestsAsync.cs
New editor tools: get_test_job (poll job) and run_tests_async (start async job), with parameter parsing and TestJobManager integration.
Unity test project updates
TestProjects/UnityMCPTests/Assets/Tests/...
Tests updated for per-run isolation, synchronous refresh enforcement, and doc/attribute tweaks.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Server
    participant Preflight as Preflight Guard
    participant State as get_editor_state_v2
    participant Unity as Unity Transport
    participant Tool as Tool (e.g., manage_scene)

    Client->>Server: call tool
    Server->>Preflight: preflight(wait_for_no_compile=True, refresh_if_dirty=True)
    Preflight->>State: get_editor_state_v2
    State->>Unity: send v2 or legacy editor_state command
    Unity-->>State: editor state
    State-->>Preflight: enriched state (advice/staleness)
    alt assets dirty & refresh_if_dirty
        Preflight->>Server: refresh_unity (best-effort)
        Server->>Unity: refresh_unity (with retry)
        Unity-->>Server: response (may hint "retry")
        Server->>State: poll get_editor_state_v2 until advice.ready_for_tools
    end
    alt blocked (compiling/tests)
        Preflight-->>Server: MCPResponse(busy, retry_after_ms)
        Server-->>Client: busy response
    else ready
        Server->>Tool: proceed to execute
        Tool->>Unity: send tool command
        Unity-->>Server: result
        Server-->>Client: result
    end
Loading
sequenceDiagram
    actor User
    participant Server
    participant Refresh as refresh_unity
    participant Unity as Unity Transport
    participant State as get_editor_state_v2
    participant Scanner as ExternalChangesScanner

    User->>Server: refresh_unity(wait_for_ready=True,...)
    Server->>Unity: send_command_with_retry(refresh_unity params)
    alt response with retry hint
        Unity-->>Server: {success:false, hint:"retry"}
        Server->>State: poll get_editor_state_v2 until advice.ready_for_tools
        State->>Unity: fetch state
    else success
        Unity-->>Server: success
    end
    alt ready detected
        Server->>Scanner: clear_dirty(instance_id)
        Scanner-->>Server: OK
        Server-->>User: SuccessResponse (recovered_from_disconnect if applicable)
    else timeout/error
        Server-->>User: Error/timeout MCPResponse
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • msanatan

Poem

🐰 I hop through snapshots, quick and spry,

Caches hum as assets fly,
Preflight guards the busy gate,
Refresh and tests now coordinate,
V2 blooms — a carrot-sky high!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.93% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: external package change detection (ExternalChangesScanner, external_changes_scanner.py) and refresh retry hardening (RefreshUnity, preflight, refresh_unity.py with retry logic).

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (13)
MCPForUnity/Editor/Services/TestRunStatus.cs (1)

19-37: Consider consistent brace style in lock statements.

The property getters use lock (LockObj) return _field; without braces, while MarkStarted and MarkFinished use braces within their lock blocks. While both are valid C#, consistent use of braces improves readability.

🔎 Suggested refactor for consistency
 public static bool IsRunning
 {
-    get { lock (LockObj) return _isRunning; }
+    get { lock (LockObj) { return _isRunning; } }
 }

 public static TestMode? Mode
 {
-    get { lock (LockObj) return _mode; }
+    get { lock (LockObj) { return _mode; } }
 }

 public static long? StartedUnixMs
 {
-    get { lock (LockObj) return _startedUnixMs; }
+    get { lock (LockObj) { return _startedUnixMs; } }
 }

 public static long? FinishedUnixMs
 {
-    get { lock (LockObj) return _finishedUnixMs; }
+    get { lock (LockObj) { return _finishedUnixMs; } }
 }
MCPForUnity/Editor/Tools/RefreshUnity.cs (2)

36-39: Empty catch block silently swallows exceptions.

Catching all exceptions without at least logging makes debugging difficult. Consider logging a warning or being more specific about what parse failures are expected.

🔎 Proposed improvement
             catch
             {
-                // ignore parse failures
+                // Ignore parse failures for wait_for_ready; default to false is acceptable.
+                // Uncomment for debugging: McpLog.Debug("[RefreshUnity] Failed to parse wait_for_ready");
             }

61-65: Consider adding a comment clarifying the intentional no-op.

The empty block for scope == "scripts" is intentional but may confuse future maintainers. Adding a brief explanation of why the refresh is skipped would improve clarity.

🔎 Proposed improvement
                     if (string.Equals(scope, "scripts", StringComparison.OrdinalIgnoreCase))
                     {
                         // For scripts, requesting compilation is usually the meaningful action.
-                        // We avoid a heavyweight full refresh by default.
+                        // We avoid a heavyweight full refresh by default; the caller should set
+                        // compile="request" to trigger script compilation instead.
                     }
Server/tests/integration/test_refresh_unity_registration.py (1)

9-9: Consider removing the unused noqa directive.

Per static analysis, the F401 rule (unused imports) is not enabled, making the noqa comment unnecessary. The import is intentionally side-effectual for registration, which the preceding comment already explains.

🔎 Proposed fix
     # Import the specific module to ensure it registers its decorator without disturbing global registry state.
-    import services.tools.refresh_unity  # noqa: F401
+    import services.tools.refresh_unity
Server/tests/integration/test_editor_state_v2_contract.py (1)

15-16: Consider removing the unnecessary noqa directive.

The import is for side effects (triggering resource registration via decorator). Ruff indicates F401 is not enabled in your configuration, so the noqa directive is unused.

🔎 Proposed fix
     # Import the v2 module to ensure it registers its decorator without disturbing global registry state.
-    import services.resources.editor_state_v2  # noqa: F401
+    import services.resources.editor_state_v2
Server/src/services/tools/preflight.py (1)

46-52: Consider adding debug-level logging for swallowed exceptions.

The broad exception handling is intentional for resilience—preflight should not block tools on unexpected errors. However, logging at debug level would aid troubleshooting without affecting normal operation.

🔎 Proposed improvement
+    import logging
+    logger = logging.getLogger(__name__)
+
     try:
         from services.resources.editor_state_v2 import get_editor_state_v2
         state_resp = await get_editor_state_v2(ctx)
         state = state_resp.model_dump() if hasattr(state_resp, "model_dump") else state_resp
     except Exception:
         # If we cannot determine readiness, fall back to proceeding (tools already contain retry logic).
+        logger.debug("Preflight: failed to get editor state, proceeding", exc_info=True)
         return None
Server/src/services/tools/refresh_unity.py (1)

55-69: Consider logging when readiness polling times out.

The wait loop exits silently after 60s if readiness is never achieved. While proceeding is reasonable (best-effort wait), a warning log would help diagnose situations where Unity remains stuck.

🔎 Proposed improvement
+    import logging
+    logger = logging.getLogger(__name__)
+
     if wait_for_ready:
         timeout_s = 60.0
         start = time.monotonic()
         from services.resources.editor_state_v2 import get_editor_state_v2

         while time.monotonic() - start < timeout_s:
             state_resp = await get_editor_state_v2(ctx)
             state = state_resp.model_dump() if hasattr(state_resp, "model_dump") else state_resp
             data = (state or {}).get("data") if isinstance(state, dict) else None
             advice = (data or {}).get("advice") if isinstance(data, dict) else None
             if isinstance(advice, dict) and advice.get("ready_for_tools") is True:
                 break
             await asyncio.sleep(0.25)
+        else:
+            logger.warning("refresh_unity: readiness polling timed out after %.1fs", timeout_s)
Server/src/services/resources/editor_state_v2.py (4)

15-16: Minor duplication with external_changes_scanner._now_unix_ms.

This helper duplicates the same function in external_changes_scanner.py. Consider extracting to a shared utility module if more timestamp helpers are needed, but acceptable as-is for module isolation.


24-31: Unused ctx parameter.

The ctx parameter is declared but never used in this function. Either remove it if not needed, or document why it's reserved for future use.

🔎 Proposed fix

If ctx is not needed:

-async def _infer_single_instance_id(ctx: Context) -> str | None:
+async def _infer_single_instance_id() -> str | None:

Then update the call site at line 231:

-            inferred = await _infer_single_instance_id(ctx)
+            inferred = await _infer_single_instance_id()

32-66: Consider debug logging for inference failures.

The broad exception handling is appropriate for best-effort inference, but silent failures can make transport issues hard to diagnose. Consider adding debug-level logging before returning None.

🔎 Example
import logging
logger = logging.getLogger(__name__)

# Then in each except block:
except Exception as e:
    logger.debug("Failed to infer instance ID via HTTP: %s", e)
    return None

237-265: Add logging to the external changes integration block.

The try-except-pass pattern silently swallows all errors from external changes scanning. While best-effort is appropriate here, logging at debug level would aid troubleshooting when filesystem scanning unexpectedly fails.

🔎 Proposed fix
+import logging
+logger = logging.getLogger(__name__)

     except Exception:
-        # Best-effort; do not fail readiness resource if filesystem scan can't run.
-        pass
+        # Best-effort; do not fail readiness resource if filesystem scan can't run.
+        logger.debug("External changes scan failed", exc_info=True)
Server/src/services/state/external_changes_scanner.py (2)

123-129: Consider narrowing the exception type or adding logging.

The bare except Exception at line 126 will catch both JSON parsing errors and unexpected I/O errors. While defensive, this could mask issues. Consider catching json.JSONDecodeError specifically or adding debug logging.

🔎 Proposed fix
         try:
             raw = manifest_path.read_text(encoding="utf-8")
             doc = json.loads(raw)
-        except Exception:
+        except (OSError, json.JSONDecodeError):
             st.extra_roots = []
             st.manifest_last_mtime_ns = mtime_ns
             return []

213-216: Add logging for manifest resolution failures.

The try-except-pass silently swallows errors from _resolve_manifest_extra_roots. While non-fatal, logging would help diagnose issues with local package detection.

🔎 Proposed fix
+import logging
+logger = logging.getLogger(__name__)

         try:
             paths.extend(self._resolve_manifest_extra_roots(root, st))
-        except Exception:
-            pass
+        except Exception:
+            logger.debug("Failed to resolve manifest extra roots for %s", project_root, exc_info=True)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b153b6 and 3d58862.

⛔ Files ignored due to path filters (1)
  • Server/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • MCPForUnity/Editor/Dependencies/Models.meta
  • MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs
  • MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs.meta
  • MCPForUnity/Editor/Services/EditorStateCache.cs
  • MCPForUnity/Editor/Services/EditorStateCache.cs.meta
  • MCPForUnity/Editor/Services/TestRunStatus.cs
  • MCPForUnity/Editor/Services/TestRunStatus.cs.meta
  • MCPForUnity/Editor/Services/TestRunnerService.cs
  • MCPForUnity/Editor/Tools/RefreshUnity.cs
  • MCPForUnity/Editor/Tools/RefreshUnity.cs.meta
  • MCPForUnity/package.json
  • Server/README.md
  • Server/pyproject.toml
  • Server/src/services/resources/editor_state_v2.py
  • Server/src/services/state/external_changes_scanner.py
  • Server/src/services/tools/manage_asset.py
  • Server/src/services/tools/manage_gameobject.py
  • Server/src/services/tools/manage_scene.py
  • Server/src/services/tools/preflight.py
  • Server/src/services/tools/refresh_unity.py
  • Server/src/services/tools/run_tests.py
  • Server/tests/integration/test_editor_state_v2_contract.py
  • Server/tests/integration/test_external_changes_scanner.py
  • Server/tests/integration/test_refresh_unity_registration.py
  • Server/tests/integration/test_refresh_unity_retry_recovery.py
  • Server/tests/integration/test_run_tests_busy_semantics.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-21T05:19:59.258Z
Learnt from: dsarno
Repo: dsarno/unity-mcp PR: 85
File: tests/test_telemetry_queue_worker.py:33-37
Timestamp: 2025-10-21T05:19:59.258Z
Learning: In the unity-mcp repository, pyproject.toml is located in MCPForUnity/UnityMcpServer~/src/, not at the repository root. When testing code that imports telemetry.py, the working directory should be changed to SRC (MCPForUnity/UnityMcpServer~/src/) so that telemetry.py's get_package_version() can find pyproject.toml via relative path.

Applied to files:

  • Server/pyproject.toml
🧬 Code graph analysis (10)
MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs (1)
MCPForUnity/Editor/Services/EditorStateCache.cs (3)
  • JObject (90-213)
  • JObject (215-226)
  • EditorStateCache (34-62)
Server/src/services/tools/manage_asset.py (1)
Server/src/services/tools/preflight.py (1)
  • preflight (27-105)
MCPForUnity/Editor/Services/TestRunnerService.cs (1)
MCPForUnity/Editor/Services/TestRunStatus.cs (3)
  • TestRunStatus (10-59)
  • MarkStarted (39-48)
  • MarkFinished (50-58)
Server/tests/integration/test_refresh_unity_retry_recovery.py (3)
Server/src/services/state/external_changes_scanner.py (1)
  • ExternalChangesState (21-31)
Server/tests/integration/test_helpers.py (3)
  • DummyContext (16-55)
  • set_state (49-51)
  • model_dump (10-13)
Server/src/services/tools/refresh_unity.py (1)
  • refresh_unity (20-88)
Server/tests/integration/test_editor_state_v2_contract.py (2)
Server/tests/integration/test_helpers.py (2)
  • DummyContext (16-55)
  • model_dump (10-13)
Server/tests/integration/test_run_tests_busy_semantics.py (1)
  • fake_send_with_unity_instance (14-20)
Server/src/services/resources/editor_state_v2.py (5)
Server/src/transport/legacy/unity_connection.py (2)
  • async_send_command_with_retry (753-785)
  • get_unity_connection_pool (652-665)
Server/src/services/state/external_changes_scanner.py (4)
  • _now_unix_ms (11-12)
  • _in_pytest (15-17)
  • set_project_root (51-54)
  • update_and_get (172-240)
Server/src/services/tools/preflight.py (1)
  • _in_pytest (11-14)
Server/src/transport/unity_transport.py (2)
  • _current_transport (23-25)
  • send_with_unity_instance (79-113)
Server/tests/integration/test_helpers.py (1)
  • model_dump (10-13)
Server/src/services/tools/manage_gameobject.py (1)
Server/src/services/tools/preflight.py (1)
  • preflight (27-105)
Server/tests/integration/test_refresh_unity_registration.py (1)
Server/src/services/tools/refresh_unity.py (1)
  • refresh_unity (20-88)
MCPForUnity/Editor/Tools/RefreshUnity.cs (2)
MCPForUnity/Editor/Services/EditorStateCache.cs (2)
  • JObject (90-213)
  • JObject (215-226)
MCPForUnity/Editor/Services/TestRunStatus.cs (1)
  • TestRunStatus (10-59)
Server/src/services/tools/manage_scene.py (1)
Server/src/services/tools/preflight.py (1)
  • preflight (27-105)
🪛 Ruff (0.14.10)
Server/src/services/tools/preflight.py

50-50: Do not catch blind exception: Exception

(BLE001)


69-71: try-except-pass detected, consider logging the exception

(S110)


69-69: Do not catch blind exception: Exception

(BLE001)


100-100: Do not catch blind exception: Exception

(BLE001)

Server/src/services/tools/refresh_unity.py

78-79: try-except-pass detected, consider logging the exception

(S110)


78-78: Do not catch blind exception: Exception

(BLE001)

Server/tests/integration/test_run_tests_busy_semantics.py

14-14: Unused function argument: send_fn

(ARG001)


14-14: Unused function argument: unity_instance

(ARG001)


14-14: Unused function argument: params

(ARG001)


14-14: Unused function argument: kwargs

(ARG001)

Server/src/services/state/external_changes_scanner.py

126-126: Do not catch blind exception: Exception

(BLE001)


215-216: try-except-pass detected, consider logging the exception

(S110)


215-215: Do not catch blind exception: Exception

(BLE001)

Server/tests/integration/test_refresh_unity_retry_recovery.py

25-25: Unused function argument: send_fn

(ARG001)


25-25: Unused function argument: unity_instance

(ARG001)


25-25: Unused function argument: params

(ARG001)


25-25: Unused function argument: kwargs

(ARG001)

Server/tests/integration/test_editor_state_v2_contract.py

16-16: Unused noqa directive (non-enabled: F401)

Remove unused noqa directive

(RUF100)


26-26: Unused function argument: send_fn

(ARG001)


26-26: Unused function argument: unity_instance

(ARG001)


26-26: Unused function argument: params

(ARG001)


26-26: Unused function argument: kwargs

(ARG001)

Server/src/services/resources/editor_state_v2.py

24-24: Unused function argument: ctx

(ARG001)


34-34: Do not catch blind exception: Exception

(BLE001)


50-50: Do not catch blind exception: Exception

(BLE001)


64-64: Do not catch blind exception: Exception

(BLE001)


146-146: Do not catch blind exception: Exception

(BLE001)


263-265: try-except-pass detected, consider logging the exception

(S110)


263-263: Do not catch blind exception: Exception

(BLE001)

Server/tests/integration/test_refresh_unity_registration.py

9-9: Unused noqa directive (non-enabled: F401)

Remove unused noqa directive

(RUF100)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🔇 Additional comments (40)
Server/README.md (1)

72-72: LGTM! Documentation version reference updated correctly.

The version bump to v8.6.0 aligns with the package version changes across the project.

MCPForUnity/package.json (1)

3-3: LGTM! Package version bumped consistently.

The version bump to 8.6.0 is synchronized with the server-side version in pyproject.toml and the documentation reference in README.md.

MCPForUnity/Editor/Services/EditorStateCache.cs.meta (1)

1-11: Standard Unity meta file.

This is standard Unity-generated metadata for the EditorStateCache.cs file. All fields contain expected default values.

Server/src/services/tools/manage_asset.py (2)

15-15: Good addition: preflight import for readiness checks.

Importing the preflight guard enables stability checks before asset operations.


51-53: Excellent safeguard: preflight check before asset operations.

The preflight guard with wait_for_no_compile=True and refresh_if_dirty=True is a smart addition that:

  • Prevents operations during compilation/domain reload
  • Ensures external asset changes are imported first
  • Reduces flaky timeouts and stale reads

The best-effort nature (falls through on errors) maintains backward compatibility while adding resilience. The default 30-second timeout is reasonable for asset operations.

Server/pyproject.toml (1)

3-3: LGTM! Server version synchronized with Unity package.

The version bump to 8.6.0 maintains consistency with MCPForUnity/package.json and properly reflects the new v2 editor state architecture features introduced in this PR.

MCPForUnity/Editor/Services/TestRunStatus.cs (1)

39-58: LGTM! Thread-safe state management is correct.

The locking strategy properly guards all state mutations and reads. The state transitions in MarkStarted and MarkFinished correctly manage the test run lifecycle.

MCPForUnity/Editor/Services/EditorStateCache.cs (3)

64-88: LGTM! Throttling and thread-safe updates are well implemented.

The throttling logic correctly reduces overhead while still capturing compilation edge transitions. The ForceUpdate method properly guards the cached snapshot with a lock to ensure thread safety across editor callbacks.


113-133: Activity phase prioritization is logical and comprehensive.

The phase detection correctly prioritizes: tests → compiling → domain reload → asset import → playmode transition → idle. This provides clear semantics for downstream preflight and readiness checks.


215-226: Defensive cloning prevents external mutation of cached state.

Returning a deep clone ensures callers cannot accidentally mutate the cached snapshot, maintaining thread safety guarantees.

MCPForUnity/Editor/Tools/RefreshUnity.cs (2)

128-170: LGTM - Async polling pattern is well-structured.

The WaitForUnityReadyAsync implementation properly:

  • Uses TaskCreationOptions.RunContinuationsAsynchronously to avoid deadlocks
  • Wraps the tick handler in try-catch to ensure delegate cleanup on exceptions
  • Checks all relevant readiness conditions matching EditorStateCache logic
  • Nudges the editor loop to handle throttled updates

92-126: LGTM - Response handling is comprehensive.

The wait logic handles timeout and general exceptions appropriately, and the success response provides actionable hints for callers based on whether wait_for_ready was used.

MCPForUnity/Editor/Services/TestRunnerService.cs (3)

96-97: Good placement of status tracking before Execute.

Marking the run as started before _testRunnerApi.Execute(settings) ensures the readiness snapshot reflects the busy state even if the execution throws synchronously. This aligns well with the early-return guard in RefreshUnity.HandleCommand.


118-129: LGTM - Proper cleanup on startup failure.

The catch block correctly clears the test run status before releasing the lock and rethrowing, ensuring the readiness snapshot doesn't get stuck in a "running" state when the test run fails to start.


172-183: LGTM - Status correctly cleared on run completion.

The RunFinished callback properly marks the test run as finished after completing the task, ensuring the status is cleared even if the run result processing has issues downstream.

MCPForUnity/Editor/Tools/RefreshUnity.cs.meta (1)

1-11: LGTM - Standard Unity meta file.

This is a correctly structured Unity asset metadata file for the new RefreshUnity.cs source file.

MCPForUnity/Editor/Services/TestRunStatus.cs.meta (1)

1-11: LGTM - Standard Unity meta file.

This is a correctly structured Unity asset metadata file for the new TestRunStatus.cs source file.

Server/tests/integration/test_run_tests_busy_semantics.py (2)

14-20: Unused mock arguments are acceptable here.

The static analysis flags for unused arguments (send_fn, unity_instance, params, kwargs) are false positives. The mock function signature must match the real send_with_unity_instance signature for the monkeypatch to work correctly. The assertion on command_type at line 15 validates the call is made with the expected command.


24-35: LGTM - Comprehensive busy-state contract validation.

The test properly validates:

  • Failure indication (success: False)
  • Retry hint presence (hint: "retry")
  • Reason categorization (tests_running or busy)
  • Minimum backoff (retry_after_ms >= 500)

This ensures callers can reliably detect and handle busy states.

MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs.meta (1)

1-11: LGTM - Standard Unity meta file.

This is a correctly structured Unity asset metadata file for the new EditorStateV2.cs resource file.

Server/tests/integration/test_refresh_unity_registration.py (1)

4-13: LGTM - Simple and effective registration test.

The test correctly triggers module import to register the tool via decorators, then validates presence in the registry. This pattern ensures the tool is properly wired up without testing implementation details.

MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs (1)

1-27: LGTM! Clean resource handler implementation.

The implementation correctly retrieves the cached v2 snapshot and wraps it in appropriate response types. Exception handling is adequate for surfacing errors to clients. The @params parameter is unused but likely required by the MCP resource handler contract.

Server/tests/integration/test_refresh_unity_retry_recovery.py (1)

10-44: Well-structured integration test for disconnect recovery.

The test effectively validates the critical recovery path when Unity disconnects with a retry hint. It correctly:

  • Seeds the external changes scanner with dirty state
  • Mocks transport to simulate a disconnect with hint="retry"
  • Mocks editor state to indicate readiness
  • Verifies both the response payload and the side effect (dirty flag cleared)

The unused arguments in fake_send_with_unity_instance (flagged by static analysis) are intentional—the mock must match the real function's signature.

Server/src/services/tools/run_tests.py (2)

61-63: Good preflight integration.

The preflight guard correctly gates test execution with requires_no_tests=True to prevent overlapping test runs. The early return pattern is clean and avoids unnecessary work before the guard passes.


106-118: Well-structured busy-state response.

The busy-state detection for in-progress test runs is properly implemented:

  • Case-insensitive matching handles variations in error messages
  • The structured response with error="tests_running" and hint="retry" enables clients to handle this gracefully
  • The retry_after_ms: 5000 provides actionable guidance
Server/tests/integration/test_editor_state_v2_contract.py (1)

48-56: Solid contract validation.

The assertions comprehensively verify the v2 contract structure including the required fields (schema_version, observed_at_unix_ms, sequence, advice, staleness). This ensures the resource meets client expectations for readiness polling.

Server/tests/integration/test_external_changes_scanner.py (2)

6-43: Comprehensive test for dirty state lifecycle.

The test properly validates the scanner's core behavior:

  • Establishes a baseline scan (not dirty)
  • Touches a file with a future timestamp to trigger dirty detection
  • Verifies the dirty flag and associated timestamps
  • Confirms clear_dirty resets state appropriately

The monkeypatch.delenv("PYTEST_CURRENT_TEST") is correctly used to enable the scanner during this unit-style test.


46-84: Good coverage for external package root detection.

This test validates an important edge case: detecting changes in external packages referenced via file: dependencies in manifest.json. The test correctly:

  • Creates a package outside the project root
  • Declares it in manifest.json with a relative file: path
  • Verifies that touching the external package triggers dirty state
Server/src/services/tools/preflight.py (2)

11-14: Appropriate test environment detection.

Using PYTEST_CURRENT_TEST to detect the test environment is a reasonable approach. This allows preflight to be a no-op during integration tests that mock transports.


79-101: Compilation wait loop is well-structured.

The bounded wait with periodic state refresh is a solid pattern. The 0.25s polling interval balances responsiveness with avoiding excessive requests. Returning None on state refresh failures allows tools to proceed rather than hang.

One minor note: the import inside the loop (from services.resources.editor_state_v2 import get_editor_state_v2) could be hoisted outside for slight efficiency, though the impact is negligible.

Server/src/services/tools/refresh_unity.py (2)

44-53: Good retry detection heuristics.

The recovery logic correctly identifies retryable scenarios by checking both the explicit hint="retry" and the presence of "disconnected" in the error message. This handles both explicit Unity signaling and transport-level disconnects.


71-79: Silent exception handling is acceptable for cleanup.

The dirty flag clearing is non-critical cleanup, so swallowing exceptions here is reasonable. The instance inference and clearing are best-effort operations that shouldn't fail the overall refresh.

Server/src/services/resources/editor_state_v2.py (3)

19-21: LGTM!

The pytest guard follows the same pattern used in external_changes_scanner.py and correctly prevents instance-discovery side effects during tests.


69-138: LGTM!

The legacy-to-v2 mapping is well-structured with appropriate defaults. The fallback handling for wrapped vs unwrapped responses (line 76) and safe type coercions are correct.


141-179: LGTM!

The advice computation logic is clear and correct. The 2000ms staleness threshold is well-documented. The blocking reasons aggregation properly captures all states that should prevent tool execution.

Server/src/services/state/external_changes_scanner.py (5)

1-17: LGTM!

Clean imports and utility helpers. The _in_pytest guard correctly disables filesystem scanning during integration tests.


20-31: LGTM!

Well-structured dataclass with appropriate defaults. The separation of timing fields (last_scan_unix_ms, dirty_since_unix_ms, etc.) and the manifest cache (extra_roots, manifest_last_mtime_ns) supports efficient incremental scanning.


34-62: LGTM!

The scanner configuration with configurable scan_interval_ms (1500ms) and max_entries (20000) provides sensible bounds on I/O overhead. The clear_dirty method correctly resets the mtime baseline to force re-establishment on the next scan.


64-104: LGTM!

The recursive mtime scan is well-optimized:

  • Prunes heavy Unity directories (Library, Temp, etc.) to avoid I/O on generated content
  • Entry limit prevents runaway scans on large projects
  • Graceful OSError handling for inaccessible files
  • Cross-platform case-insensitive directory matching

226-244: Thread safety consideration for concurrent access.

The scanner mutates state (st.dirty, st.last_seen_mtime_ns, etc.) without synchronization. If update_and_get is called concurrently from multiple asyncio tasks for the same instance_id, there could be race conditions. Consider whether this is a concern in your usage pattern.

If concurrent access is expected, a per-instance lock could be added:

import threading

def __init__(self, ...):
    self._locks: dict[str, threading.Lock] = {}
    
def _get_lock(self, instance_id: str) -> threading.Lock:
    return self._locks.setdefault(instance_id, threading.Lock())

Comment on lines +180 to +191
assets = new
{
is_updating = EditorApplication.isUpdating,
external_changes_dirty = false,
external_changes_last_seen_unix_ms = (long?)null,
refresh = new
{
is_refresh_in_progress = false,
last_refresh_requested_unix_ms = (long?)null,
last_refresh_finished_unix_ms = (long?)null
}
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for code that updates external_changes_dirty or external_changes_last_seen
rg -n -C3 'external_changes_dirty|external_changes_last_seen' --type=cs

Repository: dsarno/unity-mcp

Length of output: 820


🏁 Script executed:

#!/bin/bash
# Search for refresh field updates
rg -n -C3 'is_refresh_in_progress|last_refresh_requested_unix_ms|last_refresh_finished_unix_ms' --type=cs

Repository: dsarno/unity-mcp

Length of output: 916


🏁 Script executed:

#!/bin/bash
# Search for methods that create or modify the state snapshot
rg -n 'CreateSnapshot|GetSnapshot|SetSnapshot' --type=cs -B2 -A5

Repository: dsarno/unity-mcp

Length of output: 1474


🏁 Script executed:

#!/bin/bash
# Search for external changes scanner or related logic
rg -n 'external.*change|scanner' --type=cs -i

Repository: dsarno/unity-mcp

Length of output: 285


These fields are hardcoded placeholders with no update mechanism in the codebase.

Verification confirms that external_changes_dirty, external_changes_last_seen_unix_ms, and all refresh tracking fields are hardcoded at initialization and are never updated anywhere in the repository. There is no external changes scanner implementation and no logic to populate these fields. Either the feature is incomplete or these fields are intentional stubs for future implementation.

🤖 Prompt for AI Agents
In MCPForUnity/Editor/Services/EditorStateCache.cs around lines 180 to 191,
several asset-tracking fields are hardcoded placeholders
(external_changes_dirty, external_changes_last_seen_unix_ms, and the refresh
tracking fields) but there is no scanner or update logic populating them; either
implement the missing update mechanism or remove/mark these fields as TODO. Add
a background or event-driven external-changes detector (or hook into Unity's
file/asset change callbacks) that updates external_changes_dirty and
external_changes_last_seen_unix_ms, and implement refresh state transitions (set
is_refresh_in_progress, last_refresh_requested_unix_ms and
last_refresh_finished_unix_ms) around the refresh workflow; alternatively, if
the feature is not ready, remove these fields or clearly annotate them as
TODO/stubs and ensure no code consumes them to avoid misleading state. Ensure
timestamps use consistent Unix ms conversion and all updates occur on the main
thread or are synchronized appropriately.

Comment on lines +198 to +211
# If Unity returns a structured retry hint or error, surface it directly.
if isinstance(response, dict) and not response.get("success", True):
return MCPResponse(**response)

# If v2 is unavailable (older plugin), fall back to legacy get_editor_state and map.
if not (isinstance(response, dict) and isinstance(response.get("data"), dict) and response["data"].get("schema_version")):
legacy = await unity_transport.send_with_unity_instance(
async_send_command_with_retry,
unity_instance,
"get_editor_state",
{},
)
if isinstance(legacy, dict) and not legacy.get("success", True):
return MCPResponse(**legacy)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find MCPResponse class definition
rg -n "class MCPResponse" --type py -A 15

Repository: dsarno/unity-mcp

Length of output: 1058


🏁 Script executed:

#!/bin/bash
# Search for MCPResponse imports and usage
rg -n "MCPResponse" --type py -l | head -20

Repository: dsarno/unity-mcp

Length of output: 939


🏁 Script executed:

#!/bin/bash
# Check for Pydantic model configuration in the file containing MCPResponse
fd . --type f -name "*.py" | xargs rg -l "MCPResponse" | head -5

Repository: dsarno/unity-mcp

Length of output: 539


🏁 Script executed:

#!/bin/bash
# Get the full MCPResponse definition with proper context
cat -n Server/src/models/models.py | head -50

Repository: dsarno/unity-mcp

Length of output: 1985


🏁 Script executed:

#!/bin/bash
# Check for Pydantic version and imports
rg -n "pydantic|ConfigDict|model_config" Server/src/models/models.py -B 2 -A 2

Repository: dsarno/unity-mcp

Length of output: 162


🏁 Script executed:

#!/bin/bash
# Check project dependencies for Pydantic version
cat pyproject.toml | grep -i pydantic

Repository: dsarno/unity-mcp

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Find pyproject.toml in Server directory
find . -name "pyproject.toml" -type f

Repository: dsarno/unity-mcp

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Find requirements files
find . -name "requirements*.txt" -type f

Repository: dsarno/unity-mcp

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Search for Pydantic version specification anywhere
rg -n "pydantic.*==" --type toml --type txt

Repository: dsarno/unity-mcp

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Read Server/pyproject.toml to find Pydantic version
cat Server/pyproject.toml

Repository: dsarno/unity-mcp

Length of output: 1853


🏁 Script executed:

#!/bin/bash
# Double-check MCPResponse for any ConfigDict or model_config we might have missed
rg -n "MCPResponse" Server/src/models/models.py -A 20 -B 5

Repository: dsarno/unity-mcp

Length of output: 819


🏁 Script executed:

#!/bin/bash
# Check if there are any tests or error handling related to MCPResponse validation
rg -n "MCPResponse\|ValidationError" --type py -l | head -10

Repository: dsarno/unity-mcp

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check how responses are actually constructed from Unity in the code
rg -n "\*\*response" Server/src/services/resources/editor_state_v2.py -B 3 -A 1

Repository: dsarno/unity-mcp

Length of output: 271


🏁 Script executed:

#!/bin/bash
# Search for test fixtures or mock responses to see what fields Unity actually returns
rg -n "success.*message.*error\|MCPResponse\|mock.*response" --type py Server/tests/ -B 2 -A 2 | head -80

Repository: dsarno/unity-mcp

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check integration tests for actual response patterns
cat Server/tests/integration/test_refresh_unity_retry_recovery.py 2>/dev/null | head -100

Repository: dsarno/unity-mcp

Length of output: 1925


🏁 Script executed:

#!/bin/bash
# Look at how responses from Unity are structured elsewhere in the codebase
rg -n "isinstance.*dict.*response\|response\[.*\]" Server/src/services/ --type py -B 1 -A 1 | head -60

Repository: dsarno/unity-mcp

Length of output: 42


Add explicit extra field handling to MCPResponse model.

Lines 200 and 211 unpack response dicts directly: MCPResponse(**response). With Pydantic v2 (specified in pyproject.toml as >=2.12.5), the default behavior is extra="forbid", which will raise a ValidationError if Unity returns any undocumented response fields. Add model_config = ConfigDict(extra="ignore") to the MCPResponse class definition in Server/src/models/models.py to gracefully handle unexpected fields from the Unity plugin.

🤖 Prompt for AI Agents
In Server/src/services/resources/editor_state_v2.py around lines 198 to 211, the
code calls MCPResponse(**response) which will raise a Pydantic v2
ValidationError if Unity returns extra unexpected fields; update the MCPResponse
model in Server/src/models/models.py to allow unknown fields by adding
model_config = ConfigDict(extra="ignore") (import ConfigDict from pydantic) so
unpacking arbitrary response dicts succeeds, then run tests/flake to ensure no
import or typing issues.

@greptile-apps
Copy link

greptile-apps bot commented Jan 3, 2026

Greptile Summary

This PR implements a comprehensive editor readiness system (v2) with external change detection and hardened refresh retry logic.

Major Changes:

  • EditorStateV2 & Caching: Introduced EditorStateCache.cs that maintains a cached, thread-safe snapshot of Unity's editor state updated every 250ms, and exposed it via new get_editor_state_v2 resource endpoint
  • External Changes Detection: Added external_changes_scanner.py that monitors filesystem mtimes in Assets/Packages directories and detects external package dependencies from manifest.json to identify when assets are stale
  • Preflight Guards: Created preflight.py guard system that prevents tools from running during compilation/tests and auto-triggers refresh when external changes are detected
  • Refresh Hardening: Enhanced refresh_unity tool with disconnect recovery logic that polls readiness after Unity drops connection during refresh/compilation cycles
  • Tool Integration: Added preflight checks to manage_asset, manage_gameobject, manage_scene, and run_tests to prevent stale reads and concurrent operation conflicts
  • Test Runner State: Introduced TestRunStatus.cs to track test execution state and prevent overlapping test runs

Key Benefits:

  • Prevents retry loops when Unity disconnects during refresh/compilation (addresses Diagnosing status of unity. CoplayDev/unity-mcp#503-style issues)
  • Detects external file changes even when Unity editor is unfocused or throttled
  • Provides clients with structured advice.ready_for_tools and blocking_reasons for intelligent retry logic
  • Server-side enrichment adds staleness detection and readiness advice without requiring Unity round-trips

Testing:

Strong test coverage including contract validation, external changes scanning (with file: dependencies), and disconnect recovery scenarios.

Confidence Score: 5/5

  • This PR is safe to merge with high confidence - well-architected resilience improvements with comprehensive test coverage
  • The implementation demonstrates excellent software engineering practices: proper thread safety with locks in C#, defensive error handling throughout, backwards compatibility via legacy fallback, comprehensive test coverage of critical paths including edge cases, and clear separation of concerns between caching, scanning, and preflight logic. The changes are additive and non-breaking.
  • No files require special attention - all components are well-implemented with appropriate error handling

Important Files Changed

Filename Overview
MCPForUnity/Editor/Services/EditorStateCache.cs Well-designed caching layer that maintains thread-safe, periodically-updated editor state snapshots with comprehensive v2 contract fields
MCPForUnity/Editor/Tools/RefreshUnity.cs Robust refresh tool with wait-for-ready semantics, proper timeout handling, and structured error responses for test runner conflicts
Server/src/services/resources/editor_state_v2.py Comprehensive v2 resource implementation with legacy fallback, external changes integration, and server-side advice/staleness enrichment
Server/src/services/state/external_changes_scanner.py Lightweight filesystem scanner with manifest.json parsing for external package dependencies - well-optimized with scan throttling and entry limits
Server/src/services/tools/preflight.py Defensive preflight guard that prevents tools from running during compilation/tests, with auto-refresh on external changes detection
Server/src/services/tools/refresh_unity.py Hardened refresh implementation with disconnect recovery logic and proper external changes cleanup after successful refresh

Sequence Diagram

sequenceDiagram
    participant Client as MCP Client
    participant Server as Python Server
    participant ESV2 as editor_state_v2.py
    participant Scanner as external_changes_scanner
    participant Preflight as preflight.py
    participant Tool as Tool (manage_asset/gameobject/scene)
    participant Refresh as refresh_unity.py
    participant Unity as Unity Editor
    participant Cache as EditorStateCache.cs
    participant RefreshTool as RefreshUnity.cs

    Note over Unity,Cache: Unity-side caching (new)
    Unity->>Cache: EditorApplication.update events
    Cache->>Cache: BuildSnapshot() every 250ms
    Cache->>Cache: Lock & store v2 state

    Note over Client,Server: Client polls readiness
    Client->>Server: get resource: editor_state_v2
    Server->>ESV2: get_editor_state_v2(ctx)
    ESV2->>Unity: send "get_editor_state_v2"
    Unity->>Cache: GetSnapshot()
    Cache-->>Unity: Cached v2 snapshot
    Unity-->>ESV2: v2 state data
    ESV2->>Scanner: update_and_get(instance_id)
    Scanner->>Scanner: Scan Assets/Packages mtime
    Scanner-->>ESV2: external_changes_dirty status
    ESV2->>ESV2: _enrich_advice_and_staleness()
    ESV2-->>Server: MCPResponse with advice & staleness
    Server-->>Client: ready_for_tools, blocking_reasons

    Note over Client,Tool: Client invokes tool when ready
    Client->>Server: call tool: manage_asset
    Server->>Tool: manage_asset(ctx, params)
    Tool->>Preflight: preflight(wait_for_no_compile, refresh_if_dirty)
    Preflight->>ESV2: get_editor_state_v2(ctx)
    ESV2-->>Preflight: Check external_changes_dirty=true
    
    alt External changes detected
        Preflight->>Refresh: refresh_unity(mode="if_dirty", wait_for_ready=True)
        Refresh->>Unity: send "refresh_unity"
        Unity->>RefreshTool: HandleCommand()
        RefreshTool->>RefreshTool: AssetDatabase.Refresh()
        RefreshTool->>RefreshTool: WaitForUnityReadyAsync()
        
        alt Unity disconnects during refresh
            Unity-->>Refresh: {success: false, hint: "retry"}
            Refresh->>Refresh: Poll editor_state_v2 until ready
            Refresh->>ESV2: get_editor_state_v2(ctx)
            ESV2-->>Refresh: ready_for_tools=true
        end
        
        RefreshTool-->>Unity: Refresh complete
        Unity-->>Refresh: Success response
        Refresh->>Scanner: clear_dirty(instance_id)
        Refresh-->>Preflight: Refresh succeeded
    end
    
    Preflight->>ESV2: Check compilation status
    
    alt Still compiling
        Preflight->>Preflight: Wait loop (max 30s)
        Preflight->>ESV2: Poll editor_state_v2
        ESV2-->>Preflight: is_compiling status
    end
    
    Preflight-->>Tool: None (proceed) or busy MCPResponse
    
    alt Preflight blocks
        Tool-->>Server: MCPResponse busy/retry
        Server-->>Client: Error with retry_after_ms
    else Preflight allows
        Tool->>Unity: send tool command
        Unity-->>Tool: Tool result
        Tool-->>Server: Success response
        Server-->>Client: Tool result
    end
Loading

… stall prevention

- Add TestRunnerNoThrottle.cs: Sets editor to 'No Throttling' mode during test runs
  with SessionState persistence across domain reload
- Add run_tests_async and get_test_job tools for non-blocking test execution
- Add TestJobManager for async test job tracking with progress monitoring
- Add ForceSynchronousImport to all AssetDatabase.Refresh() calls to prevent stalls
- Mark DomainReloadResilienceTests as [Explicit] with documentation explaining
  the test infrastructure limitation (internal coroutine waits vs MCP socket polling)
- MCP workflow is unaffected - socket messages provide external stimulus that
  keeps Unity responsive even when backgrounded
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
MCPForUnity/Editor/Services/EditorStateCache.cs (1)

183-194: The placeholder fields (external_changes_dirty, external_changes_last_seen_unix_ms, and refresh fields) were already flagged in a previous review as hardcoded with no update mechanism. This remains unaddressed but has already been noted.

🧹 Nitpick comments (6)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs (1)

60-76: Cleanup logic is correct but slightly redundant.

The TearDown logic individually deletes _createdAssetPath (lines 60-63) before deleting _runRoot (lines 73-76). Since all test assets are created under _runRoot, the individual deletion of _createdAssetPath is redundant—deleting _runRoot recursively handles it. However, this explicit approach is defensive and ensures clarity, so it's a reasonable trade-off for maintainability.

Note: The materials (_matAPath, _matBPath) correctly require individual deletion since they're created at the TempRoot level, not under _runRoot.

MCPForUnity/Editor/Services/TestRunnerService.cs (1)

266-293: Consider returning null instead of 0 on error to distinguish "unknown" from "zero tests".

When an exception occurs during traversal, returning 0 conflates "error occurred" with "no leaf tests found." Since RunStarted already handles null as the unknown case (line 182), this helper should return null on failure for consistency.

🔎 Proposed fix
-        private static int CountLeafTests(ITestAdaptor node)
+        private static int? CountLeafTests(ITestAdaptor node)
         {
             if (node == null)
             {
-                return 0;
+                return null;
             }

             if (!node.HasChildren)
             {
                 return 1;
             }

             int total = 0;
             try
             {
                 foreach (var child in node.Children)
                 {
-                    total += CountLeafTests(child);
+                    var childCount = CountLeafTests(child);
+                    if (!childCount.HasValue)
+                    {
+                        return null;
+                    }
+                    total += childCount.Value;
                 }
             }
             catch
             {
                 // If Unity changes the adaptor behavior, treat it as "unknown total".
-                return 0;
+                return null;
             }

             return total;
         }
MCPForUnity/Editor/Services/EditorStateCache.cs (1)

168-173: since_unix_ms always equals observed_at_unix_ms, which may not reflect actual phase start time.

The activity.since_unix_ms field is set to _observedUnixMs (the current snapshot time), but for meaningful phase tracking, it should capture when the current phase actually started (e.g., when compilation began).

This would require tracking phase transition timestamps separately, which could be deferred to a future iteration if phase duration tracking is not immediately needed.

MCPForUnity/Editor/Tools/RunTestsAsync.cs (1)

70-72: String-based exception detection is fragile.

Checking ex.Message.IndexOf("already in progress", ...) is brittle and could break if the exception message wording changes. Consider catching a specific exception type or adding a dedicated exception class.

🔎 Proposed approach

Define a custom exception type (e.g., TestRunAlreadyInProgressException) in TestJobManager.StartJob and catch it explicitly:

catch (TestRunAlreadyInProgressException)
{
    return Task.FromResult<object>(new ErrorResponse("tests_running", new { reason = "tests_running", retry_after_ms = 5000 }));
}
catch (Exception ex)
{
    return Task.FromResult<object>(new ErrorResponse($"Failed to start test job: {ex.Message}"));
}
Server/src/services/tools/test_jobs.py (1)

64-66: Consider extracting duplicated response handling logic.

Both run_tests_async and get_test_job have identical response normalization logic. This could be extracted to a helper function for DRY compliance.

🔎 Proposed helper
def _normalize_response(response: Any) -> dict[str, Any] | MCPResponse:
    if isinstance(response, dict) and not response.get("success", True):
        return MCPResponse(**response)
    return response if isinstance(response, dict) else MCPResponse(success=False, error=str(response)).model_dump()

Then use return _normalize_response(response) in both functions.

Also applies to: 90-92

MCPForUnity/Editor/Services/TestJobManager.cs (1)

297-316: Clarify: Succeeded means job completion, not test pass rate.

The status is set to Succeeded regardless of test failures. This is likely intentional (the job completed, even if tests failed), but consider adding a brief comment to clarify that Succeeded indicates successful job completion, with actual test results available in Result.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d58862 and dd5034e.

📒 Files selected for processing (21)
  • MCPForUnity/Editor/Services/EditorStateCache.cs
  • MCPForUnity/Editor/Services/TestJobManager.cs
  • MCPForUnity/Editor/Services/TestJobManager.cs.meta
  • MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs
  • MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs.meta
  • MCPForUnity/Editor/Services/TestRunnerService.cs
  • MCPForUnity/Editor/Tools/GetTestJob.cs
  • MCPForUnity/Editor/Tools/GetTestJob.cs.meta
  • MCPForUnity/Editor/Tools/ManageAsset.cs
  • MCPForUnity/Editor/Tools/ManageGameObject.cs
  • MCPForUnity/Editor/Tools/ManageScene.cs
  • MCPForUnity/Editor/Tools/ManageScript.cs
  • MCPForUnity/Editor/Tools/ManageShader.cs
  • MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
  • MCPForUnity/Editor/Tools/RefreshUnity.cs
  • MCPForUnity/Editor/Tools/RunTestsAsync.cs
  • MCPForUnity/Editor/Tools/RunTestsAsync.cs.meta
  • Server/src/services/tools/test_jobs.py
  • Server/tests/integration/test_test_jobs_async.py
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs
✅ Files skipped from review due to trivial changes (4)
  • MCPForUnity/Editor/Services/TestJobManager.cs.meta
  • MCPForUnity/Editor/Tools/GetTestJob.cs.meta
  • MCPForUnity/Editor/Tools/RunTestsAsync.cs.meta
  • MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs.meta
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-21T05:19:59.258Z
Learnt from: dsarno
Repo: dsarno/unity-mcp PR: 85
File: tests/test_telemetry_queue_worker.py:33-37
Timestamp: 2025-10-21T05:19:59.258Z
Learning: In the unity-mcp repository, pyproject.toml is located in MCPForUnity/UnityMcpServer~/src/, not at the repository root. When testing code that imports telemetry.py, the working directory should be changed to SRC (MCPForUnity/UnityMcpServer~/src/) so that telemetry.py's get_package_version() can find pyproject.toml via relative path.

Applied to files:

  • Server/src/services/tools/test_jobs.py
🧬 Code graph analysis (6)
MCPForUnity/Editor/Tools/RunTestsAsync.cs (3)
MCPForUnity/Editor/Tools/GetTestJob.cs (2)
  • McpForUnityTool (11-51)
  • HandleCommand (14-50)
MCPForUnity/Editor/Resources/Tests/GetTests.cs (2)
  • ModeParser (77-105)
  • TryParse (79-104)
MCPForUnity/Editor/Services/TestJobManager.cs (3)
  • TestJobManager (49-571)
  • TestJobManager (64-68)
  • StartJob (234-295)
MCPForUnity/Editor/Tools/GetTestJob.cs (2)
MCPForUnity/Editor/Services/TestJobManager.cs (3)
  • TestJobManager (49-571)
  • TestJobManager (64-68)
  • ToSerializable (422-460)
MCPForUnity/Editor/Services/TestRunnerService.cs (3)
  • ToSerializable (454-482)
  • ToSerializable (531-542)
  • ToSerializable (573-585)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs (2)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/Fixtures/ManageScriptableObjectTestDefinition.cs (1)
  • ManageScriptableObjectTestDefinition (14-23)
MCPForUnity/Editor/Tools/ManageScriptableObject.cs (1)
  • HandleCommand (42-77)
Server/tests/integration/test_test_jobs_async.py (2)
Server/tests/integration/test_helpers.py (1)
  • DummyContext (16-55)
Server/src/services/tools/test_jobs.py (2)
  • run_tests_async (17-66)
  • get_test_job (70-92)
MCPForUnity/Editor/Services/EditorStateCache.cs (3)
MCPForUnity/Editor/Helpers/McpLog.cs (2)
  • McpLog (7-52)
  • Error (48-51)
MCPForUnity/Editor/Services/TestRunStatus.cs (1)
  • TestRunStatus (10-59)
MCPForUnity/Editor/Services/TestJobManager.cs (2)
  • TestJobManager (49-571)
  • TestJobManager (64-68)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs (2)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs (1)
  • TearDown (56-90)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MCPToolParameterTests.cs (1)
  • TearDown (46-70)
🪛 Ruff (0.14.10)
Server/tests/integration/test_test_jobs_async.py

12-12: Unused function argument: send_fn

(ARG001)


12-12: Unused function argument: unity_instance

(ARG001)


12-12: Unused function argument: kwargs

(ARG001)


39-39: Unused function argument: send_fn

(ARG001)


39-39: Unused function argument: unity_instance

(ARG001)


39-39: Unused function argument: kwargs

(ARG001)

🔇 Additional comments (43)
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageScriptableObjectTests.cs (4)

19-20: Excellent per-run isolation pattern.

The introduction of unique per-run directories (_runRoot) with GUID-based naming effectively eliminates reimport churn that would occur from deleting and recreating the entire TempRoot between tests. The computed _nestedFolder path provides good test coverage for deep folder hierarchies.

Also applies to: 31-36


93-121: Well-focused test for nested folder creation.

Splitting this concern into its own test improves clarity and makes test failures easier to diagnose. The assertions thoroughly validate:

  • Nested folder structure is created (line 114)
  • Asset placement under the correct hierarchy (line 115)
  • Asset file extension (line 116)
  • Asset can be loaded with the correct type (lines 119-120)

123-160: Good separation of concerns with lightweight path strategy.

Using _runRoot instead of _nestedFolder for the patch test (line 131) is a sensible optimization—it keeps this test focused on patch correctness without the overhead of nested folder creation. The comment on line 130 clearly explains the rationale.

The patch validation assertions (lines 157-159) cover private fields, inherited fields, and nested struct properties, providing comprehensive test coverage.


170-170: Correct usage of per-run isolation.

Updating this test to use _runRoot maintains consistency with the new isolation pattern.

MCPForUnity/Editor/Tools/ManageGameObject.cs (1)

585-597: Synchronous refresh after prefab folder creation is reasonable and consistent

Forcing a synchronous AssetDatabase.Refresh right after creating the directory ensures the asset DB definitely sees the new folder before SaveAsPrefabAssetAndConnect, which matches the rest of the PR’s “deterministic refresh” story. Given this path is user/tool‑triggered (not hot‑loop), the extra blocking refresh is acceptable.

MCPForUnity/Editor/Tools/ManageScene.cs (1)

220-226: Synchronous refresh for new/updated scenes and screenshots aligns with deterministic editor state

Using AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport) after scene creation, scene save, and screenshot capture makes the new/changed assets immediately visible to tooling that inspects the project state right after these operations. Given these are explicit editor commands (not per‑frame), the extra blocking cost is an acceptable tradeoff for the more predictable state your v2 editor‑state and refresh workflows need.

Also applies to: 363-369, 382-423

MCPForUnity/Editor/Tools/ManageScript.cs (1)

975-985: Immediate refresh after moving script to trash keeps script state consistent with other flows

Calling AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport) right after MoveAssetToTrash makes the deletion visible to the asset DB and compilation pipeline immediately, matching the synchronous import semantics you use for apply_text_edits and structured edits. This keeps downstream editor‑state / refresh tooling from briefly seeing stale scripts.

MCPForUnity/Editor/Tools/ManageAsset.cs (2)

180-184: LGTM! Synchronous refresh ensures directory recognition.

The forced synchronous import after directory creation ensures Unity's AssetDatabase recognizes the new folder before subsequent asset operations, reducing timing-related failures.


864-874: LGTM! Consistent synchronous refresh pattern.

The forced synchronous import ensures Unity recognizes the new directory immediately, maintaining consistency with the pattern applied at line 183.

MCPForUnity/Editor/Tools/ManageShader.cs (3)

89-106: LGTM! Synchronous refresh after directory creation.

The forced synchronous import ensures Unity recognizes the new shader directory before proceeding with create/update operations.


173-187: LGTM! Ensures shader is immediately available.

The synchronous refresh after shader creation ensures Unity fully imports and recognizes the shader, making it immediately available for subsequent operations like Shader.Find().


241-255: LGTM! Consistent synchronous refresh on shader update.

The forced synchronous import ensures shader changes are immediately reflected in Unity's AssetDatabase, maintaining consistency with the CreateShader pattern.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs (4)

15-24: LGTM! Clear documentation of test infrastructure limitation.

The updated documentation clearly explains that these tests are marked [Explicit] due to test infrastructure limitations (internal Unity coroutine waits stall when backgrounded), not issues with the MCP workflow itself. This helps maintainers understand the context.


29-38: LGTM! Synchronous refresh in test setup.

The forced synchronous import ensures the temp directory is fully recognized by Unity before tests execute, preventing timing-related failures.


40-63: Excellent! Critical synchronous refresh in TearDown.

The forced synchronous refresh at the end of TearDown is critical for preventing compilation from running into subsequent tests. The clear comment explains this prevents stalling other tests, which is especially important given these tests intentionally trigger domain reloads.


85-87: LGTM! Synchronous refresh after script creation in tests.

The forced synchronous imports after script file creation ensure Unity immediately starts compilation and recognizes the new scripts. This is correct for these domain reload resilience tests, which need deterministic timing for their assertions.

Also applies to: 177-178, 225-226

MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs (1)

214-228: LGTM! Consistent synchronous refresh pattern for prefab directories.

The forced synchronous import ensures Unity recognizes the prefab directory immediately before attempting to save prefab assets, maintaining consistency with the pattern applied across ManageAsset.cs and ManageShader.cs.

MCPForUnity/Editor/Tools/GetTestJob.cs (3)

14-20: LGTM! Flexible parameter name handling.

Supporting both job_id and jobId parameter names provides good flexibility for API consumers while maintaining clear error messaging.


22-40: LGTM! Lenient boolean parsing with safe defaults.

The boolean parsing is intentionally lenient (silently ignoring parse failures) and defaults to false for both flags. This provides good API robustness for optional parameters.


42-50: LGTM! Clean delegation to TestJobManager.

The tool appropriately delegates job retrieval and serialization to TestJobManager, maintaining good separation of concerns. Error handling for unknown job IDs is clear and user-friendly.

MCPForUnity/Editor/Services/TestRunnerService.cs (3)

96-97: LGTM - Early state marking ensures consistency.

Marking the test run as started before _testRunnerApi.Execute() ensures the readiness snapshot reflects the busy state even if callbacks are delayed. This is a good defensive pattern.


170-183: LGTM - Robust callback handling with fallback.

The try-catch around leaf test counting with a null fallback on failure is appropriate since the adaptor API may change. This ensures the run proceeds even if counting fails.


229-260: LGTM - Defensive failure detection with nested error handling.

The nested try-catch for outcome parsing and the outer try-catch for reporting isolate adaptor quirks from the main test result collection. The string-based failure detection ("failed" or "error") handles NUnit's adaptor states appropriately.

MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs (2)

72-87: LGTM - Idempotent settings capture.

The AreSettingsCaptured() guard prevents overwriting the original settings if ApplyNoThrottling is called multiple times (e.g., after domain reload recovery).


102-116: LGTM - Reflection fallback is appropriately guarded.

Using reflection on an internal Unity method is fragile, but the empty catch ensures the feature degrades gracefully if Unity's internals change.

Server/tests/integration/test_test_jobs_async.py (1)

12-15: Unused parameters in mock functions are intentional and correct.

The static analysis hints about unused send_fn, unity_instance, and kwargs parameters are false positives. These mock functions must match the signature of the real send_with_unity_instance function to work correctly with monkeypatch.setattr.

Also applies to: 39-42

MCPForUnity/Editor/Services/EditorStateCache.cs (3)

35-63: LGTM - Robust initialization with event subscriptions.

The static constructor correctly subscribes to editor lifecycle events and handles initialization failures gracefully. The domain reload tracking via beforeAssemblyReload/afterAssemblyReload ensures accurate state across reloads.


65-81: LGTM - Smart throttling with edge-triggered updates.

The throttle logic correctly bypasses the interval check when compilation state changes, ensuring timestamps remain meaningful while reducing overhead during idle periods.


219-230: LGTM - Thread-safe snapshot access with defensive cloning.

The lock ensures thread-safety and DeepClone() prevents callers from mutating the cached state. The null check with rebuild is a good defensive measure.

MCPForUnity/Editor/Tools/RunTestsAsync.cs (2)

78-124: LGTM - Robust filter options parsing.

The ParseStringArray helper correctly handles both single strings and arrays, with proper null/empty filtering. Returning null when no filters are specified avoids unnecessary allocations.


34-53: No action needed. The includeDetails and includeFailedTests parameters are correctly handled. These flags control result serialization (used by ToSerializable), not job execution. The pattern is: in RunTestsAsync, they're parsed and echoed in the response so the client knows what preferences were set; when the client later calls GetTestJob, it resends these same flags, which GetTestJob then passes to ToSerializable to format results appropriately. Passing them to StartJob would be incorrect since job execution parameters are separate from display preferences.

MCPForUnity/Editor/Tools/RefreshUnity.cs (2)

129-171: LGTM - Well-structured async polling with proper cleanup.

The WaitForUnityReadyAsync implementation correctly:

  • Removes itself from update on all exit paths (completion, timeout, exception)
  • Uses TaskCompletionSource with RunContinuationsAsynchronously to avoid deadlocks
  • Includes a QueuePlayerLoopUpdate nudge for throttled editors

41-48: LGTM - Early exit prevents refresh during test runs.

Returning early with retry guidance when tests are running prevents disrupting the test run with asset database refreshes.

Server/src/services/tools/test_jobs.py (2)

16-66: LGTM - Well-structured async test runner with proper preflight.

The function correctly:

  • Gates on preflight checks before starting tests
  • Coerces filter parameters to list format with whitespace handling
  • Only includes optional params when they have meaningful values

69-92: LGTM - Clean job polling implementation.

The get_test_job function is straightforward with proper parameter handling and response normalization.

MCPForUnity/Editor/Services/TestJobManager.cs (8)

1-44: LGTM!

The data structures are well-defined. TestJob captures comprehensive state for tracking async test runs including progress, failures, and timing information.


46-84: LGTM!

Good use of SessionState for domain reload survival. Thread-safe property access with consistent locking on shared state.


86-232: LGTM!

Persistence logic is well-designed with best-effort semantics. The separation of taking a snapshot under lock and serializing outside the lock is correct. Legacy key migration is a nice touch.


277-295: LGTM!

Good defensive pattern: the task continuation acts as a safety net while the primary finalization happens via RunFinished callback. FinalizeFromTask correctly checks if the job was already finalized before modifying state.


318-408: LGTM!

Progress callbacks are well-implemented with consistent locking, early exits for missing jobs, and proper state persistence. The failure cap is correctly enforced.


410-460: LGTM!

Clean serialization with comprehensive diagnostic fields (stuck_suspected, editor_is_focused, blocked_reason). This will be helpful for debugging stalled test runs.


462-507: LGTM!

Good diagnostic helpers. The stuck detection with a 60-second threshold and the blocked reason categorization will help identify why tests may be stalling.


509-571: LGTM!

FinalizeFromTask correctly guards against double finalization by checking if the job is still Running. The exception unwrapping via GetBaseException() properly handles AggregateException from tasks.

Comment on lines 39 to 57
static TestRunnerNoThrottle()
{
try
{
var api = ScriptableObject.CreateInstance<TestRunnerApi>();
api.RegisterCallbacks(new TestCallbacks());

// Check if recovering from domain reload during an active test run
if (IsTestRunActive())
{
McpLog.Info("[TestRunnerNoThrottle] Recovered from domain reload - reapplying No Throttling.");
ApplyNoThrottling();
}
}
catch (Exception e)
{
McpLog.Warn($"[TestRunnerNoThrottle] Failed to register callbacks: {e}");
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

TestRunnerApi instance is created but never disposed.

The TestRunnerApi created on line 43 is a ScriptableObject that should be disposed to avoid resource leaks. Consider storing a reference and implementing cleanup, or use a static lifetime pattern with HideFlags.HideAndDontSave.

🔎 Proposed fix
+        private static TestRunnerApi _api;
+
         static TestRunnerNoThrottle()
         {
             try
             {
-                var api = ScriptableObject.CreateInstance<TestRunnerApi>();
-                api.RegisterCallbacks(new TestCallbacks());
+                _api = ScriptableObject.CreateInstance<TestRunnerApi>();
+                _api.hideFlags = HideFlags.HideAndDontSave;
+                _api.RegisterCallbacks(new TestCallbacks());

                 // Check if recovering from domain reload during an active test run
                 if (IsTestRunActive())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static TestRunnerNoThrottle()
{
try
{
var api = ScriptableObject.CreateInstance<TestRunnerApi>();
api.RegisterCallbacks(new TestCallbacks());
// Check if recovering from domain reload during an active test run
if (IsTestRunActive())
{
McpLog.Info("[TestRunnerNoThrottle] Recovered from domain reload - reapplying No Throttling.");
ApplyNoThrottling();
}
}
catch (Exception e)
{
McpLog.Warn($"[TestRunnerNoThrottle] Failed to register callbacks: {e}");
}
}
private static TestRunnerApi _api;
static TestRunnerNoThrottle()
{
try
{
_api = ScriptableObject.CreateInstance<TestRunnerApi>();
_api.hideFlags = HideFlags.HideAndDontSave;
_api.RegisterCallbacks(new TestCallbacks());
// Check if recovering from domain reload during an active test run
if (IsTestRunActive())
{
McpLog.Info("[TestRunnerNoThrottle] Recovered from domain reload - reapplying No Throttling.");
ApplyNoThrottling();
}
}
catch (Exception e)
{
McpLog.Warn($"[TestRunnerNoThrottle] Failed to register callbacks: {e}");
}
}
🤖 Prompt for AI Agents
In MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs around lines 39-57, the
TestRunnerApi ScriptableObject created in the static constructor is never stored
or cleaned up; store it in a private static TestRunnerApi field, set
api.hideFlags = HideFlags.HideAndDontSave to avoid accidental scene persistence,
and register a cleanup callback (e.g., AssemblyReloadEvents.beforeAssemblyReload
or EditorApplication.quitting) that calls DestroyImmediate on the stored
instance and nulls the field so the ScriptableObject is properly disposed on
domain reload/editor quit; ensure any registration of callbacks uses the stored
api instance.

Comment on lines +61 to +71
if (string.Equals(scope, "scripts", StringComparison.OrdinalIgnoreCase))
{
// For scripts, requesting compilation is usually the meaningful action.
// We avoid a heavyweight full refresh by default.
}
else
{
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
refreshTriggered = true;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The scripts scope branch is a no-op with only a comment.

When scope is "scripts", lines 61-65 do nothing, then the fallback at lines 79-86 triggers a full refresh anyway (since refreshTriggered is false and scope is not "all"). This seems unintended. If scripts-only refresh should skip asset refresh, the fallback condition needs adjustment.

🔎 Proposed fix to skip fallback refresh for scripts scope
-                if (string.Equals(scope, "all", StringComparison.OrdinalIgnoreCase) && !refreshTriggered)
+                if (!refreshTriggered && !string.Equals(scope, "scripts", StringComparison.OrdinalIgnoreCase))
                 {

Or, if scripts scope should trigger compilation by default:

                 if (string.Equals(scope, "scripts", StringComparison.OrdinalIgnoreCase))
                 {
                     // For scripts, requesting compilation is usually the meaningful action.
-                    // We avoid a heavyweight full refresh by default.
+                    CompilationPipeline.RequestScriptCompilation();
+                    compileRequested = true;
                 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +7 to +30
async def test_run_tests_async_forwards_params(monkeypatch):
from services.tools.test_jobs import run_tests_async

captured = {}

async def fake_send_with_unity_instance(send_fn, unity_instance, command_type, params, **kwargs):
captured["command_type"] = command_type
captured["params"] = params
return {"success": True, "data": {"job_id": "abc123", "status": "running"}}

import services.tools.test_jobs as mod
monkeypatch.setattr(mod.unity_transport, "send_with_unity_instance", fake_send_with_unity_instance)

resp = await run_tests_async(
DummyContext(),
mode="EditMode",
test_names="MyNamespace.MyTests.TestA",
include_details=True,
)
assert captured["command_type"] == "run_tests_async"
assert captured["params"]["mode"] == "EditMode"
assert captured["params"]["testNames"] == ["MyNamespace.MyTests.TestA"]
assert captured["params"]["includeDetails"] is True
assert resp["success"] is True
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Test may fail due to unmocked preflight dependency.

The run_tests_async function calls preflight(ctx, ...) before the transport call. Without mocking preflight, the test may fail or have unintended side effects. Consider mocking the preflight to return a non-MCPResponse value.

🔎 Proposed fix
 @pytest.mark.asyncio
 async def test_run_tests_async_forwards_params(monkeypatch):
     from services.tools.test_jobs import run_tests_async

     captured = {}

     async def fake_send_with_unity_instance(send_fn, unity_instance, command_type, params, **kwargs):
         captured["command_type"] = command_type
         captured["params"] = params
         return {"success": True, "data": {"job_id": "abc123", "status": "running"}}

+    async def fake_preflight(ctx, **kwargs):
+        return None  # Non-MCPResponse indicates success
+
     import services.tools.test_jobs as mod
     monkeypatch.setattr(mod.unity_transport, "send_with_unity_instance", fake_send_with_unity_instance)
+    monkeypatch.setattr(mod, "preflight", fake_preflight)

     resp = await run_tests_async(
         DummyContext(),
🧰 Tools
🪛 Ruff (0.14.10)

12-12: Unused function argument: send_fn

(ARG001)


12-12: Unused function argument: unity_instance

(ARG001)


12-12: Unused function argument: kwargs

(ARG001)

🤖 Prompt for AI Agents
In Server/tests/integration/test_test_jobs_async.py around lines 7 to 30, the
test calls run_tests_async which invokes preflight(ctx, ...); since preflight is
not mocked the test may fail or produce side effects—mock preflight via
monkeypatch to return a simple non-MCPResponse (e.g. a dict or None) before
calling run_tests_async so the preflight path is bypassed; place the
monkeypatch.setattr for services.tools.test_jobs.preflight near where
unity_transport.send_with_unity_instance is patched and ensure the fake
preflight signature matches the real one so the test continues to assert the
forwarded params.

dsarno added 14 commits January 3, 2026 11:02
- Remove unused Newtonsoft.Json.Linq import from TestJobManager
- Add throttling to SessionState persistence (once per second) to reduce overhead
- Critical job state changes (start/finish) still persist immediately
- Fix duplicate XML summary tag in DomainReloadResilienceTests
- Add run_tests_async and get_test_job to main README tools list
- Document background stall limitation for domain reload tests in DEV readme
Run [Explicit] domain_reload tests in their own job using -testCategory
Combines into single job with two test steps to reuse cached Library
- Fix TOCTOU race in TestJobManager.StartJob (single lock scope for check-and-set)
- Store TestRunnerApi reference with HideAndDontSave to prevent GC/serialization issues
- run_tests_async is now marked as preferred for long-running suites
- run_tests description notes it blocks and suggests async alternative
…sults

- manage_asset, manage_gameobject, manage_scene now check preflight return
  value and propagate busy/retry signals to clients (fixes Sourcery #1)
- TestJobManager.FinalizeCurrentJobFromRunFinished now sets job status to
  Failed when resultPayload.Failed > 0, not always Succeeded (fixes Sourcery #2)
When 'Force fresh server install' is enabled, uvx uses --no-cache --refresh
which rebuilds the package and takes significantly longer to start.

- Increase timeout from 10s to 45s when dev mode is enabled
- Add informative log message explaining the longer startup time
- Show actual timeout value in warning message
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs (1)

42-61: Missing cleanup callback for TestRunnerApi disposal.

While the static field and HideFlags.HideAndDontSave have been added (addressing part of the earlier feedback), there's still no cleanup callback to call DestroyImmediate(_api) on editor quit or assembly reload. Without this, the ScriptableObject may leak.

Consider adding a cleanup hook:

AssemblyReloadEvents.beforeAssemblyReload += () => {
    if (_api != null)
    {
        ScriptableObject.DestroyImmediate(_api);
        _api = null;
    }
};
🧹 Nitpick comments (2)
MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs (1)

106-120: Silent failure is acceptable, but consider debug logging.

The silent catch is reasonable for production robustness. However, if the reflection fails (e.g., Unity API changes in a future version), debugging could be difficult.

Consider adding a debug log when the method isn't found:

if (method == null)
{
    McpLog.Debug("[TestRunnerNoThrottle] UpdateInteractionModeSettings not found - interaction mode may not apply immediately.");
}
README.md (1)

19-19: Minor: Missing period after "etc" in American English.

Static analysis suggests adding a period after "etc" for consistency with American English conventions: (Claude, Cursor, Antigravity, VS Code, etc.)

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd5034e and 72cb011.

⛔ Files ignored due to path filters (1)
  • docs/images/unity-mcp-ui-v8.6.png is excluded by !**/*.png
📒 Files selected for processing (11)
  • .github/workflows/unity-tests.yml
  • MCPForUnity/Editor/Services/TestJobManager.cs
  • MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs
  • README.md
  • Server/src/services/tools/manage_asset.py
  • Server/src/services/tools/manage_gameobject.py
  • Server/src/services/tools/manage_scene.py
  • Server/src/services/tools/run_tests.py
  • Server/src/services/tools/test_jobs.py
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs
  • docs/README-DEV.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/DomainReloadResilienceTests.cs
🧰 Additional context used
🧬 Code graph analysis (5)
Server/src/services/tools/manage_scene.py (2)
Server/src/services/tools/preflight.py (1)
  • preflight (27-105)
Server/tests/integration/test_helpers.py (1)
  • model_dump (10-13)
MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs (1)
MCPForUnity/Editor/Helpers/McpLog.cs (3)
  • McpLog (7-52)
  • Info (37-41)
  • Warn (43-46)
Server/src/services/tools/manage_asset.py (2)
Server/src/services/tools/preflight.py (1)
  • preflight (27-105)
Server/tests/integration/test_helpers.py (1)
  • model_dump (10-13)
Server/src/services/tools/test_jobs.py (4)
Server/src/services/tools/preflight.py (1)
  • preflight (27-105)
Server/src/transport/legacy/unity_connection.py (1)
  • async_send_command_with_retry (753-785)
Server/src/services/tools/run_tests.py (1)
  • _coerce_string_list (66-74)
Server/src/transport/unity_transport.py (1)
  • send_with_unity_instance (79-113)
Server/src/services/tools/run_tests.py (1)
Server/src/services/tools/preflight.py (1)
  • preflight (27-105)
🪛 LanguageTool
README.md

[style] ~19-~19: In American English, abbreviations like “etc.” require a period.
Context: ... (Claude, Cursor, Antigravity, VS Code, etc) to interact directly with your Unity E...

(ETC_PERIOD)

🔇 Additional comments (15)
docs/README-DEV.md (1)

340-348: Well-documented workaround for a real OS-level limitation.

The new section clearly explains the macOS background-threading throttle that can stall domain reload tests, provides three practical workarounds, and reassures developers that the core MCP workflow is unaffected. This aligns well with the PR's investment in test tooling and refresh resilience, and will save developers debugging time.

Server/src/services/tools/manage_gameobject.py (2)

11-11: LGTM! Import added correctly.

The import for preflight is properly added and supports the new preflight validation logic.


96-98: Excellent! Critical issue resolved.

The preflight return value is now properly captured and handled. When Unity is busy (compiling, running tests, etc.), the function correctly returns the gate response early, preventing operations during unsafe states. This addresses the previously flagged critical issue.

Server/src/services/tools/manage_scene.py (1)

9-9: Excellent fix: Critical issue resolved.

The preflight return value is now properly captured and checked. When Unity is busy (gate is non-None), the function correctly returns the busy/retry signal, preventing the tool from executing prematurely. This addresses the critical issue raised in the previous review.

Also applies to: 44-46

.github/workflows/unity-tests.yml (1)

67-71: AI summary inconsistency: Upload step still present.

The AI-generated summary states "Removed the Upload test results step from the workflow," but this upload-artifact step is still present in the code. The actual code takes precedence.

MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs (4)

63-74: LGTM!

Clean helper abstractions over SessionState for persisting throttle state across domain reloads.


76-91: LGTM!

The guard AreSettingsCaptured() correctly prevents overwriting previously saved settings during domain reload recovery.


93-104: LGTM!

Restoration logic correctly clears both state flags after restoring the original throttling settings.


122-137: LGTM!

Clean implementation of ICallbacks with appropriate lifecycle hooks.

Server/src/services/tools/run_tests.py (2)

61-63: LGTM: Preflight gating prevents invalid test runs.

The preflight check with requires_no_tests=True ensures Unity is ready and no tests are already running before starting a new test run. This aligns well with the PR's goal of hardening retry logic.


106-118: LGTM: Structured error handling prevents retry loops.

The specialized handling for "test run is already in progress" errors returns a structured MCPResponse with retry guidance instead of a generic failure. This prevents clients from misinterpreting the error and entering retry loops (addresses issue CoplayDev#503 mentioned in comments).

MCPForUnity/Editor/Services/TestJobManager.cs (3)

269-278: LGTM: TOCTOU race condition resolved.

The previous review comment flagged a TOCTOU race between the check and set of _currentJobId. This has been properly addressed by combining the check (line 272) and registration (lines 276-277) within a single lock scope, ensuring atomicity. Well done.


190-242: LGTM: Persistence throttling prevents overhead.

The throttling logic (lines 194-198) limits persistence to once per second for non-critical updates while allowing forced persistence for critical state changes. This is a smart optimization for large test runs that would otherwise trigger frequent SessionState writes.


471-500: LGTM: Comprehensive blocked reason detection.

The blocked reason logic checks the most common causes of test run delays (editor unfocused, compilation, asset import) and provides actionable feedback to clients. The editor focus check (line 484) is particularly valuable for catching OS-level throttling of background Unity processes.

Server/src/services/tools/manage_asset.py (1)

15-15: LGTM!

The import is correctly added and used in the preflight guard below.

Comment on lines +41 to +54
# Run domain reload tests first (they're [Explicit] so need explicit category)
- name: Run domain reload tests
uses: game-ci/unity-test-runner@v4
id: domain-tests
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: ${{ matrix.testMode }}
customParameters: -testCategory domain_reload

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Domain reload test artifacts are not uploaded.

The new domain reload test step produces artifacts via steps.domain-tests.outputs.artifactsPath, but these are not captured by the upload step (Line 67-71), which only uploads steps.tests.outputs.artifactsPath. Consider uploading both sets of test results for complete CI visibility.

🔎 Proposed fix to upload domain reload test artifacts

Add a separate upload step for domain reload tests:

       customParameters: -testCategory domain_reload

+     - uses: actions/upload-artifact@v4
+       if: always()
+       with:
+         name: Test results for ${{ matrix.testMode }} (domain reload)
+         path: ${{ steps.domain-tests.outputs.artifactsPath }}
+
      - name: Run tests
        uses: game-ci/unity-test-runner@v4
🤖 Prompt for AI Agents
.github/workflows/unity-tests.yml around lines 41 to 54: the domain reload test
step (id: domain-tests) produces artifacts but they aren’t uploaded; update the
workflow to capture and upload those artifacts as well by adding an
upload-artifacts step that references steps.domain-tests.outputs.artifactsPath
(either add a new upload step for domain-tests or modify the existing upload
step to upload both steps.tests.outputs.artifactsPath and
steps.domain-tests.outputs.artifactsPath), ensuring the step uses the same
name/path pattern and runs after the domain-tests step.

Comment on lines +51 to +55
# Best-effort guard: if Unity is compiling/reloading or known external changes are pending,
# wait/refresh to avoid stale reads and flaky timeouts.
gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True)
if gate is not None:
return gate.model_dump()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all callers of manage_asset function
rg -nP --type=py -C3 '\bmanage_asset\s*\(' -g '!**/manage_asset.py' | head -100

Repository: dsarno/unity-mcp

Length of output: 4818


🏁 Script executed:

#!/bin/bash
# Check for test coverage of the preflight integration with manage_asset
rg -nP --type=py -C2 'preflight|manage_asset' -g '**/test_*.py' -g '**/*_test.py' | head -100

Repository: dsarno/unity-mcp

Length of output: 8210


🏁 Script executed:

#!/bin/bash
# Verify MCPResponse class definition and model_dump() method
rg -nP --type=py -A10 'class MCPResponse' | head -50

Repository: dsarno/unity-mcp

Length of output: 736


🏁 Script executed:

#!/bin/bash
# Check the preflight function signature and return type
rg -nP --type=py -B2 -A8 'async def preflight' | head -50

Repository: dsarno/unity-mcp

Length of output: 735


🏁 Script executed:

#!/bin/bash
# Read lines 40-70 of manage_asset.py to see full context around lines 51-55
sed -n '40,70p' Server/src/services/tools/manage_asset.py | cat -n

Repository: dsarno/unity-mcp

Length of output: 2123


🏁 Script executed:

#!/bin/bash
# Check if any tests verify gate != None behavior (preflight returning a response)
rg -nP --type=py 'preflight.*None|gate.*None' Server/tests/

Repository: dsarno/unity-mcp

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Look for test cases that might mock preflight to return a non-None value
rg -nP --type=py -B5 -A5 'preflight.*return|mock.*preflight' Server/tests/

Repository: dsarno/unity-mcp

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Look for any mocking or testing of preflight function itself
rg -nP --type=py 'preflight' Server/tests/ | head -50

Repository: dsarno/unity-mcp

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check if manage_asset tests stub/mock the preflight call
rg -nP --type=py -B10 -A10 'async def test_manage_asset' Server/tests/integration/ | head -100

Repository: dsarno/unity-mcp

Length of output: 1709


🏁 Script executed:

#!/bin/bash
# Check what happens in manage_asset when preflight is called - does it get mocked in tests?
cat -n Server/tests/integration/test_script_tools.py | sed -n '175,220p'

Repository: dsarno/unity-mcp

Length of output: 1494


🏁 Script executed:

#!/bin/bash
# Final check: Look at the full manage_asset function signature and return type
sed -n '1,50p' Server/src/services/tools/manage_asset.py | cat -n

Repository: dsarno/unity-mcp

Length of output: 3179


🏁 Script executed:

#!/bin/bash
# Verify if there are any test files specifically for preflight behavior
find Server/tests -type f -name '*preflight*' 2>/dev/null

Repository: dsarno/unity-mcp

Length of output: 42


Add test coverage for the preflight gate response path.

The preflight guard implementation is correct and follows a clean early-exit pattern. The gate.model_dump() call properly returns a dict[str, Any] as required, and the parameters (wait_for_no_compile=True, refresh_if_dirty=True) are appropriate for asset operations.

However, the existing tests (test_script_tools.py, test_manage_asset_param_coercion.py, test_manage_asset_json_parsing.py) all mock async_send_command_with_retry and never verify the case where preflight returns a non-None gate response. Add a test case that mocks preflight to return an MCPResponse and confirms the function returns gate.model_dump() without executing the asset operation logic.

🤖 Prompt for AI Agents
In Server/src/services/tools/manage_asset.py around lines 51-55, add a unit test
that covers the early-exit preflight gate path by mocking preflight to return a
populated MCPResponse and asserting the function returns gate.model_dump()
without performing asset operations; specifically, patch preflight to return an
MCPResponse instance (with known fields), call the async entry point (e.g.,
manage_asset) with a test ctx, assert the return equals the
MCPResponse.model_dump() value, and also assert that
async_send_command_with_retry (and any other downstream operation mocks) were
not called; implement this using pytest/asyncio and monkeypatch or
unittest.mock.patch and place the test alongside existing manage_asset tests (or
in a new test_manage_asset_preflight.py).

Comment on lines +33 to +41
def _coerce_string_list(value) -> list[str] | None:
if value is None:
return None
if isinstance(value, str):
return [value] if value.strip() else None
if isinstance(value, list):
result = [str(v).strip() for v in value if v and str(v).strip()]
return result if result else None
return None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Code duplication: Extract _coerce_string_list to a shared utility.

This helper function is duplicated from Server/src/services/tools/run_tests.py (lines 65-73). Consider extracting it to a shared utility module (e.g., services/tools/utils.py) to maintain DRY principles and ensure consistent behavior across tools.

🔎 Suggested refactor

Create or update Server/src/services/tools/utils.py:

def coerce_string_list(value) -> list[str] | None:
    """Convert string or list input to a list of non-empty strings."""
    if value is None:
        return None
    if isinstance(value, str):
        return [value] if value.strip() else None
    if isinstance(value, list):
        result = [str(v).strip() for v in value if v and str(v).strip()]
        return result if result else None
    return None

Then import and use in both files:

+from services.tools.utils import coerce_string_list as _coerce_string_list
-    def _coerce_string_list(value) -> list[str] | None:
-        if value is None:
-            return None
-        if isinstance(value, str):
-            return [value] if value.strip() else None
-        if isinstance(value, list):
-            result = [str(v).strip() for v in value if v and str(v).strip()]
-            return result if result else None
-        return None

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Server/src/services/tools/test_jobs.py around lines 33-41, the
_coerce_string_list helper is duplicated from run_tests.py; extract it to a
shared module Server/src/services/tools/utils.py as coerce_string_list (preserve
the exact behavior and signature), remove the duplicate functions from both
test_jobs.py and run_tests.py, and update both files to import
coerce_string_list from services.tools.utils; ensure the new utility uses the
same type hints and behavior, update any references, and run lint/tests to
confirm no import or behavior regressions.

Comment on lines +64 to +66
if isinstance(response, dict) and not response.get("success", True):
return MCPResponse(**response)
return response if isinstance(response, dict) else MCPResponse(success=False, error=str(response)).model_dump()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent return handling and type annotation mismatch.

The return logic is inconsistent:

  • Line 65: Returns MCPResponse(**response) (an MCPResponse instance)
  • Line 66: Returns either response (a dict) or MCPResponse(...).model_dump() (also a dict)

The return type annotation dict[str, Any] | MCPResponse suggests both types are valid returns, but the actual code path on line 66 always returns a dict, never an MCPResponse instance. This creates confusion about the actual contract.

Consider standardizing: either always return MCPResponse instances (removing .model_dump()), or always return dicts (calling .model_dump() on line 65 as well).

Same issue exists in get_test_job at lines 90-92.

🤖 Prompt for AI Agents
In Server/src/services/tools/test_jobs.py around lines 64-66 and similarly in
get_test_job around lines 90-92, the function mixes returning MCPResponse
instances and dicts which contradicts the declared return type; standardize
returns by choosing one form: either always return MCPResponse instances (remove
.model_dump() calls) or always return dicts (call .model_dump() on the
MCPResponse returned on line 65 and the analogous line in get_test_job). Update
both code paths so both branches return the same type and adjust the function
return annotation if needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (1)

630-644: Timeout adjustment for dev mode looks good; consider extracting timeout constants.

The implementation correctly addresses the hardened retry objective by extending the timeout to 45 seconds when dev mode is enabled. The logging is helpful and the dynamic timeout is properly used in both the method call and warning message.

💡 Consider extracting timeout constants for maintainability

The timeout values (45s and 10s) are currently magic numbers. Extracting them as named constants would improve maintainability and make the intent clearer:

+        private static readonly TimeSpan DevModeServerStartupTimeout = TimeSpan.FromSeconds(45);
+        private static readonly TimeSpan DefaultServerStartupTimeout = TimeSpan.FromSeconds(10);
+
         private async Task TryAutoStartSessionAfterHttpServerAsync()
         {
             // ...
             
             // Dev mode uses --no-cache --refresh which causes uvx to rebuild the package, taking significantly longer.
             bool devModeEnabled = false;
             try { devModeEnabled = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); } catch { }
-            var startupTimeout = devModeEnabled ? TimeSpan.FromSeconds(45) : TimeSpan.FromSeconds(10);
+            var startupTimeout = devModeEnabled ? DevModeServerStartupTimeout : DefaultServerStartupTimeout;

Note: The empty catch block on line 632 is consistent with similar patterns in the codebase (e.g., McpLog.ReadDebugPreference). As this is reading a feature flag with a safe default fallback, the defensive approach is reasonable, though logging the exception could aid diagnostics if EditorPrefs access issues occur.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72cb011 and a946be9.

📒 Files selected for processing (1)
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
🧰 Additional context used
🧬 Code graph analysis (1)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (2)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-56)
MCPForUnity/Editor/Helpers/McpLog.cs (3)
  • McpLog (7-52)
  • Info (37-41)
  • Warn (43-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: nl-suite

Apply same logic as FinalizeCurrentJobFromRunFinished: check result.Failed > 0
to correctly mark jobs as Failed when tests fail, even in the fallback path
when RunFinished callback is not delivered.
@dsarno dsarno closed this Jan 4, 2026
@dsarno dsarno deleted the feat/unity-readiness-v2 branch January 4, 2026 22:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants