Repository navigation
Fix four unsatisfiable tests in the sidebar git suites - #8723
Conversation
📝 WalkthroughWalkthroughTests now use suspending condition polling for asynchronous panel updates. Git index fixtures accept controlled object IDs, and sidebar tests cover app-delegate workspace resolution and staged index signature changes. ChangesTab manager async test
Sidebar Git metadata tests
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 24 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (24 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
172043a to
00ec71c
Compare
00ec71c to
198d18e
Compare
Greptile SummaryThis PR fixes four previously unsatisfiable tests in the sidebar git suites. The changes are entirely test-scoped — no production
Confidence Score: 5/5All changes are confined to test files with no production source modifications; the fixes correctly address the root causes described in the PR. Every change is in cmuxTests/ with zero production-code modifications. The async helper is well-reasoned and documented. The git-index test fixes are mechanically correct — staging a real object-ID change is exactly what moves the content signature. The socket-routing wiring matches the existing pattern for other scoped-command tests. No new flakiness vectors are introduced. Files Needing Attention: No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant Test as async @MainActor test
participant Helper as waitForConditionSuspending
participant Clock as ContinuousClock
participant Product as MainActor product code
Note over Test,Product: New suspending path (this PR)
Test->>Helper: "await waitForConditionSuspending { condition }"
Helper->>Helper: "condition() == false"
Helper->>Clock: try await clock.sleep(pollInterval)
Clock-->>Helper: suspends yields main actor
Note over Product: MainActor.run apply lands here
Product-->>Helper: state updated
Helper->>Helper: "condition() == true"
Helper-->>Test: return true
Note over Test,Product: Old blocking path (before this PR)
Test->>Helper: "waitForCondition { condition } blocking"
Helper->>Helper: spin nested run loop via XCTWaiter
Note over Helper: libdispatch won't re-enter main-queue drain
Note over Product: MainActor.run apply blocked never runs
Helper->>Helper: deadline exceeded
Helper-->>Test: XCTFail always times out
Reviews (5): Last reviewed commit: "test: use a monotonic clock in the suspe..." | Re-trigger Greptile |
198d18e to
6e4f808
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@cmuxTests/WorkspacePullRequestSidebarTests.swift`:
- Around line 427-430: Update the staged-index fixture generation around the
object ID and index-writing helpers to produce Git-valid indexes: derive blob
object IDs from the actual tracked fixture contents and append the correct
index-body checksum instead of zero-filled trailer bytes. Apply the same
correction to the related fixture paths, using local git add with forced index
v4 where appropriate or equivalent checksum generation.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: fee36609-7a43-419e-804b-6d3cbd975eae
📒 Files selected for processing (2)
cmuxTests/TabManagerUnitTests.swiftcmuxTests/WorkspacePullRequestSidebarTests.swift
6e4f808 to
7710778
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
cmuxTests/WorkspacePullRequestSidebarTests.swift (1)
427-430: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftThe Git index fixtures remain invalid.
The new object-ID changes do not make these fixtures represent real staging: the IDs are arbitrary, and the trailer is still not the SHA-1 of the preceding index body.
cmuxTests/WorkspacePullRequestSidebarTests.swift#L427-L430: generate content-derived blob IDs and the correct index checksum.cmuxTests/WorkspacePullRequestSidebarTests.swift#L1167-L1177: use a real staged-content fixture rather than an unchanged file with an arbitrary object ID.cmuxTests/WorkspacePullRequestSidebarTests.swift#L1497-L1521: apply the same valid-index generation to the empty-index transition.🤖 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 `@cmuxTests/WorkspacePullRequestSidebarTests.swift` around lines 427 - 430, The Git index fixtures in cmuxTests/WorkspacePullRequestSidebarTests.swift must use valid content-derived blob IDs and SHA-1 trailers: update lines 427-430 to hash each staged blob’s actual content and compute the checksum over the complete preceding index body; update lines 1167-1177 to stage genuinely changed content with its matching blob ID; and apply the same valid index generation and checksum logic to the empty-index transition at lines 1497-1521.
🤖 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 `@cmuxTests/TabManagerUnitTests.swift`:
- Around line 87-97: Update the test polling helper containing the deadline loop
to use a monotonic ContinuousClock (or injected test clock) for timeout
measurement instead of Date(). Replace fixed nanosecond Task.sleep polling with
the clock’s monotonic sleep or cooperative yielding, while preserving the
existing condition check and XCTFail timeout behavior.
---
Duplicate comments:
In `@cmuxTests/WorkspacePullRequestSidebarTests.swift`:
- Around line 427-430: The Git index fixtures in
cmuxTests/WorkspacePullRequestSidebarTests.swift must use valid content-derived
blob IDs and SHA-1 trailers: update lines 427-430 to hash each staged blob’s
actual content and compute the checksum over the complete preceding index body;
update lines 1167-1177 to stage genuinely changed content with its matching blob
ID; and apply the same valid index generation and checksum logic to the
empty-index transition at lines 1497-1521.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: c10cfdd8-91d8-4cd9-9875-51925ea53ea9
📒 Files selected for processing (2)
cmuxTests/TabManagerUnitTests.swiftcmuxTests/WorkspacePullRequestSidebarTests.swift
Two of them describe a git index that git cannot produce. The index trailer is the SHA-1 of the index content, so rewriting only the trailing checksum while leaving the entry table byte-identical is not a state a real repository reaches. The product reads that shape deliberately: the index content signature covers the entry count, path, mode and object id but not the trailer, so an index whose content signature is unchanged is rebaselined as clean, which is exactly what testCleanIndexSignatureRebaselinesWhenIndexRewriteKeepsTrackedContentClean pins. The v4 and empty-index tests asserted the opposite for the same input, so one of the two had to fail. They now stage a real change -- a new object id, and an added entry -- whose stat still matches the worktree, so the dirty verdict comes from the content signature the way it does for a real staged change. The predicates are unchanged; the empty-index test's scenario and message move from a staged delete to a staged add, because staging the first entry out of an empty index is what actually moves the content signature. writeGitIndexVersion4 gains the objectIDBytes parameter that writeGitIndexVersion2EntryFromStat already had. It defaults to the zero id, so the test call sites that do not stage a change need no edit; the convenience overload threads it through. writeGitIndexVersion3SkipWorktreeEntry still hard-codes a zero object id; it has no need to express a staged change. testDisablingGitWatchClearsCachedPullRequestBadgesWhenPullRequestsAreShownByDefault sent a scoped report_git_branch to a TabManager that only TerminalController knew about. That path resolves its workspace through AppDelegate's main-window contexts, so the report was dropped and the seeded badge survived. The test now registers a windowless context like the other socket-routing tests, and asserts the workspace is resolvable before sending, so a future wiring break reports there instead of at the far assertion. testSameDirectoryInitialGitMetadataProbesShareOneSnapshotRead waited with a helper that blocks the main thread while pumping the main queue. That works in a synchronous test, but this body is async: it runs as a main-actor job, inside a main-queue drain that libdispatch will not re-enter, so the nested run loop ran neither the helper's own poll hops nor the snapshot's MainActor.run apply. The wait could only expire. It now awaits a suspending sibling helper with the same timeout and interval. The suspending helper propagates cancellation rather than swallowing it, so a cancelled test unwinds instead of spinning the condition until its deadline.
7710778 to
e1de81a
Compare
|
Two of the three findings are already addressed on the current head; one I am deliberately not taking now.
|
Four tests in the sidebar git suites could never pass. On current
mainthey fail for threeunrelated reasons, and this change fixes all four.
Two tests describe a file git cannot produce
testGitIndexVersionFourRefreshTracksIndexSignatureChangesandtestEmptyGitIndexRefreshTracksIndexSignatureChangeseach wrote an index twice and changed only thetrailing 20-byte checksum, leaving the entry table byte-identical, then asserted the sidebar went
dirty.
The index trailer is the SHA-1 of the index content, so identical content cannot carry two different
trailers. No repository reaches that state. The product reads it deliberately:
gitIndexContentSignaturehashes the entry count and each entry's path, mode and object id, and neverthe trailer, so both writes produce the same content signature. The apply then takes the rebaseline
branch in
SidebarGitMetadataService+Probe.swift— index signature changed, content signatureunchanged, so the stored clean signature moves forward and the panel stays clean. That is what
testCleanIndexSignatureRebaselinesWhenIndexRewriteKeepsTrackedContentCleanpins, immediately afterthe v4 test in the same file, and it passes. Two tests asked for opposite outcomes from the same input, so one had to fail, and the
rebaseline test is the correct one: it describes a stash-like rewrite.
Both now stage a real change — a new object id for the v4 index, an added entry for the empty one —
with a stat that still matches the worktree, so the stat scan stays clean and the dirty verdict has to
come from the content signature, which is the path a real staged change takes. The predicates are
unchanged. The empty-index test's scenario and message do change, from a staged delete to a staged
add, because staging the first entry out of an empty index is what actually moves the content
signature; its name still says "TracksIndexSignatureChanges", which remains true.
writeGitIndexVersion4gains theobjectIDBytesparameter thatwriteGitIndexVersion2EntryFromStatalready had. It defaults to the zero id, so the callers that donot stage a change need no edit.
writeGitIndexVersion3SkipWorktreeEntrystill hard-codes a zero object id —it has no need to express a staged change.
A scoped branch report went to a manager nothing could resolve
testDisablingGitWatchClearsCachedPullRequestBadgesWhenPullRequestsAreShownByDefaultseeded a badge,then sent
report_git_branchwith both--taband--panel. That scoped path resolves its workspacethrough AppDelegate's main-window contexts, not through TerminalController's active manager, so a
manager only the controller knew about was invisible to it: the handler returned before reaching the
clear and the seeded entry survived. A nearby
report_prtest passes because the PR path resolves throughcontrolSidebarTabForMutation, which checks the controller's own manager first andthen falls back to AppDelegate.
The test now registers a windowless context the way the other socket-routing tests do, and asserts the
workspace is resolvable before sending, so a future wiring break reports at that line instead of at
the far assertion. This is test wiring, not a product defect:
tabManagerFor(tabId:)resolves everyreal window, so no user-visible path depends on the difference.
An async test waited in a way that could only expire
testSameDirectoryInitialGitMetadataProbesShareOneSnapshotReadused a helper that blocks the thread onXCTWaiterwhile polling throughDispatchQueue.main. That works in a synchronous test, whose bodyruns from an ordinary run-loop callback. This body is
asyncon a@MainActorclass, so it runs as amain-actor job — already inside a main-queue drain, which libdispatch will not re-enter. The nested
run loop therefore ran neither the helper's own poll hops nor the snapshot's
MainActor.runapply, andthe wait expired every time while the work it waited for landed immediately after the body suspended
again.
I confirmed the mechanism with a standalone binary rather than reasoning about it: a nested
CFRunLoopRunInModeentered from a main-actor async job does not runDispatchQueue.mainblocks,while the same nested loop entered from a run-loop callback does.
The test now awaits a suspending sibling helper with the same 3s budget and 0.05s interval. It is a
separate function rather than an inline loop so the next async test does not reintroduce the blocking
form, and its doc comment says why. It was the only async test in this target using the blocking
helper.
When reading a failure log in these suites, note that a timeout from the file-private helper is
reported twice, once for the
XCTAssertTrueand once for the helper's ownXCTFail, because itsline: UInt = #lineresolves at the call site. Consecutive line numbers are one failure, not two.Test plan
Measured on
4253cc2884, one GUI test host at a time, same checkout and DerivedData for both arms:mainunchangedNo host restarts in either arm, and
CLICodexHookTimeoutRegressionTestsreportsTest run with 8 tests in 1 suite passedin both.The four tests this fixes are
testGitIndexVersionFourRefreshTracksIndexSignatureChanges,testEmptyGitIndexRefreshTracksIndexSignatureChanges,testDisablingGitWatchClearsCachedPullRequestBadgesWhenPullRequestsAreShownByDefaultandtestSameDirectoryInitialGitMetadataProbesShareOneSnapshotRead.Pull-request CI on this repo runs review bots and security scanners, not the
test suite, so the arms above are the only test evidence this carries.
Still red on this branch, and not addressed here
testFocusedPanelTitleRefreshesAutoWorkspaceTitleInSplitWorkspace,testRemoteSplitSkipsInitialGitMetadataProbeandtestUnrelatedDefaultsChangeDoesNotRestartGitMetadataRefreshes— all three pass an explicittimeout:towaitForCondition, which resolves to a blocking helper that starves the main-actorwork they wait on. Stop a blocking wait helper from shadowing the pumping ones #8725 fixes them.
testPullRequestRefreshRepositoryDiscoveryDoesNotBlockMainRunLoop— its stub watches agit remote -vsubprocess the product stopped spawning in Fix git index.lock polling in sidebar metadata watcher #2797, so its counter cannot leave zero. Give the PR refresh run-loop test something real to observe #8724 fixesit.
testCloseWorkspaceIgnoresWorkspaceNotOwnedByManager— a real product bug inTabManager.closeWorkspace. Close only the workspaces a tab manager actually owns #8753 fixes it.Summary by CodeRabbit