Skip to content

fix: Detect pause-point interruption during Press/KeyDown observation wait#1919

Merged
hatayama merged 4 commits into
feature/round7-pause-point-improvementsfrom
fix/key-state-pause-interruption
Jul 21, 2026
Merged

fix: Detect pause-point interruption during Press/KeyDown observation wait#1919
hatayama merged 4 commits into
feature/round7-pause-point-improvementsfrom
fix/key-state-pause-interruption

Conversation

@hatayama

@hatayama hatayama commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • WaitForPressLifetime/WaitForRuntimeFrames could silently absorb a genuine Unity pause into a false Completed result: real time keeps advancing while paused, so a duration/frame-count condition could become satisfied inside the same real-time window a pause started, and the original code re-checked pause state at some return points but not the TimedOut-with-duration-already-satisfied one.
  • Added EditorPauseAwaiter (event-based, races EditorApplication.pauseStateChanged against the existing frame/timeout wait) so a pause is observed the instant it happens instead of only between completed waits.
  • Extracted the post-wait decision into PressLifetimeIterationResolver so the pause-priority rule is unit-testable independent of the Editor player loop.
  • Added a regression harness scene/script (Assets/RegressionHarness/KeyStateAfterPauseInterruption/, scripts/regression-harness-key-state-after-pause-interruption.sh) that arms a pause-point on a key-consuming line before Press starts, so the hit fires naturally from game code, and verifies the interruption is reported before the requested duration elapses.

Verification

  • Live deterministic reproduction is not possible from the CLI. The swallow only triggers when EditorApplication.update ticks stall for several seconds while paused (main-thread continuation starvation) — an environment condition, not something a CLI command sequence can reliably orchestrate. Attempting to synthesize the stall (e.g. racing a second CLI command against Press) was tried and rejected: any concurrent CLI command is itself subject to the same starvation, making that approach structurally unstable and non-deterministic.
  • Red was confirmed at the unit level instead. With the isPausedFallback check in PressLifetimeIterationResolver.ResolvePostWaitOutcome temporarily reverted to pre-fix behavior, PressLifetimeIterationResolverTests.ResolvePostWaitOutcome_WhenTimedOutWithDurationSatisfiedAndPausedFallback_ReturnsPaused fails: Expected: Paused / But was: Completed. With the fix restored, the same test (and the other 5 in the file) pass: 6/6.
  • Full suite after the fix: PlayMode 93/93 passed; EditMode 2268/2271 passed (7 skipped). The 3 EditMode failures are pre-existing and unrelated to this change — confirmed by stashing all files in this PR and re-running the same 3 tests against the unmodified feature/round7-pause-point-improvements HEAD, which fail identically (2 dispatcher-version-pin drift assertions in CliSetupApplicationServiceTests, 1 DescriptionAttribute exposure assertion in FirstPartyToolSchemaMetadataTests — none touch files this PR changes).
  • Live E2E with the correct procedure (arm the pause-point on the key-consuming line before Press, not via a concurrent CLI command) passes with the fix restored: simulate-keyboard Press --key Space --duration 5 returns InterruptedByPausePoint: true immediately (0s elapsed, well under the 5s requested).
  • uloop compile: 0 errors, 0 warnings.

Known related observation (out of scope for this PR)

The ~5s+ delay in enable-pause-point's reported EnabledAtUtc sometimes observed during heavy concurrent CLI usage is a symptom of main-thread continuation-queue starvation, not command serialization — there is no locking in the IPC command dispatch path. This is the same root cause that makes the swallow bug in this PR environment-dependent. Fixing the starvation itself is out of scope here.

Test plan

  • uloop compile — 0 errors, 0 warnings
  • uloop run-tests --test-mode EditMode --filter-value PressLifetimeIterationResolverTests — 6/6 passed (and confirmed failing pre-fix)
  • uloop run-tests --test-mode PlayMode — 93/93 passed
  • uloop run-tests --test-mode EditMode — 2268/2271 passed, 3 pre-existing unrelated failures confirmed via baseline stash comparison
  • scripts/regression-harness-key-state-after-pause-interruption.sh — PASS (interrupted after 0s, under the 5s requested duration)

hatayama added 2 commits July 21, 2026 19:26
Round-7 feedback traps kept getting rediscovered because each round's
repro lived only in a throwaway memo. Establish a permanent, re-runnable
scene+script convention under Assets/RegressionHarness/<TrapName>/ (docs
in docs/regression-harness.md) and add the first scene for the pause-point
key-state divergence investigation (KeyStateAfterPauseInterruption).
WaitForPressLifetime and WaitForRuntimeFrames could silently absorb a
genuine Unity pause into a false "Completed" result: real time keeps
advancing while paused, so a duration/frame-count condition could become
satisfied inside the same window a pause started, with no re-check of
pause state at that exact return point.

