The titlebar stand-in stops reappearing on a window that attaches after the board has loaded
`configureWindow` removed the loading stand-in from whatever window it happened to be running against, but left `onAttach` installed exactly as `configureLoadingWindow` set it — a closure that builds a fresh stand-in on every attach, deliberately, so it survives the provisional-window swap while the board is still loading. SwiftUI's dismantle-then- make swap has no deadline, though: when it lands after the store has already loaded, the stale closure reinstalls a stand-in on the real window a moment before `attach`'s own `addTitlebarAccessoryIfPossible()` installs the real widget beside it — both in the titlebar, stand-in leading, and nothing left holding a reference to remove it a second time. `onAttach` is now replaced in `configureWindow` too, keeping only the frame- placement half a re-attached window still needs. New suite `BoardLoadingTitlebarSwapRaceTests` drives `HostedWindowController` through the same attach sequence and pins both the fixed and (as a negative control) the unfixed shape. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -243,3 +243,121 @@ struct BoardLoadingTitlebarStandInTests {
|
||||
#expect(window.titlebarAccessoryViewControllers.count == 1, "nothing to remove, nothing removed")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The reopened bug: a late provisional-to-real swap
|
||||
|
||||
/// **Pipeline card a73bad86, reopened 2026-08-09**: the removal above is not the whole fix.
|
||||
/// `configureWindow` removed the stand-in from *the window it happened to run against*, but left
|
||||
/// `windowController.onAttach` exactly as `configureLoadingWindow` had installed it — a closure that
|
||||
/// builds a *fresh* stand-in and adds it to whatever window attaches next, deliberately, so it
|
||||
/// survives the provisional-window swap while the board is still loading
|
||||
/// (`HostedWindowController.detach`'s own doc comment: "install arrives before any window, a
|
||||
/// dismantle follows, and only then does the real window attach").
|
||||
///
|
||||
/// The trouble is that swap has no deadline. When it lands *after* the store has already loaded and
|
||||
/// `configureWindow` has already run, the stale closure is still the one `HostedWindowController
|
||||
/// .attach` calls — it reinstalls a stand-in on the real window a moment before `attach`'s own
|
||||
/// `addTitlebarAccessoryIfPossible()` installs the real widget beside it. Both sit in the titlebar,
|
||||
/// stand-in leading — exactly the owner's screenshot — and nothing left with a reference to the
|
||||
/// stand-in ever removes it. "Occasionally" was this ordering: whenever the load wins the race
|
||||
/// against the swap.
|
||||
///
|
||||
/// `BoardWindowHost` is a SwiftUI view struct with no seam of its own to call `configureWindow`
|
||||
/// directly, so this drives `HostedWindowController` through the same sequence that method does —
|
||||
/// `configureLoadingWindow`'s attach, then the fixed `configureWindow`'s replacement of both the
|
||||
/// stand-in and `onAttach` itself, then a late attach standing in for the delayed swap — the same
|
||||
/// structural level `BoardLoadingTitlebarStandInTests` above tests at.
|
||||
@MainActor
|
||||
@Suite("Board loading state ▸ the provisional-window swap race")
|
||||
struct BoardLoadingTitlebarSwapRaceTests {
|
||||
|
||||
private static func window() -> NSWindow {
|
||||
NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 600, height: 400),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||
backing: .buffered,
|
||||
defer: true
|
||||
)
|
||||
}
|
||||
|
||||
@Test("A late provisional-to-real attach installs only the real widget, never a second stand-in")
|
||||
func lateAttachAfterLoadDoesNotReinstallTheStandIn() {
|
||||
let controller = HostedWindowController()
|
||||
var loadingAccessory: NSTitlebarAccessoryViewController?
|
||||
|
||||
// `configureLoadingWindow`'s own closure, verbatim: a fresh stand-in per attach, held so a
|
||||
// later removal can find it by identity.
|
||||
controller.onAttach = { window in
|
||||
let accessory = boardLoadingTitlebarAccessory(title: "Roadmap")
|
||||
loadingAccessory = accessory
|
||||
window.addTitlebarAccessoryViewController(accessory)
|
||||
}
|
||||
|
||||
// The loading window the user sees first — the provisional window's attach.
|
||||
let provisional = Self.window()
|
||||
controller.attach(to: provisional)
|
||||
#expect(provisional.titlebarAccessoryViewControllers.count == 1, "the stand-in, and only the stand-in")
|
||||
|
||||
// The load finishes before the real-window swap arrives: `configureWindow`'s own sequence,
|
||||
// this suite's fix included — removal by identity, then `onAttach` replaced rather than left
|
||||
// standing.
|
||||
let realWidget = boardLoadingTitlebarAccessory(title: "Roadmap")
|
||||
if let loadingAccessory,
|
||||
let index = provisional.titlebarAccessoryViewControllers.firstIndex(where: { $0 === loadingAccessory }) {
|
||||
provisional.removeTitlebarAccessoryViewController(at: index)
|
||||
}
|
||||
loadingAccessory = nil
|
||||
controller.installTitlebarAccessory(realWidget)
|
||||
// The fix under test: `onAttach` keeps only the frame-placement half a re-attached window
|
||||
// still needs — no stand-in built or installed for anything that attaches from here on.
|
||||
controller.onAttach = { _ in }
|
||||
|
||||
#expect(provisional.titlebarAccessoryViewControllers.count == 1, "the real widget replaced the stand-in")
|
||||
#expect(provisional.titlebarAccessoryViewControllers.first === realWidget)
|
||||
|
||||
// SwiftUI's dismantle-then-make swap, landing *after* the load — the exact ordering the
|
||||
// reopened bug depended on. `detach()` releases the provisional window without discarding the
|
||||
// held chrome; the real window's `attach` reinstalls it.
|
||||
controller.detach()
|
||||
let real = Self.window()
|
||||
controller.attach(to: real)
|
||||
|
||||
#expect(
|
||||
real.titlebarAccessoryViewControllers.count == 1,
|
||||
"no stand-in reappears on a window that attaches after the board has already loaded"
|
||||
)
|
||||
#expect(real.titlebarAccessoryViewControllers.first === realWidget, "only the real widget carries over")
|
||||
}
|
||||
|
||||
@Test("Without the fix, the same late attach reinstalls a stand-in beside the real widget")
|
||||
func theUnfixedClosureReproducesTheDuplicate() {
|
||||
// The regression's own negative space: this pins that the scenario above is not vacuously
|
||||
// true by construction — with `configureWindow` never replacing `onAttach` (the bug this
|
||||
// card reopened over), the same late swap really does leave two accessories installed.
|
||||
let controller = HostedWindowController()
|
||||
var loadingAccessory: NSTitlebarAccessoryViewController?
|
||||
controller.onAttach = { window in
|
||||
let accessory = boardLoadingTitlebarAccessory(title: "Roadmap")
|
||||
loadingAccessory = accessory
|
||||
window.addTitlebarAccessoryViewController(accessory)
|
||||
}
|
||||
|
||||
let provisional = Self.window()
|
||||
controller.attach(to: provisional)
|
||||
|
||||
let realWidget = boardLoadingTitlebarAccessory(title: "Roadmap")
|
||||
if let loadingAccessory,
|
||||
let index = provisional.titlebarAccessoryViewControllers.firstIndex(where: { $0 === loadingAccessory }) {
|
||||
provisional.removeTitlebarAccessoryViewController(at: index)
|
||||
}
|
||||
loadingAccessory = nil
|
||||
controller.installTitlebarAccessory(realWidget)
|
||||
// `onAttach` deliberately left untouched — the pre-fix state.
|
||||
|
||||
controller.detach()
|
||||
let real = Self.window()
|
||||
controller.attach(to: real)
|
||||
|
||||
#expect(real.titlebarAccessoryViewControllers.count == 2, "the bug: a fresh stand-in beside the real widget")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user