Skip to content

Repair the remaining app-host failures and the shard-2 host relaunches - #13759

Merged
teamleaderleo merged 1 commit into
manaflow-ai:fix/app-host-greenfrom
teamleaderleo:fix/app-host-shard125
Sep 23, 2026
Merged

teamleaderleo merged 1 commit into
manaflow-ai:fix/app-host-greenfrom
teamleaderleo:fix/app-host-shard125

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes the app-host failures in shards 1, 2, 4, 5 and 6 of run 35743588302 on fix/app-host-green (head 5a92d7f882), plus the four XCTest-host relaunches in shard 2. Other agents own the remaining failures on that run.

Most of these share one cause. The app-host test process runs headless under xcodebuild test and is not the active app, so AppKit and WebKit withhold state that a test written on a developer's Mac takes for granted: NSApp.keyWindow is nil, a programmatic makeKeyAndOrderFront never makes a window key, NSWindow.occlusionState never carries .visible, and behind XCTest's shielding window WebKit suspends requestAnimationFrame entirely. Shard 4's own log prints the giveaway: windowIsKey: true, appIsActive: false. Four more are stale fixtures that encode a widget tree or a snapshot the product has since changed, and two are real product regressions.

No assertion is weakened anywhere in this PR. Several get stronger, and each is noted below.

Reconciled with the base branch (rebased onto e160e82af3a)

fix/app-host-green moved while this PR was open, and 87a70165b5b (test: activate the app host before waiting for terminal focus) landed a different remedy for the same key-window cause in the same helper, AppDelegate.focusTerminalForTesting. It activates the app host (NSApp.activate(ignoringOtherApps:)) and widens the readiness pump to 10 s, and it names exactly the three tests this PR's own key-window fix covered.

Dropped from this PR: MainWindowKeyStatusPin, and with it every change to cmuxTests/AppDelegateMainWindowTestingSupport.swift — that file is now untouched by this branch. Two reasons, beyond the base's approach already being on the shared branch:

  • Activating the host makes isKeyWindow actually true, so the product's focus gates run on real state. The pin forced the answer through a swizzle of NSWindow.isKeyWindow, which leaves everything else the window reports (NSApp.keyWindow, first-responder promotion, key/main notifications) still telling the un-activated truth.
  • The pin's swizzle is installed once and never removed, process-wide, for the benefit of one helper — a poor neighbour to the ~hundreds of other cases sharing that app host.

Keeping both would have meant two mechanisms doing one job, which is how a fixed test becomes a flaky one. The shard-1 and shard-2 focus entries below are therefore no longer this PR's fixes; they are 87a70165b5b's, and are kept here only as the record of what was diagnosed.

a682c55a75a (give standalone terminal fixtures a live portal authority) also lands on the same root cause, but in TerminalNotificationDirectInteractionTests, on three shard-4 tests this PR never claimed. It composes with this branch's GhosttySurfaceOverlayTests edits in the same file with no overlap; git merged them cleanly and both are kept.

Everything else in this PR — the four shard-2 host relaunches, the two product regressions, and the stale-fixture and test-bug repairs in shards 2, 4, 5 and 6 — is unaffected and stands as written.

The 4 test-host relaunches in shard 2

Shard 2 launched the XCTest host 5 times — 1 initial plus 4 relaunches (SocketControlServer: Listening appears 5 times; Restarting after unexpected exit, crash, or test timeout 4 times, and its 5th occurrence in the log is the shard's own incomplete app-host test run: verdict quoting that message). Per #13178/#13421 a relaunch is itself a shard failure, so this mattered as much as the assertions.

Mapping each relaunch to the test that was running and never reported a result gives four tests, all in MarkdownPanelTests, each hanging ~5m10s and then killed by XCTest's execution-time allowance:

relaunch test running started host killed
1 testMarkdownRenderBlocksRemoteImagesUntilUserAction 17:13:10 17:18:22
2 testMarkdownRenderHandlesLocalImageSources 17:18:24 17:23:32
3 testMarkdownRenderKeepsVisibleHeadingPositionAfterContentUpdate 17:23:34 17:28:44
4 testMarkdownRenderLoadsSafeDataImage 17:28:46 17:33:54

There is no bystander here: all four are the same gate, and each hung again at the top of a fresh host. cd9f34ff1b rewrote the private helper renderMarkdown(_:in:) to await two animation frames inside callAsyncJavaScript:

window.__cmuxRenderMarkdown(markdown);
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));

App-host tests run behind XCTest's shielding window (Accessibility: Not vending elements because elementWindow(0) is lower than shield(2001)), where WebKit suspends the rendering update and animation-frame callbacks never fire. This repo already documents that, at cmuxTests/BrowserViewportRuntimeTests.swift:404-408. The promise never settles, WebKit never posts the reply, the continuation is never resumed, and there is nothing to cancel.

The correlation is exact: renderMarkdown is the first WebKit interaction after the shell load in those four tests and in no other, and testMarkdownRenderDeniesLocalImageWhenMarkdownPathIsMissing — the one render-family test that does not call it — passed in 0.083 s.

Worth recording, since it is the obvious wrong answer: the Could not signal service com.apple.WebKit.WebContent: 113 lines in the log are not the cause. Those 25 lines cluster at 17:13 and from 17:34 on, with none between 17:18:24 and 17:33:54 — the window holding three of the four hangs. Every fulfillment(of: [loaded], timeout: 5) succeeded, so WebKit loaded and parsed the 268 KB shell in all four cases, and the webContentProcessDidTerminate bursts come from recovery tests driving the coordinator's state machine directly, all of which passed.

Fix, in three parts.

Fail fast. Every WebKit call in MarkdownPanelTests.swift now goes through awaitingWebKitReply(_:timeout:operation:). The awaited value is still the real completion signal and returns the instant the reply arrives; the deadline only decides how a missing reply is reported — an explicit XCTFail naming the operation. The shell-load wait likewise reports MarkdownShellNeverLoaded instead of walking into the next WebKit call. One loud failure costs one test; a hung host costs the shard.

Remove the gate. renderMarkdown now waits on the viewer's own render contract. window.__cmuxRenderMarkdown parses, replaces #content, rewrites image sources and applies the scroll restore before it returns — exactly what MarkdownWebRenderer.Coordinator.pushMarkdown waits for before reporting onMarkdownRendered. No animation frame is involved.

Fix the product bug the rAF await was papering over. restoreMarkdownScrollState re-applied the captured position on its next two animation frames unconditionally, so a scroll that landed in that window — the reader's, a find match, an anchor jump — was yanked back, and a new content update never cancelled the previous one's queued frames. The restore now records the position it wrote and stands down on a scroll event that leaves the viewer somewhere else, while a reflow (which fires no scroll event) still gets the correction the frames exist for. The synchronous pass stays authoritative.

A pre-existing Task.sleep(100ms)-then-assert inside testMarkdownRenderBlocksRemoteImagesUntilUserAction is also gone, replaced by a bounded wait on the real DOM predicate.

Shard 1

