Summary
On Windows with more than one Unity editor open, the test-job focus nudge (utils/focus_nudge.py, called from services/tools/run_tests.py) repeatedly pulls an unrelated Unity editor to the foreground, never gives focus back, and keeps doing it for as long as the job reports running. In practice the desktop is unusable while an agent polls get_test_job: a Unity window takes the foreground for up to 12 seconds out of every ~17.
Environment
- Windows 11 Pro 10.0.26200
mcpforunityserver 10.1.0 (the relevant code is identical in 10.2.0), stdio transport
- Unity 6000.3.9f1, four editors open at once (four worktrees of one project)
runInBackground: 1 in Player Settings, so PlayMode tests do not need focus to progress
Steps to reproduce
- Open two or more Unity editors on Windows.
- From an MCP client, call
run_tests with mode: PlayMode against the editor that is not the lowest-PID Unity process, using a test that runs longer than 3 seconds.
- Poll
get_test_job with wait_timeout: 60 while a non-Unity window has focus.
Expected
If a nudge is needed at all, the editor running the job is focused briefly and focus then returns to the window I was using.
Actual
Four separate problems, all in the Windows path:
1. The wrong editor is focused. _focus_app passes unity_project_path only to the macOS implementation. _focus_app_windows(window_title) takes no project path and runs:
$unity = Get-Process | Where-Object {$_.MainWindowTitle -like "*Unity*"} | Select-Object -First 1
That is simply the lowest-PID process with "Unity" in its title, regardless of which editor owns the job. (It would also match any non-Unity window whose title contains "Unity".) Because the editor that owns the job never receives focus, the nudge cannot un-stall it, so reset_nudge_backoff() is never reached and the loop runs at maximum cadence.
2. Focus is never restored. _get_frontmost_app_windows does not discard the return value of GetWindowText:
[Win32]::GetWindowText($hwnd, $sb, 256) # int return value is written to stdout
$sb.ToString()
so stdout is "<length>\n<title>" and that whole string becomes the app name. The server log shows it:
utils.focus_nudge - INFO - Nudging Unity focus (interval: 10.0s, consecutive: 6, duration: 12.0s, will return to 139
<window title here>)
The restore step then looks for MainWindowTitle -eq '139\n<title>', which never matches. The PowerShell script still exits 0, so the server logs Returned focus to 139... even though nothing happened. Fix: [void][Win32]::GetWindowText(...) or | Out-Null. Restoring by the saved hwnd rather than by title would be more robust, since titles change and are not unique.
3. The stall heuristic fires on healthy tests. should_nudge treats "no last_update_unix_ms change for 3s + editor unfocused" as a stall. TestJobManager only bumps LastUpdateUnixMs on run start, test start, test finish and run finish, so any single test longer than 3 seconds counts as stalled, even when the editor is running in the background and the test is progressing normally.
4. There is no cap and no opt-out. A job that never progresses (in my case a test that hung at completed=0, total=2, then lost its current-test state across a domain reload and stayed running) is nudged indefinitely at 12s focus / 10s interval. The only environment variables are UNITY_MCP_NUDGE_BASE_INTERVAL_S, UNITY_MCP_NUDGE_MAX_INTERVAL_S and UNITY_MCP_NUDGE_DURATION_S, and _parse_env_float rejects values <= 0, so there is no supported way to switch it off.
Also worth noting: the nudge log line had no for <project path> suffix, so _get_unity_project_path(unity_instance) resolved to None for a stdio session with an explicitly pinned instance. On Windows that makes no difference today because of problem 1, but it will matter once the Windows path honours the project path.
Log excerpt
13:44:37 Tool 'run_tests' returned: ... job_id='5cab…', status='running', mode='PlayMode'
13:44:41 Tool 'get_test_job' called with ... 'wait_timeout': 60
13:44:44 Test job 5cab… appears stalled (unfocused Unity), attempting nudge...
13:44:44 Nudging Unity focus (interval: 1.0s, consecutive: 0, duration: 3.0s, will return to 6
13:44:47 Unity activation didn't complete - current app is 6
...
13:45:40 Nudging Unity focus (interval: 8.0s, consecutive: 3, duration: 12.0s, will return to 97
13:45:59 Nudging Unity focus (interval: 10.0s, consecutive: 4, duration: 12.0s, will return to 6
(repeats every ~17s until the client exited play mode at 13:50:02)
Suggested fixes
- Windows: resolve the target editor by project path, for example by matching the
-projectpath argument in Win32_Process.CommandLine, the same way the macOS path uses _find_unity_pid_by_project_path. If no match is found, do not nudge rather than focusing an arbitrary editor.
- Windows: suppress the
GetWindowText return value, and restore focus by hwnd.
- Add an explicit opt-out, for example
UNITY_MCP_DISABLE_FOCUS_NUDGE=1.
- Cap consecutive nudges that produce no progress (three or four), then stop and surface
stuck_suspected to the client instead.
- Skip the nudge when the editor reports
Application.runInBackground == true, or have the editor send a heartbeat during long tests so a slow test is not mistaken for a stalled one.
Workaround
Set both UNITY_MCP_NUDGE_BASE_INTERVAL_S and UNITY_MCP_NUDGE_MAX_INTERVAL_S to 1000000000 in the environment the server is launched from. _last_nudge_time starts at 0.0 and time.monotonic() never reaches 1e9, so the rate limiter skips every nudge.
Summary
On Windows with more than one Unity editor open, the test-job focus nudge (
utils/focus_nudge.py, called fromservices/tools/run_tests.py) repeatedly pulls an unrelated Unity editor to the foreground, never gives focus back, and keeps doing it for as long as the job reportsrunning. In practice the desktop is unusable while an agent pollsget_test_job: a Unity window takes the foreground for up to 12 seconds out of every ~17.Environment
mcpforunityserver10.1.0 (the relevant code is identical in 10.2.0), stdio transportrunInBackground: 1in Player Settings, so PlayMode tests do not need focus to progressSteps to reproduce
run_testswithmode: PlayModeagainst the editor that is not the lowest-PID Unity process, using a test that runs longer than 3 seconds.get_test_jobwithwait_timeout: 60while a non-Unity window has focus.Expected
If a nudge is needed at all, the editor running the job is focused briefly and focus then returns to the window I was using.
Actual
Four separate problems, all in the Windows path:
1. The wrong editor is focused.
_focus_apppassesunity_project_pathonly to the macOS implementation._focus_app_windows(window_title)takes no project path and runs:That is simply the lowest-PID process with "Unity" in its title, regardless of which editor owns the job. (It would also match any non-Unity window whose title contains "Unity".) Because the editor that owns the job never receives focus, the nudge cannot un-stall it, so
reset_nudge_backoff()is never reached and the loop runs at maximum cadence.2. Focus is never restored.
_get_frontmost_app_windowsdoes not discard the return value ofGetWindowText:so stdout is
"<length>\n<title>"and that whole string becomes the app name. The server log shows it:The restore step then looks for
MainWindowTitle -eq '139\n<title>', which never matches. The PowerShell script still exits 0, so the server logsReturned focus to 139...even though nothing happened. Fix:[void][Win32]::GetWindowText(...)or| Out-Null. Restoring by the savedhwndrather than by title would be more robust, since titles change and are not unique.3. The stall heuristic fires on healthy tests.
should_nudgetreats "nolast_update_unix_mschange for 3s + editor unfocused" as a stall.TestJobManageronly bumpsLastUpdateUnixMson run start, test start, test finish and run finish, so any single test longer than 3 seconds counts as stalled, even when the editor is running in the background and the test is progressing normally.4. There is no cap and no opt-out. A job that never progresses (in my case a test that hung at
completed=0, total=2, then lost its current-test state across a domain reload and stayedrunning) is nudged indefinitely at 12s focus / 10s interval. The only environment variables areUNITY_MCP_NUDGE_BASE_INTERVAL_S,UNITY_MCP_NUDGE_MAX_INTERVAL_SandUNITY_MCP_NUDGE_DURATION_S, and_parse_env_floatrejects values<= 0, so there is no supported way to switch it off.Also worth noting: the nudge log line had no
for <project path>suffix, so_get_unity_project_path(unity_instance)resolved toNonefor a stdio session with an explicitly pinned instance. On Windows that makes no difference today because of problem 1, but it will matter once the Windows path honours the project path.Log excerpt
Suggested fixes
-projectpathargument inWin32_Process.CommandLine, the same way the macOS path uses_find_unity_pid_by_project_path. If no match is found, do not nudge rather than focusing an arbitrary editor.GetWindowTextreturn value, and restore focus byhwnd.UNITY_MCP_DISABLE_FOCUS_NUDGE=1.stuck_suspectedto the client instead.Application.runInBackground == true, or have the editor send a heartbeat during long tests so a slow test is not mistaken for a stalled one.Workaround
Set both
UNITY_MCP_NUDGE_BASE_INTERVAL_SandUNITY_MCP_NUDGE_MAX_INTERVAL_Sto1000000000in the environment the server is launched from._last_nudge_timestarts at0.0andtime.monotonic()never reaches 1e9, so the rate limiter skips every nudge.