test: keep saved window and sidebar state from leaking between app-host and UI tests

AU
16d ago
+301-37

App-host and UI tests inherited window and sidebar state from earlier tests and earlier jobs on the same Mac. Since split admission (#15392) refuses a split that would leave a pane narrower than 160 pt, which tests had room to split depended on shard packing and on which fleet Mac ran the shard. Each test now starts from the app's defaults and gives back what it found. This is the split-failure cluster in #15488.

Two leaks

Within one app-host process. When a main window closes, it saves its frame as the last window geometry. A new window with no source window opens at that size, and at the right sidebar's last saved visibility, width and mode. AppDelegateShortcutRoutingTests.testCmdShiftNCreatesWindowFromEventWindowWithoutAddingWorkspace closes a 560 pt window. That leaves every later window a 320 pt terminal area beside the 240 pt sidebar, one point short of a horizontal split. An instrumented rerun at 478e3232b showed this directly: a fresh window opened at the saved 640×512 frame, and splitSpaceVerdict turned noSpace after one split.

Across jobs on a fleet Mac. The per-run home (HOME, CFFIXED_USER_HOME) does not move an app's preferences. cfprefsd keeps them in the runner account's own com.cmuxterm.app.debug domain, shared by every job and kept across jobs. A local probe confirmed it: a defaults write under a fake CFFIXED_USER_HOME landed in the real domain. Reading that domain on the minis (ssh cmux@<mini> defaults export com.cmuxterm.app.debug -) found 92–124 keys left by earlier runs:

Macright sidebarother
cmux10sshown, Sessionssaved 1440×900 frame
cmux7sshown, Docksaved 1000×700 frame
cmux9sshown, Files75% terminal font magnification
cmux13sshown, Sessionstmux overlay experiment on

A new window on those Macs lost 276 pt to the right sidebar. On cmux7s the sidebar also created a Dock, which is why testWorkspaceTerminalFontSizeShortcutSeedsDockCreatedAfterShortcut found one already there. The font magnification is why CloudRestoreReplayGridTests and TerminalFontZoomSessionPersistenceTests failed only on cmux9s and cmux14, with sizes off by exactly the magnification. The same saved state reached BrowserPaneNavigationKeybindUITests: in run 36680987910 Cmd+D recorded lastSplitDirection right with paneCountAfterSplit 1.

What changes

  • In-process isolation. MainWindowDefaultsIsolation removes the saved window geometry and the right sidebar's fileExplorer.isVisible, fileExplorer.width and rightSidebar.mode for the length of a test, then puts back what was there.
    • CmuxTestsPrincipal applies it to every XCTest case.
    • The 37 Swift Testing suites that open main windows take .isolatedMainWindowDefaults. The trait does its defaults writes on the main actor. A defaults write waits for observers registered on the main queue, so a background write that holds the isolation lock could wait on a main thread that is waiting for that lock.
    • It writes only keys whose value changed, and removes only keys that are present. Every defaults write posts UserDefaults.didChangeNotification to every observer in the process, even for an identical value. A test that never touches these keys now writes nothing.
  • Launch-time reset. scripts/ci/reset-app-defaults.sh deletes the DEV build's own defaults domain before each isolated app-host batch (run-app-host-xcodebuild.sh) and before each display UI regression.
    • It never touches a release bundle's domain, and it does nothing outside GitHub Actions, where it would wipe the settings of a tagged build under test.
    • Both callers hold the Mac's GUI token, so no other test run is using the domain.
    • It is listed in choose_ci_suite.py's app-host consumers, so a PR that edits it runs app-host tests.

Nothing here changes app code or admission behavior.

Evidence

The new XCTest pair in AppDelegateBareSpaceShortcutRoutingTests reproduces the in-process leak deterministically. The first test closes a 560 pt window; the second, which XCTest runs next by name order, must still split. Both runs use the app build from full-suite run 36685498203, with 5 iterations, via app-host-test-rerun.yml:

  • Red, test commit only: run 36706506679. testWindowGeometryIsolation2NextTestWindowStillSplits failed 5 of 5 iterations ("a window opened at an earlier test's 560 pt size has no room for a split").
  • Green, with isolation and the launch reset: run 36709314234. Every selected test passed 5 of 5, including the pair and the original split failures (RemoteTmuxMirrorSplitRoutingTests, AppDelegateEqualizeSplitsShortcutTests, WorkspaceManualUnreadTests). It ran on cmux10s-mac-mini-glaeda-2, where shard 1 failed those tests in each of main's last three full-suite runs, and logged Cleared the persistent defaults earlier runs left in com.cmuxterm.app.debug.
  • No collateral failures: shard 1's two failing batches from run 36685498203, replayed in full with the isolation, passed: batch 0 and batch 1.

testDefaultsKeyIsolationRestoresOnlyChangedKeys checks the isolation directly against a suite domain that records writes. A scope over absent keys writes nothing, nested scopes restore once, and only changed keys are written back. The review follow-ups, rebased onto the validation build, are running as run 36720211217: the whole AppDelegateBareSpaceShortcutRoutingTests class, RemoteTmuxMirrorRenameTests, AppDelegateEqualizeSplitsShortcutTests and RemoteTmuxMirrorSplitRoutingTests, 3 iterations each.

scripts/ci/reset-app-defaults.sh was exercised locally against probe domains. With GITHUB_ACTIONS=true and a stubbed defaults, a DEV-pattern domain is cleared and a second run is a no-op, a release-pattern domain is left alone, an app found only by the find fallback is cleared, and a missing product exits 0. Without GITHUB_ACTIONS it changes nothing. tests/test_ci_change_areas.py passes with the new consumer path. The app-host wrapper guards pass locally: test_ci_app_host_xcodebuild_retry.sh, test_ci_app_host_xcodebuild_attempts.sh, test_ci_app_host_home_isolation.py and test_ci_app_host_pipe_capture.py.

The cross-job leak needs a fleet Mac with that saved state; the green run above is one. The full-suite validation of the #15488 repair (#15960) runs every shard on the minis.

#15985 clears the saved frame when a test process launches, which also covers the launch window in local runs. It does not cover what one test leaves for the next test in the same process, or the sidebar and font state, so the two PRs complement each other. Both add a test at the same spot in AppDelegateBareSpaceShortcutRoutingTests.swift; whichever lands second keeps both. #15919, which sized two split fixtures by hand, was closed in favor of #15985.

Changelog

  • none

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests
    • Expanded coverage for window geometry persistence, terminal splitting, preference restoration, and window-close routing.
    • Added consistent isolation of window preferences across app and shortcut test suites to reduce interference between test cases.
  • Chores
    • CI test runs now clear app preferences before selected runs, helping keep test results consistent across repeated executions.