fix(windows): a window closed during launch stays closed when the restore's workspace is reported late

Merged
LA
4d ago
+208-29

Summary

BackgroundAccessoryPolicyTests.aWindowClosedBeforeTheRestoreFinishesStaysClosed failed 1 run in 3 on cmux12s (job 691124a2e9b4323980de7e07). It is a product race from the WindowManager restore (#23695), not a test fault: a window the person closed during the launch comes back.

Cause: the restore creates its first workspace (FirstWorkspaceLaunch.create). The create's reply can come before the store applies the event that lists the workspace (the store drains once per frame; the reply resumes the restore at once). The restore then ended and cleared closedDuringRestore and closedLaunchWindowID (WindowManager.swift:246-249). The late report's reconcileMembership (WindowManager+Membership.swift:497) found no window and no memory, registered a new fallback window and presented it.

Fix: the memory of what the person closed during the restore is now one value next to WindowRegistry, RestoreClosedWindows (CmuxNextLayout). The restore records the workspace it asked for that the store does not list yet, with the event sequence taken after the create's reply (DaemonConnection.eventSequence, a write barrier). When the person closed the launch window, that window is remembered past the restore: reconcile still gives a late workspace the closed launch window as its fallback and keeps it closed in the same registry step (no sync ever opens it), and the memory ends once each such workspace is in a window, or the store applied past the barrier without listing it (gone). Reconcile settles it before it picks the fallback, so a memory with nothing left to wait for never takes an unrelated workspace. The check is O(the restore's workspaces), runs only in reconcile, and adds no main-thread wait or action-path work.

Test seam: DaemonStore.eventHold (nil in the app) keeps buffered events unapplied while it returns true, and asks for no frame meanwhile; releaseHeldEvents() applies them in that turn. The new test holds the batch with the new workspace until the restore ended, so the failing order is certain, not a race.

Testing

cmux-ci run --class light --display --script scripts/ci/package-test-lane.sh --ref <sha> --arg=suite --arg=Packages/macOS/CmuxNext --arg=<suites> --env CMUX_TEST_REQUIRE_GUI=1 --env CMUX_CI_XCODE_APP=/Applications/Xcode_26.6.app

  • Red, test commit fb2ff7e1bd6 (seam + test, no fix): job 68c59e5646b0d7ed72ed12d6 (cmuxs-Mac-mini-3): aWorkspaceTheRestoreAskedForReportedAfterItEndedStaysInTheClosedLaunchWindow failed at BackgroundAccessoryPolicyTests.swift:170-172: a window was presented, one controller open, the workspace not in a closed window. Its preconditions held (the store listed nothing at the restore's end; the hold caught the report).
  • Green, head ae9ad325179: job 0b1e36cfdd8800c4ceebc9db (cmux12s, one host): RestoreClosedWindowsTests once, then BackgroundAccessoryPolicyTests 5 times in a row: all 6 suites passed (each run: the new test, aWindowClosedBeforeTheRestoreFinishesStaysClosed, noWindowIsAMenuBarApp…).

Observed / Not run

  • Observed: the fleet runs above; the prepush gate (no new violations; CmuxNextApp lines -2).
  • Not run: a full CmuxNextAppTests run; the original 1-in-3 race was not re-run at volume (the new test makes that order deterministic instead).

Owner: no OWNERS.md row covers WindowManager (CmuxNextApp/Windows/WindowManager*.swift), CmuxNextLayout/Windows/ or the Swift DaemonStore (not listed).

Changelog

  • Fixed: a window closed while cmux was still starting no longer opens again when the first workspace arrives late.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes a race where a window the person closed while cmux was still launching could open again when the restore's first workspace was reported late. The closed window now stays closed.

The restore's first workspace create can reply before the store applies the event that lists the workspace; the restore then ended and forgot the closed window, so the late report's membership reconcile opened a new fallback window.

Bug Fixes

  • Replaces closedDuringRestore and closedLaunchWindowID with RestoreClosedWindows, which keeps the closed launch window remembered until the late workspaces land in a window or the store shows them gone.
  • Reconcile settles the memory before picking a fallback, so each late workspace goes to the closed launch window and stays closed in the same registry step.
  • Adds a DaemonStore.eventHold/releaseHeldEvents() test seam and tests that hold the workspace report until the restore ends, making the failing order deterministic instead of a race.