Add EditorPauseAwaiter (event-based, races EditorApplication.pauseStateChanged
against the existing frame/timeout wait) and extract the post-wait decision
into PressLifetimeIterationResolver so the pause-priority rule is unit
testable independent of the Editor player loop. Add a regression harness
scene/script that arms a pause-point on a key-consuming line before Press
starts, verifying the interruption is reported before the requested
duration elapses.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@hatayama, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c009fd2d-9490-4309-bb1f-4207c1be100d

📥 Commits

Reviewing files that changed from the base of the PR and between eef1fe7 and 5e8b051.

📒 Files selected for processing (1)
  • Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputSystemRuntimeFrameWaiter.cs
📝 Walkthrough

Walkthrough

The change adds pause-aware frame and press-lifetime handling, centralizes outcome resolution, adds tests, and introduces a manual Unity regression harness for Space-key interruption after a pause-point.

Changes

Pause interruption handling

Layer / File(s) Summary
Press lifetime decision contract
Packages/src/Editor/FirstPartyTools/Common/InputSystem/PressLifetimeIterationResolver.cs, Assets/Tests/Editor/PressLifetimeIterationResolverTests.cs
Defines pause-prioritized iteration decisions and tests pause, timeout, completion, and fallback outcomes.
Editor pause race integration
Packages/src/Editor/FirstPartyTools/Common/InputSystem/EditorPauseAwaiter.cs, Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputSystemRuntimeFrameWaiter.cs
Races frame and timeout waits against Editor pause events and routes press-lifetime results through the resolver.
Key interruption regression harness
Assets/RegressionHarness/KeyStateAfterPauseInterruption/SpaceHoldPoller.cs, scripts/regression-harness-key-state-after-pause-interruption.sh, docs/regression-harness.md, AGENTS.md
Adds a Space polling scene, manual harness workflow documentation, repository guidance, and a script that validates interrupted key simulation results and elapsed duration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant simulate-keyboard
  participant InputSystemRuntimeFrameWaiter
  participant EditorPauseAwaiter
  participant UnityEditor
  simulate-keyboard->>InputSystemRuntimeFrameWaiter: start press-lifetime wait
  InputSystemRuntimeFrameWaiter->>EditorPauseAwaiter: race frame/timeout against pause
  EditorPauseAwaiter->>UnityEditor: subscribe to pauseStateChanged
  UnityEditor-->>EditorPauseAwaiter: report PauseState.Paused
  EditorPauseAwaiter-->>InputSystemRuntimeFrameWaiter: return Paused
  InputSystemRuntimeFrameWaiter-->>simulate-keyboard: return InterruptedByPausePoint
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: detecting pause-point interruption during Press/KeyDown observation waits.
Description check ✅ Passed The description is detailed and directly describes the same pause-interruption fix, tests, and regression harness changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/key-state-pause-interruption

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.

EditorPauseAwaiter deliberately never reads EditorApplication.isPaused
(only subscribes to pauseStateChanged), so the comment claiming otherwise
was corrected. The harness script's wait under set -e would exit before
printing the RESULT_FILE diagnostics on a non-zero Press exit; tolerate
that and let the jq/FAIL check report it instead.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputSystemRuntimeFrameWaiter.cs (1)

42-54: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

WaitForRuntimeFrames's TimedOut branch is missing the pause fallback recheck that WaitForPressLifetime now has.

Right below, a genuine frameOutcome == Completed result gets an IsPaused() recheck (lines 56-60) before continuing, but a TimedOut result returns immediately without ever checking IsPaused(). Since WaitOneRuntimeFrameOrTimeout's wall-clock timeout keeps advancing during a real pause, this reintroduces the exact "pause absorbed into a wrong outcome" bug this PR is fixing elsewhere — just as TimedOut here instead of Completed in WaitForPressLifetime. WaitForPressLifetime now guards against this via PressLifetimeIterationResolver's isPausedFallback; this sibling method has no equivalent guard.