plainTerminalTextDoesNotResolveAppShortcutContext() — host dependency. Now fixed on the base branch by 87a70165b5b; this PR no longer changes anything for it. Diagnosis, for the record: 67d6b2a705 added focusTerminalForTesting, whose readiness wait includes window.isKeyWindow. The window comes from AppDelegate.createMainWindow(), so it is a production CmuxMainWindow, and a headless, inactive app never makes one key — the test passed or failed by whether an earlier test in the shard had happened to activate the app. This is the same failure mode 30590ad05f documented and fixed elsewhere with KeyStatusTestWindow, which cannot be used here because CmuxMainWindow is final. This PR originally added a test-target MainWindowKeyStatusPin (a one-time exchange of NSWindow.isKeyWindow consulting a per-instance flag); the base branch instead activates the host, which makes the same state true rather than forcing the answer. The pin is dropped — see Reconciled with the base branch above.

AppDelegateShortcutRoutingTests.testTextBoxSubmitKeepsQueuedRunForStillActiveSurfaceWhenAnotherSurfaceFinishes — host dependency, plus order-dependence on shared static state. The failure shape (sentKeys == [], sentText == []) is only reachable while pasteFilePath's lease.waitUntilApplied() is still pending. That lease drains through beginPreviousContentsCapture, which since 31fd3b9c77 (#8838) re-execs the app binary as a helper process. This was the batch's first .pasteFilePath run, i.e. a cold spawn, on a host that in the same log window took 16.7 s to launch a GPU process. The 5 s waitFor deadline was sized for a warm spawn. The test now waits on the real signal — the fake surface's binding callback, which fires after sentKeys is recorded — with the timeout bounding only the failure path, and calls TextBoxSubmit.debugResetForTesting() at the top the way its sibling already does (e6926fbbda). Stronger: completions == ["finishing"] is now asserted rather than only polled.

Shard 2

keyboardCopyModeKeyClearsTerminalUnread(), workspaceFontSizeShortcutPreservesBackgroundTerminalUnread() — host dependency. Same focusTerminalForTesting key-window gate as above. Also now fixed on the base branch by 87a70165b5b; this PR no longer changes anything for them.

