From 2f1b813cba12cd1e735a007c8dc38efaf5acfdd0 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Mon, 5 Oct 2026 14:57:58 -0700 Subject: [PATCH] test(cloud): follow the Coderouter section and the Cloud sheet wrapper Main's full CI at 1c84448 (run 37374680247) failed three suites on test drift, which the window crash loop fixed in #17433 had been hiding: - CloudTreeAvailabilityTests:23/:47 and MachineCreateOptimisticProjectionTests:89: #17233 always appends the Coderouter section, so exact tree IDs and node counts gained it. - CloudCreateTeamSheetPresenterTests:101: #17422 hosts every Cloud sheet in CloudSheetWindow's CloudSheetContent wrapper, so the window no longer has an NSHostingController. Find the window by its cmux.cloudCreateTeam identifier and read the sheet through the wrapper, which is now internal (its size reporter stays fileprivate). Co-Authored-By: Claude Opus 5.5 --- Sources/Cloud/CloudSheetWindow.swift | 7 +++-- .../CloudCreateTeamSheetPresenterTests.swift | 31 ++++++++++++------- cmuxTests/CloudTreeAvailabilityTests.swift | 5 +-- ...chineCreateOptimisticProjectionTests.swift | 4 +-- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/Sources/Cloud/CloudSheetWindow.swift b/Sources/Cloud/CloudSheetWindow.swift index c7e70e67362d..5988683966bf 100644 --- a/Sources/Cloud/CloudSheetWindow.swift +++ b/Sources/Cloud/CloudSheetWindow.swift @@ -95,10 +95,11 @@ final class CloudSheetWindow { /// Lays the content out at its ideal height, pinned to the top, and reports /// that size. The ideal height does not depend on the window's height, so a -/// resize never changes what is reported. -private struct CloudSheetContent: View { +/// resize never changes what is reported. Internal so tests can reach the +/// content's actions through the hosting controller. +struct CloudSheetContent: View { let content: Content - let report: CloudSheetWindow.SizeReporter + fileprivate let report: CloudSheetWindow.SizeReporter var body: some View { content diff --git a/cmuxTests/CloudCreateTeamSheetPresenterTests.swift b/cmuxTests/CloudCreateTeamSheetPresenterTests.swift index 73a3f2e24b3f..028cbb56a64e 100644 --- a/cmuxTests/CloudCreateTeamSheetPresenterTests.swift +++ b/cmuxTests/CloudCreateTeamSheetPresenterTests.swift @@ -23,11 +23,11 @@ struct CloudCreateTeamSheetPresenterTests { let window = try sheetWindow(excluding: existingWindows) defer { close(window) } - let sheet = try #require(window.contentViewController as? NSHostingController) + let sheet = try teamSheet(in: window) #expect(window.isVisible || window.sheetParent != nil) // Cancel's action. - sheet.rootView.onCancel() + sheet.onCancel() try await waitUntilClosed(window) #expect(window.sheetParent == nil, "The sheet stayed attached after Cancel.") @@ -46,9 +46,9 @@ struct CloudCreateTeamSheetPresenterTests { let window = try sheetWindow(excluding: existingWindows) defer { close(window) } - let sheet = try #require(window.contentViewController as? NSHostingController) - sheet.rootView.onCreate("Launch Crew") - sheet.rootView.onCreate("Launch Crew") + let sheet = try teamSheet(in: window) + sheet.onCreate("Launch Crew") + sheet.onCreate("Launch Crew") try await waitUntilClosed(window) #expect(created == ["Launch Crew"]) @@ -64,7 +64,7 @@ struct CloudCreateTeamSheetPresenterTests { presenter.present(accountFlow: flow) { created.append($0) } let window = try sheetWindow(excluding: existingWindows) defer { close(window) } - let oldSheet = try #require(window.contentViewController as? NSHostingController) + let oldSheet = try teamSheet(in: window) #expect(window.sheetParent == nil, "The regression must exercise the standalone window.") #expect(window.isVisible) @@ -74,7 +74,7 @@ struct CloudCreateTeamSheetPresenterTests { try await waitUntilClosed(window) #expect(window.sheetParent == nil) #expect(!window.isVisible) - oldSheet.rootView.onCreate("Closed Team") + oldSheet.onCreate("Closed Team") #expect(created.isEmpty) presenter.present(accountFlow: flow) { created.append($0) } @@ -86,13 +86,13 @@ struct CloudCreateTeamSheetPresenterTests { // Late actions from the closed session must not dismiss or submit the // next presentation. - oldSheet.rootView.onCancel() - oldSheet.rootView.onCreate("Stale Team") + oldSheet.onCancel() + oldSheet.onCreate("Stale Team") #expect(reopened.isVisible) #expect(created.isEmpty) - let newSheet = try #require(reopened.contentViewController as? NSHostingController) - newSheet.rootView.onCreate("Launch Crew") + let newSheet = try teamSheet(in: reopened) + newSheet.onCreate("Launch Crew") #expect(!reopened.isVisible) #expect(created == ["Launch Crew"]) } @@ -100,10 +100,17 @@ struct CloudCreateTeamSheetPresenterTests { private func sheetWindow(excluding existingWindows: Set) throws -> NSWindow { try #require(NSApp.windows.first { !existingWindows.contains(ObjectIdentifier($0)) - && $0.contentViewController is NSHostingController + && $0.identifier?.rawValue == "cmux.cloudCreateTeam" }) } + /// The sheet view inside the `CloudSheetWindow` sizing wrapper. + private func teamSheet(in window: NSWindow) throws -> CloudCreateTeamSheet { + try #require( + window.contentViewController as? NSHostingController> + ).rootView.content + } + private func waitUntilClosed(_ window: NSWindow) async throws { let deadline = ContinuousClock.now + .seconds(2) while window.isVisible || window.sheetParent != nil, ContinuousClock.now < deadline { diff --git a/cmuxTests/CloudTreeAvailabilityTests.swift b/cmuxTests/CloudTreeAvailabilityTests.swift index ed3e5d24ad82..0d5036743fca 100644 --- a/cmuxTests/CloudTreeAvailabilityTests.swift +++ b/cmuxTests/CloudTreeAvailabilityTests.swift @@ -20,7 +20,8 @@ struct CloudTreeAvailabilityTests { ) // The link placeholder leads; Ports stays reachable, and Resources is // always the final machine section. - #expect(CloudTreeNodeBuilder.flattened(asleep).map(\.id) == ["machine:quiet-owl", "machine:quiet-owl/placeholder", "machine:quiet-owl/ports", "machine:quiet-owl/ports/status", "machine:quiet-owl/resources", "machine:quiet-owl/resources/cpu", "machine:quiet-owl/resources/memory", "machine:quiet-owl/resources/disk", "machine:quiet-owl/resources/usage"]) + // The Coderouter section always closes the tree (#17233). + #expect(CloudTreeNodeBuilder.flattened(asleep).map(\.id) == ["machine:quiet-owl", "machine:quiet-owl/placeholder", "machine:quiet-owl/ports", "machine:quiet-owl/ports/status", "machine:quiet-owl/resources", "machine:quiet-owl/resources/cpu", "machine:quiet-owl/resources/memory", "machine:quiet-owl/resources/disk", "machine:quiet-owl/resources/usage", "coderouter-section", "coderouter-section/codex", "coderouter-section/claude", "coderouter-section/opencode-go"]) if case .placeholder(_, let placeholder) = asleep[0].children[0].kind { #expect(placeholder.style == .dimmed) } else { Issue.record("Unexpected node kind") } if case .placeholder(_, let ports) = asleep[0].children[1].children[0].kind { #expect(ports.style == .dimmed) } else { Issue.record("Unexpected node kind") } @@ -44,7 +45,7 @@ struct CloudTreeAvailabilityTests { snapshot: SurfaceCatalogSnapshot(machines: [machineInfo(.cloud("ghost"))], resources: [], projections: []), localWorkspaces: [] ) - #expect(catalogOnly.map(\.id) == ["machine:ghost"]) + #expect(catalogOnly.map(\.id) == ["machine:ghost", "coderouter-section"]) } @Test diff --git a/cmuxTests/MachineCreateOptimisticProjectionTests.swift b/cmuxTests/MachineCreateOptimisticProjectionTests.swift index 828e3b519fdd..ead621d340aa 100644 --- a/cmuxTests/MachineCreateOptimisticProjectionTests.swift +++ b/cmuxTests/MachineCreateOptimisticProjectionTests.swift @@ -86,8 +86,8 @@ struct MachineCreateOptimisticProjectionTests { let refreshed = CloudTreeNodeBuilder.nodes( machines: [machine], pendingCreates: coordinator.operations, snapshot: .empty, localWorkspaces: [] ) - #expect(refreshed.count == 1) - #expect(refreshed.first?.id == selectedID) + // The Coderouter section always follows the machines (#17233). + #expect(refreshed.map(\.id) == [selectedID, "coderouter-section"]) #expect(coordinator.hasRunningOperations) }