🐛 Proposed fix
                 if (frameOutcome == InputSimulationWaitOutcome.TimedOut)
                 {
-                    return InputSimulationWaitOutcome.TimedOut;
+                    await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
+                    if (InputSystemUpdateHelper.IsPaused())
+                    {
+                        return InputSimulationWaitOutcome.Paused;
+                    }
+
+                    return InputSimulationWaitOutcome.TimedOut;
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputSystemRuntimeFrameWaiter.cs`
around lines 42 - 54, Update the TimedOut handling in WaitForRuntimeFrames to
recheck IsPaused() before returning. If the timeout occurred while paused,
return InputSimulationWaitOutcome.Paused; otherwise preserve the existing
TimedOut result, matching the pause-fallback behavior used by
WaitForPressLifetime.
🧹 Nitpick comments (1)
scripts/regression-harness-key-state-after-pause-interruption.sh (1)

66-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

set -e can swallow diagnostics on a failed simulate-keyboard run.

If the backgrounded run_uloop simulate-keyboard call (or the jq parse at lines 72-73) exits non-zero, set -e (line 2) terminates the script right there, before the FAIL log / cat "$RESULT_FILE" diagnostics at lines 76-79 run — only the cleanup trap fires. That's the exact scenario where the diagnostic output is most needed.

♻️ Proposed fix to preserve diagnostics on failure
-wait "$PRESS_PID"
+set +e
+wait "$PRESS_PID"
+PRESS_EXIT_CODE=$?
+set -e
+if [ "$PRESS_EXIT_CODE" -ne 0 ]; then
+    log "FAIL: simulate-keyboard exited with code $PRESS_EXIT_CODE"
+    cat "$RESULT_FILE"
+    exit 1
+fi
 ELAPSED_SECONDS=$(( $(date +%s) - START_SECONDS ))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/regression-harness-key-state-after-pause-interruption.sh` around
lines 66 - 79, Update the regression flow around the backgrounded run_uloop
command and the jq extractions so failures do not trigger set -e before
diagnostics are emitted. Capture each command’s exit status without aborting,
then route non-zero results through the existing FAIL log and cat "$RESULT_FILE"
path while preserving the Success and InterruptedByPausePoint validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputSystemRuntimeFrameWaiter.cs`:
- Around line 207-213: Update the wait flow around Task.WhenAny in
InputSystemRuntimeFrameWaiter so execution switches back to the main editor
thread before calling raceCancellation.Cancel(). Preserve the existing winner
check and paused outcome handling, ensuring EditorPauseAwaiter’s cancellation
callback unregisters pauseStateChanged on the editor thread.

---

Outside diff comments:
In
`@Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputSystemRuntimeFrameWaiter.cs`:
- Around line 42-54: Update the TimedOut handling in WaitForRuntimeFrames to
recheck IsPaused() before returning. If the timeout occurred while paused,
return InputSimulationWaitOutcome.Paused; otherwise preserve the existing
TimedOut result, matching the pause-fallback behavior used by
WaitForPressLifetime.

---

Nitpick comments:
In `@scripts/regression-harness-key-state-after-pause-interruption.sh`:
- Around line 66-79: Update the regression flow around the backgrounded
run_uloop command and the jq extractions so failures do not trigger set -e
before diagnostics are emitted. Capture each command’s exit status without
aborting, then route non-zero results through the existing FAIL log and cat
"$RESULT_FILE" path while preserving the Success and InterruptedByPausePoint
validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 530162f6-51c2-4269-9200-1256483b32a6

📥 Commits

Reviewing files that changed from the base of the PR and between f8f5018 and c6a0460.

⛔ Files ignored due to path filters (8)
  • Assets/RegressionHarness.meta is excluded by none and included by none
  • Assets/RegressionHarness/KeyStateAfterPauseInterruption.meta is excluded by none and included by none
  • Assets/RegressionHarness/KeyStateAfterPauseInterruption/KeyStateAfterPauseInterruption.unity is excluded by !**/*.unity and included by none
  • Assets/RegressionHarness/KeyStateAfterPauseInterruption/KeyStateAfterPauseInterruption.unity.meta is excluded by none and included by none
  • Assets/RegressionHarness/KeyStateAfterPauseInterruption/SpaceHoldPoller.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/PressLifetimeIterationResolverTests.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Common/InputSystem/EditorPauseAwaiter.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Common/InputSystem/PressLifetimeIterationResolver.cs.meta is excluded by none and included by none
📒 Files selected for processing (8)
  • AGENTS.md
  • Assets/RegressionHarness/KeyStateAfterPauseInterruption/SpaceHoldPoller.cs
  • Assets/Tests/Editor/PressLifetimeIterationResolverTests.cs
  • Packages/src/Editor/FirstPartyTools/Common/InputSystem/EditorPauseAwaiter.cs
  • Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputSystemRuntimeFrameWaiter.cs
  • Packages/src/Editor/FirstPartyTools/Common/InputSystem/PressLifetimeIterationResolver.cs
  • docs/regression-harness.md
  • scripts/regression-harness-key-state-after-pause-interruption.sh

…ead safety

WaitForRuntimeFrames's TimedOut branch had no pause recheck, unlike the
Completed branch right below it and WaitForPressLifetime's TimedOut branch
— reintroducing the same class of pause-absorbed-into-wrong-outcome bug
this PR fixes elsewhere. Also switch back to the main thread before
raceCancellation.Cancel(), since EditorPauseAwaiter's cancellation callback
unsubscribes EditorApplication.pauseStateChanged (main-thread-only) and the
preceding ConfigureAwait(false) may have left the continuation off-thread.
@hatayama
hatayama merged commit df3c171 into feature/round7-pause-point-improvements Jul 21, 2026
2 checks passed
@hatayama
hatayama deleted the fix/key-state-pause-interruption branch July 21, 2026 12:04
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.

1 participant