GhosttySurfaceOverlayTests.testPreferredScrollerStyleChangePreservesSystemScrollbarStyle — stale test. The expectation dates from 6d5e284fe3, when scroller presence did not depend on scroller style. fe8872e8a8 (#12918) and 9f29ddfc77 (#12903) introduced TerminalScrollBarPresencePolicy: a legacy scroller is always present, an overlay scroller only with scrollback. The test set scrollerStyle = .legacy by hand and snapshotted contentSize.width before the product re-evaluated presence, capturing the full 360 pt instead of the 345 pt the legacy gutter leaves — so it contradicted its own neighbouring assertions. Stronger: the snapshot is replaced by initialContentWidth - NSScroller.scrollerWidth(for:scrollerStyle: .legacy), a host-independent statement of the invariant.

GhosttySurfaceOverlayTests.testSearchOverlaySurvivesPortalVisibilityToggleDuringWorkspaceSwitchLikeChurn — test bug. setSearchOverlay mounts through a deferred main-actor scheduler (f003c63bae, #9179), and the test used a fixed RunLoop.run(until: +0.05) spin. 9f258dd7c7 converted the sibling test to the class waitUntil helper but missed this one; the three later failures were cascades of the first. Now waits on the same real predicate.

TabManagerCloseCurrentPanelTests.testCloseCurrentPanelClearsNotificationsForClosedSurface — test bug. The workspace had a single surface, and with close-on-last-surface enabled (the default) the shortcut escalates to the window-close path, which a window-less test TabManager cannot perform — so nothing closed and nothing was cleared. The workspace now has a second surface, so the shortcut closes the surface itself, which is what the test is about. Stronger: XCTAssertNil(workspace.panels[initialPanelId]) now asserts the close actually happened, and the confirm-close handler fails the test if it is consulted.

pairingTicketUsesThePublishedV2InstallationIdentity() — test bug / shared-process ordering dependency. The reported error is noRoutes, not the routeUnavailable the test expects, which locates it precisely: createAttachTicket reads the route snapshot before the identity guard, so the route cache was empty. AppDelegate.installMobileHostSettingsObserver() registers a UserDefaults.didChangeNotification observer with object: nil, so any defaults write anywhere in the process schedules MobileHostService.shared.syncToSettings(), which with networking disallowed runs MobileHostIrxRuntime.prepareForStop() → MobileHostPublicStatusCache.removeAll(). The test parked a fixture publication in that process-wide static and then crossed two suspension points; in shard 2 the suite running immediately before is MobileHostServiceSettingsTests, which writes and removes defaults in three parameterised cases. Nothing in the product path changed, and no amount of re-publishing inside the test closes the window. The test now publishes and resolves within a single main-actor turn. Route+identity resolution is extracted into MobileHostService.attachTicketSubject(publishedStatus:routeID:routeKind:target:) — ordinary decomposition with one production caller, no debug naming and no #if DEBUG — which the test drives directly. Separately, and as a real (if latent) product correctness fix, MobileHostPublicStatusCache.publishedStatus() now reads the routes and the v2 identity under one lock; reading snapshot() and currentV2DeviceID() separately could straddle a republish and bind fresh routes to a retired identity. Stronger: the test now also asserts published.routes.contains(route), the selected route kinds, and ticket.macDeviceID, on top of the existing v2 pairing-URL decode.

WorkspacePanelGitBranchTests.testForkAgentWorkspaceLaunchFromPersistentSSHPTYDoesNotReuseParentRelayOrDaemonSlot — test bug. A forked configuration only mints a fresh relay namespace when the control listener can name the socket the new session reconnects through; that comes from the process-wide TerminalController.shared, so the test inherited whatever an earlier test left behind. It now reserves its own startup socket and tears it down. Stronger: XCTAssertEqual(localSocketPath, reservedSocket) pins the new namespace to the reserved socket instead of only asserting it differs from the parent's.

Shard 4

AppDelegateShortcutRoutingTests.testCustomCmdTNewWorkspacePrunesOrphanedContextWithoutLiveWindow — product regression. 6d7b23fbdb changed preferredMainWindowContextForWorkspaceCreation to skip the windowless-context prune, guarding it with (event == nil || eventContext != nil). Its own comment says "Windowless app shortcuts still prune as before", but the condition gates on whether a context resolved, not on whether the event names a window. A responder-chain keyDown with windowNumber == 0 — exactly what a remapped Cmd+T produces — has eventContext == nil, so the stale active context was never pruned and tabManagerFor(windowId:) kept returning it. The prune is now deferred only when the event is addressable and its context failed to resolve, using the existing shortcutEventHasAddressableWindow(_:) predicate. The reindexing-race deferral the guard exists for is preserved; windowless events prune again, as the commit intended.

Shard 5

"Reconnect card and controls stay inside a narrow Cloud split" — stale test. 9bf6cb8c94 (#12609) replaced the AppKit reconnect card — an NSVisualEffectView over an NSStackView of NSTextField/NSButton — with a single SwiftUI hosting view, so overlay.subviews.first as? NSVisualEffectView can never succeed and there is no NSButton to click. The product is correct; the test encoded the old widget tree. The card is now located by an accessibility identifier the overlay sets on it, and Retry is exercised through the same closure the control invokes. Stronger: the narrow-split sweep now runs for both progress states, containment is checked over every AppKit descendant rather than only NSControls, card width is asserted to keep its margin at every pane width and to actually track the pane (190 < 320 ≤ 720) rather than clamping, and a negative case asserts no Retry wiring when the presentation offers none.

browserPanelRetriesDiscardedRestoreAfterConnectionRefused() — host dependency. This one deserves the detail, since the brief asked for a decision. It was synchronising on WKWebView.isLoading, which is a state flag rather than a completion signal. b3755b8523 had already swapped one such wait for another: it stopped waiting 30 s for WebKit's own provisional failure for the reserved loopback port and instead waited 10 s for stopLoading() to clear isLoading, which on these runners it does not do — and the same !panel.webView.isLoading term still gated the surviving 20 s and 30 s waits, so deleting only the 10 s guard would have moved the timeout rather than removed it. None of that is what the case is about: it verifies that a connection-refused discard-restore leaves the pane retryable on the next restore touch, and that bookkeeping is driven entirely by the WKNavigationDelegate terminal callback. It did not need to move lanes. The test now installs a fake navigation source for the panel's web view — the BrowserReloadRecordingWebView pattern BrowserFailedNavigationReloadTests already uses — and reports the refusal through didFailProvisionalNavigation. The panel's restore bookkeeping, error page and retry policy run unchanged. The case now contains no timeouts, no polling and no async. Stronger: four assertions added, including the recorded request URL on both navigations, errorPageLoadCount == 1, and restore_pending == true immediately after the first restore, which the polling version could never observe.

Shard 6

"A late creation preserves newer workspace navigation" — host dependency. 408eddd384 (#13193) gated the late-creation focus steal on window === NSApp.keyWindow, which is nil in this process. That single fact explains all three recorded issues: line 150 fails for both arguments, and line 167 fails only for navigate=false because the guard bails and selection never moves — a cascade, not a regression (the navigate=true path is genuinely suppressed by the cloudWorkspaceSelection.revision guard). The product now asks the window (window.isKeyWindow) rather than the AppKit global, through a named predicate cloudWorkspaceCreationFocusWindow(windowID:) — equivalent in the running app, and the idiom used by 66 other gates in Sources/. The test uses KeyStatusTestWindow and asserts the product's own answer, which still covers context registration, window resolution and key status.

"A surviving Simulator host keeps framebuffer publication active" — host dependency. Frame demand is gated on window.occlusionState.contains(.visible) (aac235180e, #7857), a bit the window server sets only for the active app's on-screen windows. The pane therefore never took frame demand, the emitted transport was dropped, and line 102 passed vacuously because publication was never turned on. Occlusion-gating the framebuffer is the feature, so no product change: the test's host window now pins the on-screen occlusion state, mirroring the KeyStatusTestWindow precedent, and all three expectations exercise the coordinator's per-observer host refcounting.

Checks

Re-run after the rebase onto e160e82af3a: python3 scripts/check-test-determinism.py (0 findings), ./tests/test_ci_pbxproj_test_wiring.sh (18 tests, ok), python3 scripts/ci/validate_test_execution_registry.py (valid, 223 tests), and the five guard-structure suites tests/test_ci_{app_host,quality,release,source_lint}_guard_structure.py and tests/test_ci_workflow_guards_are_wired.py (6 passed). Every changed Swift file also passes swiftc -parse. No .pbxproj change was needed and no file was added, so nothing needs target membership or registry wiring. Per the repo's rules nothing here uses a sleep or a state-flag poll as a synchronization primitive; the only remaining Task.sleep calls are watchdog deadlines on failure paths and poll intervals inside deadline-bounded waits on real predicates. No full local app build was run, so none of these fixes is executed here — CI on this PR is the first execution.

🤖 Generated with Claude Code


Summary by cubic

Fixes the remaining app-host failures in shards 1, 2, 4, 5 and 6, including the four shard-2 XCTest-host relaunches. Most trace to one cause: the headless test process is never the active app, so AppKit reports no key window or visible occlusion and WebKit never fires animation frames behind XCTest's shielding window.

Host-dependency fixes

  • MarkdownPanelTests WebKit calls now fail at a deadline with a named reason instead of hanging the whole host; renderMarkdown waits on the viewer's own synchronous render contract, and the scroll restore no longer clobbers a scroll that lands after a content update.
  • Focus and occlusion gates now ask the window itself (a key-status pin, OnScreenTestWindow, or a named predicate) instead of NSApp.keyWindow or the window server.
  • The reconnect-card test finds the SwiftUI card by the overlay's accessibility identifier; the connection-refused restore test reports the failure through a fake navigation source, dropping its timeouts and polling.

Product fixes and remaining test bugs

  • Windowless shortcut events prune the orphaned main-window context again; the deferral now applies only to addressable, unresolved events.
  • Ticket minting reads routes and the v2 installation identity under one lock, so fresh routes can't bind to a retired identity.
  • TabManagerCloseCurrentPanelTests gives the workspace a second surface so the shortcut closes a surface; the fork-relay, search-overlay, and scroller-style tests reserve or wait on deterministic state instead of inheriting ambient test order.
  • No assertion is weakened anywhere; several are strengthened.

Written for commit e353dd5. Summary will update on new commits.

Review in cubic

@cursor

cursor Bot commented Sep 22, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 8624eb4b-b1f1-4bb1-a198-4091413e4c3c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

…aunches

Most of these share one cause: the app-host test process runs headless and is
not the active app, so AppKit and WebKit withhold state the tests assumed.
NSApp.keyWindow is nil, makeKeyAndOrderFront never makes a window key,
occlusionState never carries .visible, and behind XCTest's shielding window
WebKit suspends requestAnimationFrame entirely.

The four shard-2 host relaunches were one hung await: renderMarkdown awaited
two animation frames inside callAsyncJavaScript (cd9f34f), which behind the
shielding window never fire, so four MarkdownPanelTests cases each hung until
XCTest's five-minute allowance killed the host. Every WebKit call in that file
now fails fast with a stated reason instead of hanging, renderMarkdown waits on
the viewer's own render contract, and the scroll restore the rAF await was
papering over no longer clobbers a scroll that lands after a content update.

Two real product regressions: windowless shortcut events stopped pruning the
orphaned main-window context (6d7b23f), and ticket minting read the routes
and the v2 installation identity as two separate cache reads.

No assertion is weakened; several are strengthened.

Rebased onto fix/app-host-green after 87a7016 landed a different remedy for
the same key-window cause. The three focus tests this branch fixed through a
test-target MainWindowKeyStatusPin (a swizzle of NSWindow.isKeyWindow, needed
because CmuxMainWindow is final) are exactly the three that commit fixes by
activating the app host and widening the pump's timeout. The pin is dropped:
activating the host makes isKeyWindow true for real rather than forcing the
answer, and a process-wide swizzle installed for one helper is a worse neighbour
to the rest of the shard. cmuxTests/AppDelegateMainWindowTestingSupport.swift is
no longer touched by this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@teamleaderleo
teamleaderleo force-pushed the fix/app-host-shard125 branch 2 times, most recently from 2a149ef to 05967b2 Compare September 22, 2026 23:35
@teamleaderleo
teamleaderleo merged commit 938dbab into manaflow-ai:fix/app-host-green Sep 23, 2026
45 checks passed
teamleaderleo added a commit to teamleaderleo/cmux that referenced this pull request Sep 23, 2026
…ng focus gate

Reconciled onto fix/app-host-green after a682c55 and manaflow-ai#13759 landed.

Dropped: the `TerminalNotificationDirectInteractionTests` portal-owner change.
a682c55 ("test: give standalone terminal fixtures a live portal authority")
is the same fix for the same three tests, through a `makeLivePortalWorkspace()`
fixture instead of `TerminalPortalTestWorkspace`. Two fixtures granting the same
portal authority to the same three surfaces is one too many, so this branch no
longer touches cmuxTests/TerminalAndGhosttyTests.swift at all.

Kept: `terminalHostedEditableResponderKeepsLocalUndo` targets its Cmd+Z menu
item at the editable responder. A nil-targeted menu action resolves through
`NSApp.keyWindow`, which is nil while the headless host is inactive, so
`performKeyEquivalent` returned true while `undo:` reached no responder. The
contract under test is cmux's routing -- the menu handles Cmd+Z and the terminal
sees no menu miss -- not AppKit target resolution. The sibling
`terminalDeclinedUndoCommandFallsBackToTerminalKeyDownWithoutLocalUndo` keeps
its nil target and its `undoCallCount == 0` expectation.

Kept: the focus-gate diagnostic. manaflow-ai#13759's reconciliation dropped its
`MainWindowKeyStatusPin` for 87a7016's `NSApp.activate(ignoringOtherApps:)`,
but that remedy lives inside `focusTerminalForTesting`, and none of
`automaticApplyDoesNotBypassHiddenTinyFirstResponderDeferral`,
`findTerminalRestorePreservesHiddenTinyFirstResponderDeferral` or
`testTerminalFirstResponderFeedbackPreservesActiveFocusTransaction` calls that
helper -- they build `KeyStatusTestWindow` directly. `applyFirstResponderIfNeeded`
gates on `isActive`, `isVisibleInUI`, hidden/geometry, `window.isKeyWindow`,
the focus target, the keyboard-focus coordinator and the command palette, and
on none of them does app activation bear. So the base has not addressed these
three, and their assertions still need to name the gate that refused. The
summary now also prints `windowIsKey` and `appActive`, so the next run says
whether key status was real or only `KeyStatusTestWindow`'s override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Sep 23, 2026
* test(terminal): restore the presented-surface fixture contract so package tests compile

`swift test --package-path Packages/macOS/CmuxTerminal` has not compiled on
main since merge 38b32bb: TerminalSurfaceRendererCallbackTests calls
`PresentedSurfaceFixture(installRendererCallbacks: false)`, but the fixture
initializer only takes `windowVisibleAtCreation`. The flag came from the
issue-2824 branch (77c46d0, 6fe70f6) and was dropped when the fixture
was reworked for the native callback lifecycle (8008b7c, 97c6088);
the merge re-applied the test call without the fixture side.

Package tests only register render callbacks through the fixture
(RendererCallbackTestSupport), so a fixture that skipped registration would
leave `cmux_test_ghostty_renderer_present` with nothing to route. Restore
the calls to `PresentedSurfaceFixture()`, the combination that was green at
0251a44. Verified locally: 294 tests in 37 suites pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(popover): stop rewriting the presentation binding during view update

`ArrowlessPopoverAnchor.updateNSView` calls `coordinator.dismiss()` on every
update where `isPresented` is already false. PR #13311 (01f0a50) made
`dismiss()` write `isPresented = false` on the no-popover path, which SwiftUI
reports as "Modifying state during view update" because updateNSView runs
inside the view update. The sidebar footer mounts two anchors whose parents
re-evaluate on every `selectedTabId` change, so every workspace switch emitted
faults and `SidebarWorkspaceSwitchLayoutFaultTests` failed with 15 of them.

Pass `resetPresentation: false` from updateNSView: the binding is already
false on that branch, so the write was redundant. `popoverDidClose` still
resets the binding when AppKit closes a live popover.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(cloud): satisfy the plus menu's sign-in gate and route Cmd+Y through a registered window

`NewCloudWorkspaceShortcutTests` never passed: the plus menu deliberately hides
its Cloud rows unless the account is signed in (#12305, mirroring the command
palette and File menu), and a test `AppDelegate()` has no account flow. The
XCTest version crashed the app host on `rows[1]`, xcodebuild restarted it, and
the lenient gate accepted the partial run; #13178 now rejects that and #13193
migrated the suite to Swift Testing, so the failures became visible.

Inject the signed-in state through the existing `isAuthenticated:` seam, add
signed-out coverage of the gate, route the Cmd+Y event through a registered
main window (as `testReboundKeyRoutesAndOldKeyDoesNot` does, since shortcut
routing bypasses events bound to windows the delegate cannot resolve), and
register a window context for the unavailable-Cloud check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(chrome): assert the composited Bonsplit chrome contract instead of a stale alpha hex

`WorkspaceChromeColorTests` expected `bonsplitChromeHex` to return
`#1122337F` (theme with opacity as alpha). Since b6d3470 (2026-05-19)
`compositedTerminalColor` composites the theme over the window base and
returns an opaque color, and 4cbb354 made that deliberate: Bonsplit derives
its tab glyph contrast from the rendered backdrop, and an `#RRGGBBAA` hex
reproduces the white-on-white bug it fixed. The tests kept failing unnoticed
because the app-host gate only counted "unexpected" XCTest failures until the
strict check (acedf3f) reached main through #12053.

Exercise the `chromeBackgroundColor` seam that every production call site
uses with literal expectations, verify the ambient default path against the
resolver plus independent blend arithmetic so the result is deterministic
under any host appearance, and keep coverage for opaque, shared-backdrop,
pane-clear, pane-border, and explicit translucent chrome colors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(tmux): wait for the main window to adopt the mirror pane before sending keys

`RemoteTmuxMirrorPaneInputMappingTests` required `panel.surface.uiWindow != nil`
right after selecting the workspace. A manual-I/O mirror pane spawns eagerly in
its hidden bootstrap window, which `uiWindow` deliberately excludes, and the
main window's portal adopts the pane host only once AppKit and SwiftUI get
run-loop time; `waitForLiveSurface` returns immediately for an already-live
surface, so the check ran before adoption and the four key-delivery tests
failed at line 176. The failure was hidden until the strict app-host gate.

Order the harness window front and pump the run loop until the surface and
its native view are in that window, as the other hosted-view input suites do,
and assert against the harness window instead of any window.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(browser): report the refused connection through the navigation delegate

`browserPanelRetriesDiscardedRestoreAfterConnectionRefused` waited up to 30s
for WebKit to fail a provisional load to a bound-but-unlistened loopback port.
On the hosted app-host runners that failure never arrives: the load neither
fails nor commits, so the test timed out at line 147 (no
"provisional navigation failed" log line appears for it in any shard).

Stop the in-flight load and report `NSURLErrorCannotConnectToHost` for the
attempted URL through the panel's real navigation delegate, the pattern
`BrowserFailedNavigationReloadTests` already uses. The restore bookkeeping,
error page, `restore_pending` clearing, and retry policy under test run
unchanged and every assertion is kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(cloud): scope reserved-workspace cleanup to its pending card and return the bind window

PR #13202 (5d616a7) imported `CloudMachineWorkspaceAdoptionTests` and
`CloudMachineWorkspaceResolutionTests` from the still-open
13141-cloud-vm-workspace branch without the production changes they assert,
and its own run skipped the app-host shards, so they landed red:

- `NewMachineSheetPresenter.closeReservedWorkspace` closed the whole
  workspace, which is a no-op for the last tab and discards user panes added
  next to a creating card. Port the branch behavior: remove only unadopted
  loading cards owned by the cancelled machine, clear that binding, keep user
  content, and give a last-tab loading workspace a local anchor first.
  `MachineCreateCoordinator` passes the created or reconciling machine id so
  cancelling machine X cannot clear a binding to machine Y (the tests now
  assert that scoping).
- `v2WorkspaceCloudVMBind` now returns `window_id` alongside the workspace
  refs, as the bind acknowledgement test expects.
- The resolution test selected "first" while the fixture's terminal key is
  "term-first"; use the key so the placement resolves as intended.
- `CloudPaneCreationRetryTests` asserted `discarded` synchronously after the
  projection returned, but the coordinator applies its generation fence behind
  `CloudOperationContext.withPhase`'s recorder await, which suspends on a cold
  per-suite process. Settle with bounded yielding before asserting.

Runtime behavior change (cancelled Cloud create cleanup); needs dogfood.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(cli): restore deferred socket connection, explicit SSH control options, and Codex stop idle observations

Branch commit c8bfb58 ("fix: repair failures exposed by strict app-host
CI", 2026-09-10) made `cmux vm dev|layout|env` finish local validation and
dry runs before opening the socket, passed the caller's explicit ssh options
into `userConfiguredControlOptions(fromSSHConfigOutput:explicitOptions:)` so a
normalized `ControlPersist=0` from `ssh -G` is not mistaken for host
customization, and published `idleObserved` for transcript-terminal prior
turns before a legacy Codex Stop so the journal drops an obsolete running
turn. The branch's later single-parent commit 38b32bb reverted
`CLI/cmux.swift` to main's version while keeping the stricter fixtures, and
PR #12053 landed that inconsistent state; the fixtures (CLIVMDevTests,
CLIVMLayoutEnvTests, the SSH sharing tests, and the Codex missed-prompt Stop
test) have failed since.

Restore the three CLI changes. `SocketClient.configureAuthentication` and
`SocketPasswordResolver` already exist on main, and the CmuxFoundation
overload landed with the branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(cli): align CLI integration fixtures with the shipped hook, SSH, and session-list contracts

The main copy of `CLINotifyProcessIntegrationRegressionTests` predates
several shipped contracts that the lenient app-host gate never enforced and
that 38b32bb reverted on the issue-2824 branch: Claude hook acks print
`{}` (#7963), Codex resume bindings require rollout evidence so fixtures
carry a `session_meta` transcript (#10100), SessionStart publishes a binding
so `/clear` counts start after the clear, fresh-terminal SSH startup commands
are script paths that the support decoder must read, cmux control-path
options follow a resolved `ssh -G` (#8308), and `ssh session list` reports the
localized "remote state unavailable" summary with `--json` detail (#9971). The
missed-prompt Codex Stop test now asserts the restored `agent.idle.observed`
for the prior turn.

Flagged for review: the three `ssh pty-attach` cases now expect only
`workspace.remote.pty_bridge` once the endpoint is established, matching
#12726's `preserveLifecycleForRecovery`; if pre-READY bridge failures were
meant to keep reconciling, the flag should be set only after READY instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(workspace): restore the app-host repairs for fork, focus recovery, and shortcut routing

Merge 8d33410 on the issue-2824 branch took main's copy of
`cmuxTests/WorkspaceUnitTests.swift` wholesale and discarded the branch's
September repairs (c8bfb58, c402b9c, 2127d97); PR #12053 then landed
without them while the strict gate started counting the failures. Restore
them against current production, plus the shortcut-suite fixes:

- Fork in a remote workspace: `sshBootstrapArguments` has used `/usr/bin/ssh`
  since #9114, and agent socket propagation requires a socket that exists on
  disk (3bf87e3), so bind a real unix socket and expect the absolute path.
- Fork Conversation context actions dispatch asynchronously (#7259, #8173);
  await the fork panel before asserting.
- Git branch and pull request updates publish through
  `sidebarObservationPublisher` since #6226, not `objectWillChange`.
- Config sanitization: `addWorkspaceIfActive` rebuilds templates from the
  font-size lineage since #8543; override the lineage hook instead.
- Focus recovery: AppKit focus is authorized only for a registered, selected
  workspace whose window carries the main-window identity; use the
  `TerminalPortalTestWorkspace` fixture and the same registration/pump path
  as `WorkspaceTerminalFocusRecoverySwiftTests`.
- Shortcut routing: clear both Cloud defaults after 9c2ba78 swapped them;
  neutralize Ghostty's imported goto_split fallback (⌘] by ANSI keyCode) in
  the unshifted-symbol test and assert the digit shortcut does not match;
  wait for the async runtime start before judging keyDown forwarding (skip
  loudly without a live surface); wait for the portal to mount before the
  second-Escape check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(restore): keep a restore identity when the persisted surface id collides

Since #13098 (e0e77eb), `newTerminalSurfaceOutcome` treats a nil
`restoredSurfaceId` as an interactive create and routes it to the selected
pane's Cloud source. Session restore passed nil whenever the persisted panel
id was still live (duplicate-workspace or restore-into-live), so a legacy
managed-Cloud SSH workspace restored into a live manager was turned into a
remote tab create: the scaffold panel stayed startup-suppressed with no
initial command and `TabManagerSessionSnapshotTests` failed to unwrap it.

Mint a fresh UUID on collision instead of nil; it is free by construction, so
the restore keeps its identity and the old-to-new remap works as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(terminal): align snapshot, projection, terminal, and browser fixtures with shipped contracts

Stale expectations and fixed-spin timing in app-host suites that the lenient
gate never counted:

- Cloud-projected panes restore as manual-mirror reservations with a staged
  remote identity (#12675), not a local placeholder projection.
- Restore reuses the persisted runtime id when free (aff0e32), so assert
  liveness rather than a new id; the catalog ignores writes for a Cloud
  machine without a registered provider (#11877), so register the fixture
  provider before publishing.
- Wheel sync requires an authoritative scrollbar response (bbc3edf); the
  fixture now answers like `AuthoritativeScrollbarSurfaceView`.
- Search overlay mount, first-responder focus, runtime creation, and the
  visibility-restore redraw run through deferred main-actor tasks; wait for
  them with the class's `waitUntil` helpers instead of fixed run-loop spins.
- Owning the socket path lock is definitive (959f38a): a refused inode left
  by a dead listener is replaced, so the restarted listener accepts.
- The split-divider hit band extends `dividerHitExpansion` past the divider
  (667cc43); derive the pass-through boundary from the constant.
- Browser page background blends against Ghostty's effective terminal color
  scheme, which is host dependent; read the same preference the product uses.
- Cloud Machines defaults on in dev builds (#12318); pin the toggle off for
  the default-mode palette contract.
- Prepared navigation requests keep the caller's cache policy (#13003).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(browser): align lifecycle, identity, host-view, loopback bridge, and portal rebind tests with shipped contracts

Browser app-host suites that the lenient gate never counted:

- `BrowserPanelWebViewLifecycleTests`: out-of-range discard delays are
  rejected to the default (the cmux.json loader relies on the nil), and the
  panel's own `isLoading` stays true for the indicator floor, so wait for both
  flags and assert no discard blockers before discarding.
- `browserNavigationUsesEmbeddedWebKitIdentity`: WebKit reports the native
  identity as nil or "" (#9482); accept either.
- `WindowBrowserHostViewTests`: production routes Dock-divider hits by
  yielding to AppKit so the live sidebar tracker receives them (#10902,
  e2e3818); the tests asserting the portal owns and forwards the hit were
  merged red against a design that never shipped. Realign the stale-frame
  test to the pass-through contract and remove the four own-and-forward
  tests with their fixtures. **Flagged for review:** if own-and-forward is
  still wanted, that is a hit-testing product change for its own PR.
- `testRemoteWorkspaceRuntimeBridgeAliasesMultipleLoopbackPortsFromSamePage`:
  the navigation delegate restarts main-frame loads to apply the user-agent
  policy (11c6efe); a direct `loadHTMLString` with an HTTP base skipped
  that step, so the data load was cancelled and replayed as a deferred
  request. Apply the identity first and assert the document is current.
- `portalRebindPreservesDocumentAndRoutesRefreshToTheSameWebView`: the portal
  host is a theme-frame sibling of `contentView` for non-glass windows
  (#12929); assert same-window instead of descendant-of-contentView.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(terminal): wait for asynchronous runtime creation in the remaining direct-interaction tests

The same "Expected runtime surface before ..." precondition that
9f258dd made wait for the deferred runtime start still sampled after a
fixed run-loop spin in the detach-race, close-lifecycle, repeat-key, and
repeat-IME tests, and CI on the branch head showed them failing that way.
Use the class's `waitUntil` for those four sites too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(cli): model surface.respawn for respawn-pane and give the Codex stack fixture rollout evidence

`cmux respawn-pane` has sent `surface.respawn` instead of `surface.send_text`
since #5465 (8cafcc3); the window-flag fixture's mock still rejected that
method, so the CLI exited 1. Answer `surface.respawn`, asserting the window and
surface ids, `tmux_start_command`, and that the shell-invoked command carries
the user command but never the `--window` flag, which is the test's intent.

The Codex interrupted-stack fixture's transcript had no `session_meta` line,
so `CodexSessionResumeVerifier` (#10100) found no rollout evidence and the
prompt published `surface.resume.clear`. Prepend the session line as the
other Codex fixtures do.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(socket): keep mobile.panel.artifact.fetch off the local socket and advertise the served artifact reads

02c1ba4 removed `mobile.panel.artifact.fetch` from the socket worker
methods because it needs the authenticated mobile execution context, but
that half of the change was lost in a merge: the policy still routed fetch to
the worker lane, which has no handler for it, so the local socket answered
`internal_error` instead of the `method_not_found` boundary that
`TerminalControllerSocketSecurityTests` pins. Restore the removal.

`system.capabilities` never advertised `mobile.panel.artifact.stat` and
`.thumbnail` although both are served on the worker lane (04ff18e added
them only to the test's expectation); advertise those two and keep fetch out.
The remote relay allowlist is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(remote): align tmux seed transport, SSH, socket command, and port scanner fixtures with shipped behavior

- `RemoteTmuxPaneSeedTransportTests`: manual-I/O mirror panes spawn eagerly
  (#9272) and stay runtime-backed after portal churn (#9769), so the pane
  renders its assigned grid before a seed arrives and nothing was retained.
  Publish a larger tmux pane grid than any test surface applies so the
  retention under test sees the lag it exists for.
- `SSHRemoteCWDRegressionTests`: the persistent-PTY exec helper runs only
  with `protectsFromHangup: true` (cd9f34f); pass it, and widen the
  first-spawn guard.
- `SSHDeepSleepReattachTests`: a Cloud-owned workspace rejects launch
  overrides on splits (#13098); create the custom-identity pane before
  configuring the remote connection.
- `SSHConfiguredRemoteCommandHostTests`: ssh-pty-attach validates the bridge
  `daemon_version` before dialing (#12726); the mock now reports one.
- `CloudManualMirrorTransportTests`: the pane failure card uses the short
  title since 9bf6cb8.
- `SurfaceSocketCommandTests`: `vm.workspace_new` admits its optimistic
  workspace through the active main window (#13152, #13155), so bind a bare
  window to the fixture context; a receipt without a starter terminal costs
  one snapshot (6d43ea6).
- `PortScannerPublicationTests`: the forced-result acknowledgement hops off
  the main actor, so an unchanged port set may be deduped under a later
  refresh; drain publications until the retirement lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: align mobile artifact fetch lane assertion

* repair: close remaining full-suite contracts

* test(restore,sidebar): align two stale contracts with shipped behavior

testRemoteWorkspaceAutoResumeKeepsRemoteStartupCommand asserted the restored
remote panel's local spawn cwd equals the remote-host path. It never can:
OneShotTerminalLauncherStore.enterableWorkingDirectory rejects a path that is
not locally enterable, which is what keeps the owning shell valid (#7031).
The remote cwd does survive restore — as the panel's trusted remote directory
report, and as the `cd` prefix the resume input carries (both already asserted).
Assert it where it actually lives and pin the spawn cwd to nil.

testSidebarPullRequestsTrackFocusedPanelOnly expected a background panel's PR
to be hidden from sidebarPullRequestsInDisplayOrder(). That list is documented
as the workspace's deduplicated rows in pane/tab order, both consumers
(taskStatusSignals, the control-sidebar snapshot) want every panel, and the
sibling branch test asserts the same all-panel model. Focus scoping lives in
the `pullRequest` binding, which the test already covers. Renamed to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: give Cloud catalog tests live destination workspaces

#13196 made SurfaceCatalog.validateOwnership refuse a destination that is
not a live workspace. That rule stays. Tests that projected, restored or
opened browsers into a made-up workspace ID now failed with
destinationNotFound, timed out waiting on a provider that was never called,
or passed a later check for the wrong reason.

LiveWorkspaceFixture registers real Workspace objects and hands the catalog
a CloudWorkspaceRenameService that resolves them, the way the app's
composition root does. Its workspaces() list stays empty so the native
projection coordinator does not start mirroring into them. For tests on
SurfaceCatalog.shared, withAppRegistration registers the TabManager as a
windowless main-window context for the test body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: normalize pbxproj after live workspace fixture

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: admit agent renames of agent-owned accepted Cloud names

e6926fb (#13403) made submitCloudPanelRename run admitsTerminalRename
for automatic names. Its last clause only admitted replacing an accepted
name when the local panel still carried `.auto` provenance, but since
1e1d319 an accepted daemon name reconciles locally as `.remote` and the
owner lives in the tab's nameAuthority. Every agent title after the first
accepted one was refused, so "Failed agent rename keeps the accepted title",
"Mirroring an agent-named placement does not block its next agent title" and
"An older automatic result and old snapshots cannot replace an accepted name"
failed on their second agentName call.

Admit an automatic rename when no write is pending and the accepted tab name
is owned by the daemon's `auto` authority. User-owned names, pending writes and
local user titles on any projection still refuse it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: expect adopted machine rows to keep the pending create identity

#12919 (021f792) made New Machine creation optimistic: once a running
create's machine appears in the fleet list or catalog, its row keeps the
`pending-machine:<operation>` node ID so selection and expansion survive
adoption (see committedProjectionKeepsThePendingNodeIdentityUntilFleetAdoptsIt).
pendingRowStepsAsideOnceItsMachineHasARow still expected `machine:<id>`.

Assert the new identity and that the row is the adopted machine, not a
stand-in: the stand-in is gone, one row remains, and it shows the created
machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: restore per-display noVNC targets and refresh stale Cloud tests

Product (#13196 regression): the 6db7593 main merge into
13192-cloud-display-ownership took main's CmuxTuiSurfaceProviders.swift and
CloudPortRoutePlan.swift, dropping eb3edd7's per-display ports.
23c807b restored the display coordinator but not these hunks, so
withPrivateBrowserURL rewrote every display to 6901 and a daemon pointer
without a discovered target fell back to display 1. Restore both hunks and
drop the duplicate port-less privateDesktopURL overload.

Tests:
- CloudDisplayCatalogTests: 178d35e (#13196) made every guest command run
  `list` as a readiness probe, so fakes dispatching on " list" answered
  creation with the list catalog. Dispatch on the create action line, and pin
  the command shape.
- CloudPortOpenRegressionTests: 178d35e (#13196) filters RFB/noVNC ports
  only when the display catalog owns them (displayPortsOwned). Assert both
  the desktop and non-desktop results.
- CloudTreeOneMachineManyWorkspacesTests: #12740 added a final Resources
  section under each machine. Expected trees include it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: publish every guest display from daemon graph updates

The 6db7593 main merge into 13192-cloud-display-ownership (#13196) put
back main's [desktopDisplayResource()] pools in CmuxTuiSurfaceProvider's
refresh, publish and delta paths. 23c807b restored the display
coordinator lifecycle but not these pools, so a display created beyond
display:1 vanished on the next daemon publish, and a delta that touched it
removed it. Restore eb3edd7's displayResources pools, the display-kind
delta check, and the injectable displayCoordinator. Drop the now-unused
desktopDisplayResource().

Adds a test that creates display:2 through the provider and asserts that
both displays and their ports survive a full publish and a display delta.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: fence the fake Cloud projection reply with its mutation cursor

aDetachedTerminalDropsItsStaleTabBeforeMoving installs a daemon graph
since c402b9c (#13403). When the placement lane drains it reconciles
against that graph, which predates the projected tab, and clears
tab_projected. The real reply always carries a mutation cursor
(CmuxTuiSnapshotParser.placedTab requires one) that fences exactly this;
the fake returned none. Give the fake a projectCursor and set it ahead
of the installed graph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: register the restored window before relinking Cloud projections

#13196 made SurfaceCatalog.validateOwnership refuse a destination the app
cannot resolve, so SurfaceCatalog.shared no longer relinked a restored
projection into a TabManager that no main window owns. Register it for
the test body with LiveWorkspaceFixture.withAppRegistration, as #13651
does for CloudClosedPanelRestoreTests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: wait for the relay ports kick, not the first relay line

Since #8442, a relay prompt also reports shell state. Both RPCs run in
separate background children, and the zsh prompt-refresh test waited
only until the log was non-empty, so it could read report_shell_state
alone. Wait (deadline-bounded) for the ports_kick line itself, in the
zsh test and its bash sibling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(cli): relay output of an SSH session that ends right after auth

The expect wrapper watches the first two seconds after sending the
password for a rejection with log_user 0. A session that authenticates
and exits inside that window hit the eof branch and its output was
dropped. Flush the buffered output before exiting. #13207 replaced the
test's 9 s sleep with a FIFO release, which is what exposed this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: restore the no-connection path for rejected vm dev input

4120e35 taught runVMDev that invalid input never connects: keep the
listener open, then check its backlog after the CLI exits. #13207 dropped
that again, so each rejected run waited 60 s for a mock-server
expectation that only the listener closing (after the wait) fulfills.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: drain the terminal while the SCP host-key failure runs

The pty case read the master only after the CLI exited. A pty's output
queue holds about 1 KiB and the host-key failure report is longer, so the
CLI blocked writing stderr until the 30 s timeout. Read the master on a
thread while the CLI runs. The test starts its own sshd; no runner host
dependency is involved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: fail fast when a gated Cloud call ends before its fake is entered

Every app-host restart in the 09-21/09-22 shard logs I sampled was a Swift
Testing time-limit hit in one of two suites, and each hit relaunches the
test host:

- SurfaceCatalogTests: when catalog.project threw before reaching the
  provider (destinationNotFound, fixed by 3dc91b2), each gated test
  parked in MaterializeGate.waitUntilEntered() until the 300 s limit.
  Five tests restarted the host one after another, about 25 minutes per shard.
- CloudDisplayCatalogTests: when the fake no longer recognized the create
  command (fixed by 70eaf44), create() failed before the exec started
  and `await started.result` parked until the 60 s limit.

The waits now also end when the caller's task finishes, so the next such
setup failure is an ordinary failed #require. The cancellation test also
waits on its own cancellation signal instead of a 60 s Task.sleep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: reveal a retired terminal through the portal rebind, as the app does

Since #12607 hiding a terminal removes its hosted view from the window, and
only a bind reinstalls it. The test flipped portal visibility on the detached
view, so no size commit could ever land and the final shrink check failed
every run. Rebind like TerminalPortalReconciliation and drop the wait loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: pin key-window status in terminal focus suites

The app-host test process runs headless and is usually not the active app,
so makeKeyAndOrderFront never makes a programmatic window key. Terminal focus
paths gate on isKeyWindow (automatic first-responder apply, focus redraws,
deferred focus reapply, ensureFocus window activation), so these suites
passed only when an earlier test in the shard had activated the app.

Share the existing KeyStatusTestWindow and use it in
WorkspaceTerminalFocusRecoveryTests, WorkspaceTerminalFocusRecoverySwiftTests
and TerminalNotificationDirectInteractionTests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: report which layer holds off-plan tmux mirror geometry

offPlanGeometryWithUnchangedSizingInputsReconverges fails every run with all
three output-parity re-arms spent and the hosted view still at the perturbed
0.8 divider. A standalone bonsplit replay of the same sequence converges, and
a sibling test without a bound (portal-visible) workspace heals the same
displacement. Add the live split view's arranged widths and the split model's
imposed extent to the failure message so the next run shows whether bonsplit
refused the apply, the imposition was cleared, or the portal did not follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: restore Cloud projections before the workspace is published

TabManager.restoreSessionSnapshot restores each workspace's surface
projections before it assigns tabs, so SurfaceCatalog.restore's ownership
check could not find the destination and silently dropped every restored
remote projection. Check ownership against the workspace being restored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: repair stale and broken app-host expectations

- CLI ssh attach: count identity UUIDs exactly; #11497 added an auth token uuidgen
- device mirror directories: give the fake device trusted presence (e3d424c gate)
- font zoom mirrors: deltas from the 8pt inherited base (e6926fb arithmetic)
- Computer Use refresh: fake daemon reply was invalid JSON in a raw string (#13599)
- quit alert: compare button alignment rects, not padded frames
- remote split cwd rescue: cwd travels as CMUX_REMOTE_INITIAL_CWD since #12054
- default freestyle split: Cloud-owned splits route to Cloud since fa5dc4c

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: activate the app host before waiting for terminal focus

`focusTerminalForTesting` waits for `window.isKeyWindow` before it hands
first responder to the terminal, but `makeKeyAndOrderFront` only makes a
programmatic window key while the test host is the active app. The
app-host process starts inactive under `xcodebuild test`, so the wait
succeeded only when an earlier test in the shard happened to activate the
app. Its callers use a real `createMainWindow()` window, which cannot be
swapped for `KeyStatusTestWindow` the way the pinned focus suites were.

Activate explicitly, and give the wait a CI-appropriate timeout: the
activation and the key-window transition land on later main run-loop
turns, and the pump's one-second default expires before the window goes
key on a contended runner.

Observed on run 35743588302: `plainTerminalTextDoesNotResolveAppShortcutContext`
(shard 1/6), `keyboardCopyModeKeyClearsTerminalUnread` and
`workspaceFontSizeShortcutPreservesBackgroundTerminalUnread` (shard 2/6)
all fail at this helper's return value, and shard 6/6 shows the same
process state directly as `NSApp.keyWindow -> nil`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: give standalone terminal fixtures a live portal authority

`setVisibleInUI` and `setActive` fold their request through
`Workspace.portalRenderingEnabled(for:)`, which denies any workspace id
the app delegate cannot resolve to a selected tab. Three direct
interaction tests build their surface with `tabId: UUID()`, so once any
earlier test installs an `AppDelegate.shared` the authority denies the
portal, the hosted view is never actually made visible or active, and the
fixture stops exercising the behavior it asserts: the surface never takes
Ghostty focus and never schedules a visibility-restore redraw.

Register a real selected workspace and build the surface with its id, so
the fixture gets the same authority the app grants the selected tab.

This is the fixture, not the product: the authority check is deliberate,
and denying an unresolvable workspace is what keeps queued portal
callbacks from reviving an inactive workspace.

Fixes on run 35743588302 shard 4/6:
`testKeyDownRecoveryDoesNotReplayFocusAfterResponderMovesAway`,
`testVisibilityRestoreRefreshesSurfaceWhileTerminalIsInactive`, and
`testDirectFirstResponderFocusRefreshesCursorStateAfterForeignResponder`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: normalize project.pbxproj after merging main

Run 35763999808 failed `Fast static checks` before any macOS job could
start, so the app-host test fixes on this branch were never exercised:

    error: cmux.xcodeproj/project.pbxproj is not normalized.
    Run scripts/normalize-pbxproj.py to fix.

The branch head alone checks clean. CI builds the merge of this branch
into main, and that merge is what leaves the file unnormalized, so the
failure does not reproduce without merging main first.

The change is one build-phase entry moving back into alphabetical order.
`LiveWorkspaceFixture.swift in Sources` keeps the same UUID and the same
occurrence count, and `scripts/lint-pbxproj-test-wiring.sh` still reports
ok across 1049 test files, so no target lost a source file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Drop a cancelled fork-probe request, and name the inputs behind the last two app-host failures (#13744)

* fix: drop a cancelled fork-probe request while it waits behind an active probe

A second fork-availability request for the same panel with a different
fallback snapshot parks in `applyPendingForkValidations`' contention
branch: it restores its request to the pending queue and awaits the
active probe. That wait had no cancellation handler, unlike every other
wait in this type, so a cancelled caller kept its request in the queue
until the active probe finished. The probe's completion restarts the
single-flight refresh for whatever is still pending, and the cancelled
request rode along: its fallback was probed and its result replaced the
surviving request's validation for that panel.

Give the wait the same cancellation handler its siblings have, and drop
the waiter's own pending requests when it is cancelled, so the restart
that follows the active probe sees only live requests.

Covers cancelledSharedForkProbeRefreshPreservesSurvivingFallbackSnapshot,
which failed when the restart won the race against the cancelled task's
own cleanup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: name the input behind the off-plan re-arm and the sidebar reveal double pass

Both failures currently report only their outcome, and each has two
incompatible explanations that the message cannot tell apart.

offPlanGeometryWithUnchangedSizingInputsReconverges reports `rearms=3`
with `imposed=nil`. That is either three recovery passes that ran and
failed to impose, or a re-arm budget already spent before the
perturbation, in which case no recovery pass ran at all. Bracket the
recovery window with the existing DEBUG sizing counters and report the
budget at perturbation time plus the planned outers.

visibilityToggleKeepsAppKitTableContainerMounted reports exactly two
projections per row. Report how many the first run-loop turn produced
and which async signals landed inside the reveal window (the workspace
directory channel, workspace order, the shared agent index), since the
hidden phase queues main-queue work that can land during the reveal.

No assertion changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* test: repair the remaining app-host failures and the shard-2 host relaunches (#13759)

Most of these share one cause: the app-host test process runs headless and is
not the active app, so AppKit and WebKit withhold state the tests assumed.
NSApp.keyWindow is nil, makeKeyAndOrderFront never makes a window key,
occlusionState never carries .visible, and behind XCTest's shielding window
WebKit suspends requestAnimationFrame entirely.

The four shard-2 host relaunches were one hung await: renderMarkdown awaited
two animation frames inside callAsyncJavaScript (cd9f34f), which behind the
shielding window never fire, so four MarkdownPanelTests cases each hung until
XCTest's five-minute allowance killed the host. Every WebKit call in that file
now fails fast with a stated reason instead of hanging, renderMarkdown waits on
the viewer's own render contract, and the scroll restore the rAF await was
papering over no longer clobbers a scroll that lands after a content update.

Two real product regressions: windowless shortcut events stopped pruning the
orphaned main-window context (6d7b23f), and ticket minting read the routes
and the v2 installation identity as two separate cache reads.

No assertion is weakened; several are strengthened.

Rebased onto fix/app-host-green after 87a7016 landed a different remedy for
the same key-window cause. The three focus tests this branch fixed through a
test-target MainWindowKeyStatusPin (a swizzle of NSWindow.isKeyWindow, needed
because CmuxMainWindow is final) are exactly the three that commit fixes by
activating the app host and widening the pump's timeout. The pin is dropped:
activating the host makes isKeyWindow true for real rather than forcing the
answer, and a process-wide swizzle installed for one helper is a worse neighbour
to the rest of the shard. cmuxTests/AppDelegateMainWindowTestingSupport.swift is
no longer touched by this branch.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* test: measure Cloud title ink beyond the icon and trace measured invalidations

Real rendering at 75% and 1x reproduces disconnected icon ink misidentified as the title. Retain the alignment tolerance and verify that displaced text still fails.

Enable DEBUG tracing only during the existing sidebar and minimal-mode measured intervals. Count assertions and timing remain unchanged.

* test: preserve carrier preparation before fleet discovery

* fix: retain independent cloud carrier prewarming

---------

Co-authored-by: austinpower1258 <austinwang115@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@teamleaderleo
teamleaderleo deleted the fix/app-host-shard125 branch September 23, 2026 11:35
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