Summary
CompileUseCase and RunTestsUseCase still have plain (non-ConfigureAwait(false)) awaits that violate the invariant documented on UnityCliLoopTool.ExecuteAsync (added in #1685): every await in a tool's execution path must avoid capturing Unity's SynchronizationContext, because a continuation posted to it is never drained while Play Mode is paused.
This was flagged by CodeRabbit during review of #1685 and confirmed as a real (but out of scope for that PR) latent gap.
Affected locations
Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:106 — await WaitForPlayModeExitAsync(ct)
Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:154 — await _executeCompilationAsync(request, ct)
Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:170 — await TimerDelay.Wait(POLL_INTERVAL_MS, ct) (inside WaitForPlayModeExitAsync's poll loop)
Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:95 — await _executionService.ExecutePlayModeTestAsync(filter, ct)
Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:99 — await _executionService.ExecuteEditModeTestAsync(filter, ct)
Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:102 — await _waitForTestRunnerCleanupAsync(ct)
Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:154 — await TimerDelay.Wait(TestRunnerCleanupFallbackDelayMilliseconds, ct)
(ExecuteDynamicCodeUseCase was also audited during #1685's review and is not affected — every await there already uses ConfigureAwait(false).)
Mechanism
Same second-layer bug class fixed for simulate-mouse-ui/simulate-keyboard/simulate-mouse-input in #1685: if one of these awaits is still suspended (not yet complete) when Play Mode pauses, and it later completes off the captured UnitySynchronizationContext, the resumed continuation is posted back to that context. That context's pump does not run while EditorApplication.isPaused is true, so the tool call hangs forever instead of returning.
run-tests --test-mode PlayMode is the most plausible trigger, since it genuinely executes inside Play Mode where a pause point could fire while a test is running. compile's exposure depends on whether StopPlayMode() itself resolves the pause as a side effect before the poll loop's await would otherwise hang — this needs to be measured, not assumed.
Why a mechanical fix is not safe here
Unlike the simulate-mouse-ui/simulate-keyboard wait loops (which pair every ConfigureAwait(false) with an explicit MainThreadSwitcher/InputSystemUpdateHelper.SwitchToMainThreadIfNeeded call before touching Unity APIs again), these awaits currently rely on the implicit context-capture to resume on the main thread. Code immediately after them reads main-thread-only Unity APIs (e.g. EditorApplication.isPlaying at CompileUseCase.cs:167,174).
Blindly adding ConfigureAwait(false) here would remove that implicit resume-on-main-thread guarantee, trading a rare pause-triggered hang for a frequent threadpool-touches-Unity-API bug. Do not "just add ConfigureAwait(false)" to these awaits.
Candidate fixes
- Port the
MouseUiEditorFrameWaiter pattern: ConfigureAwait(false) paired with an explicit main-thread switch-back before the next Unity API read.
- Or, for
run-tests specifically, reject execution via a preflight check if Play Mode is paused when the tool starts (may be the more appropriate design for test execution — this is Unity's highest-risk area per the Unity Freeze Prevention guidance in CLAUDE.md, so any behavioral change here needs to be validated very carefully, ideally starting as a non-EditMode/pure-unit-test-covered refactor before any Play Mode E2E repro is attempted).
Verification requirements before closing
- A real, non-mocked repro proving the current hang exists (or does not, if
compile's StopPlayMode() genuinely self-resolves the pause first) — do not assume the mechanism transfers 1:1 from the mouse-ui case without measuring it.
- After the fix, a real repro showing the tool call now returns promptly instead of hanging when a pause point fires while the awaited operation is in flight.
- Full
run-tests suite green, with special attention to the Unity Freeze Prevention guardrails in CLAUDE.md (no infinite waits, no blocking the main thread, no risky EditMode test patterns).
Context
Found during E2E verification for #1685 (source-based pause points, uloop-wait-for-pause-point skill).
Summary
CompileUseCaseandRunTestsUseCasestill have plain (non-ConfigureAwait(false)) awaits that violate the invariant documented onUnityCliLoopTool.ExecuteAsync(added in #1685): every await in a tool's execution path must avoid capturing Unity'sSynchronizationContext, because a continuation posted to it is never drained while Play Mode is paused.This was flagged by CodeRabbit during review of #1685 and confirmed as a real (but out of scope for that PR) latent gap.
Affected locations
Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:106—await WaitForPlayModeExitAsync(ct)Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:154—await _executeCompilationAsync(request, ct)Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:170—await TimerDelay.Wait(POLL_INTERVAL_MS, ct)(insideWaitForPlayModeExitAsync's poll loop)Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:95—await _executionService.ExecutePlayModeTestAsync(filter, ct)Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:99—await _executionService.ExecuteEditModeTestAsync(filter, ct)Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:102—await _waitForTestRunnerCleanupAsync(ct)Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:154—await TimerDelay.Wait(TestRunnerCleanupFallbackDelayMilliseconds, ct)(
ExecuteDynamicCodeUseCasewas also audited during #1685's review and is not affected — every await there already usesConfigureAwait(false).)Mechanism
Same second-layer bug class fixed for
simulate-mouse-ui/simulate-keyboard/simulate-mouse-inputin #1685: if one of these awaits is still suspended (not yet complete) when Play Mode pauses, and it later completes off the capturedUnitySynchronizationContext, the resumed continuation is posted back to that context. That context's pump does not run whileEditorApplication.isPausedis true, so the tool call hangs forever instead of returning.run-tests --test-mode PlayModeis the most plausible trigger, since it genuinely executes inside Play Mode where a pause point could fire while a test is running.compile's exposure depends on whetherStopPlayMode()itself resolves the pause as a side effect before the poll loop's await would otherwise hang — this needs to be measured, not assumed.Why a mechanical fix is not safe here
Unlike the
simulate-mouse-ui/simulate-keyboardwait loops (which pair everyConfigureAwait(false)with an explicitMainThreadSwitcher/InputSystemUpdateHelper.SwitchToMainThreadIfNeededcall before touching Unity APIs again), these awaits currently rely on the implicit context-capture to resume on the main thread. Code immediately after them reads main-thread-only Unity APIs (e.g.EditorApplication.isPlayingatCompileUseCase.cs:167,174).Blindly adding
ConfigureAwait(false)here would remove that implicit resume-on-main-thread guarantee, trading a rare pause-triggered hang for a frequent threadpool-touches-Unity-API bug. Do not "just add ConfigureAwait(false)" to these awaits.Candidate fixes
MouseUiEditorFrameWaiterpattern:ConfigureAwait(false)paired with an explicit main-thread switch-back before the next Unity API read.run-testsspecifically, reject execution via a preflight check if Play Mode is paused when the tool starts (may be the more appropriate design for test execution — this is Unity's highest-risk area per the Unity Freeze Prevention guidance inCLAUDE.md, so any behavioral change here needs to be validated very carefully, ideally starting as a non-EditMode/pure-unit-test-covered refactor before any Play Mode E2E repro is attempted).Verification requirements before closing
compile'sStopPlayMode()genuinely self-resolves the pause first) — do not assume the mechanism transfers 1:1 from the mouse-ui case without measuring it.run-testssuite green, with special attention to the Unity Freeze Prevention guardrails inCLAUDE.md(no infinite waits, no blocking the main thread, no risky EditMode test patterns).Context
Found during E2E verification for #1685 (source-based pause points,
uloop-wait-for-pause-pointskill).