From d9968bcc3d2bc663ef5d71b03879c562d9f6c231 Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Thu, 24 Sep 2026 11:49:30 +0800 Subject: [PATCH] fix: hide all window in TFM default tests --- TFM/App/TermindApp.swift | 43 +++++++++++++++++++-- TFMTests/AgentHostAuthenticationTests.swift | 2 +- TFMTests/AppTestHostTests.swift | 20 ++++++++++ TFMTests/DockMenuTests.swift | 4 +- TFMTests/ExternalRequestTests.swift | 26 ++++++------- TFMTests/MCPServerSessionTests.swift | 2 +- TFMTests/MCPServerToolsTests.swift | 6 +-- TFMTests/SFTPApplicationOpenerTests.swift | 3 +- TFMTests/VisibleWindowTestTrait.swift | 1 + 9 files changed, 82 insertions(+), 25 deletions(-) create mode 100644 TFMTests/AppTestHostTests.swift diff --git a/TFM/App/TermindApp.swift b/TFM/App/TermindApp.swift index afd505d9..5e3487c8 100644 --- a/TFM/App/TermindApp.swift +++ b/TFM/App/TermindApp.swift @@ -13,6 +13,25 @@ import TermindRemoteInfrastructure @main struct TermindApp: App { + static let isRunningTests: Bool = { + #if DEBUG + ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + #else + false + #endif + }() + + static var suppressesAutomaticTestWindows: Bool { + #if DEBUG + guard isRunningTests, ProcessInfo.processInfo.environment["TERMIND_WINDOW_TESTS"] != "1" else { + return false + } + return ScreenshotDemoConfiguration.active == nil + #else + return false + #endif + } + /// Hosts metadata is stored in SwiftData; secrets stay behind Keychain /// references held by the value-type projections used throughout the UI. private let modelContainer: ModelContainer @@ -29,8 +48,7 @@ struct TermindApp: App { launchLanguage = AppLanguage.stored() #endif do { - let isRunningTests = - ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + let isRunningTests = Self.isRunningTests #if DEBUG let usesInMemoryStore = isRunningTests || screenshotDemo != nil #else @@ -188,7 +206,11 @@ struct TermindApp: App { #endif } #if DEBUG - .restorationBehavior(ScreenshotDemoConfiguration.active == nil ? .automatic : .disabled) + // XCTest launches the real app. Gating individual tests does not stop + // SwiftUI from opening or restoring the host's own main window. + .defaultLaunchBehavior(Self.suppressesAutomaticTestWindows ? .suppressed : .automatic) + .restorationBehavior( + Self.isRunningTests || ScreenshotDemoConfiguration.active != nil ? .disabled : .automatic) #endif .defaultSize(app.windowSizeStore.defaultSize) // All main pages share one content floor. Opening the sidebar lets @@ -202,6 +224,10 @@ struct TermindApp: App { .environment(\.locale, launchLanguage.locale) .modelContainer(modelContainer) } + #if DEBUG + .defaultLaunchBehavior(Self.suppressesAutomaticTestWindows ? .suppressed : .automatic) + .restorationBehavior(Self.isRunningTests ? .disabled : .automatic) + #endif .defaultSize(width: 560, height: 240) .windowResizability(.contentSize) // SwiftUI lists every `Window` scene in the Window menu so people can @@ -247,7 +273,9 @@ struct TermindApp: App { weak var app: AppModel? { didSet { guard let app else { return } - guard ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil else { return } + #if DEBUG + guard !TermindApp.isRunningTests else { return } + #endif app.serverMetrics.start() metricsActivity.start(app.serverMetrics) metricsMenu.openMainWindow = { [weak app] in app?.mainWindows.openMetrics() } @@ -321,12 +349,19 @@ struct TermindApp: App { } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + #if DEBUG + // Closing an offscreen fixture must not terminate the test runner. + guard !TermindApp.isRunningTests else { return false } + #endif let menuEnabled = UserDefaults.standard.object(forKey: DesktopSettingsKey.showMetricsInMenuBar) as? Bool ?? true return !menuEnabled && app?.serverMetrics.keepsRunning != true } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + #if DEBUG + guard !TermindApp.suppressesAutomaticTestWindows else { return false } + #endif guard !flag else { return true } return app?.mainWindows.handleReopen() ?? true } diff --git a/TFMTests/AgentHostAuthenticationTests.swift b/TFMTests/AgentHostAuthenticationTests.swift index b58fc3c4..e09a66bd 100644 --- a/TFMTests/AgentHostAuthenticationTests.swift +++ b/TFMTests/AgentHostAuthenticationTests.swift @@ -326,7 +326,7 @@ struct AgentHostAuthenticationTests { #expect(store.key(key.id)?.requiresPassphraseEntry == true) } - @Test + @Test(.requiresVisibleWindow) func savedIdentityWithProtectedKeyRequestsOnlyPassphraseExternally() async throws { let keychain = MemoryHostSecretStore() let reference = "termind.key.\(UUID().uuidString)" diff --git a/TFMTests/AppTestHostTests.swift b/TFMTests/AppTestHostTests.swift new file mode 100644 index 00000000..06b8876b --- /dev/null +++ b/TFMTests/AppTestHostTests.swift @@ -0,0 +1,20 @@ +import AppKit +import Testing + +@testable import Termind + +@MainActor +struct AppTestHostTests { + @Test + func defaultTestHostDoesNotPresentWindowsOrReopenTheMainScene() { + guard TermindApp.suppressesAutomaticTestWindows else { return } + #expect(NSApp.windows.allSatisfy { !$0.isVisible }) + let delegate = AppDelegate() + #expect(!delegate.applicationShouldHandleReopen(NSApp, hasVisibleWindows: false)) + } + + @Test + func closingTheLastFixtureWindowDoesNotTerminateTheTestHost() { + #expect(!AppDelegate().applicationShouldTerminateAfterLastWindowClosed(NSApp)) + } +} diff --git a/TFMTests/DockMenuTests.swift b/TFMTests/DockMenuTests.swift index 5d845298..ab709a50 100644 --- a/TFMTests/DockMenuTests.swift +++ b/TFMTests/DockMenuTests.swift @@ -37,7 +37,7 @@ struct DockMenuTests { #expect(try #require(delegate.applicationDockMenu(NSApp)).items[0].isEnabled == false) } - @Test(arguments: [false, true]) + @Test(.requiresVisibleWindow, arguments: [false, true]) func repeatedDockActionsCreateDistinctWelcomeWindowsWithoutAnExistingWindow(closePreviousWindow: Bool) throws { let app = AppModel(hosts: HostLibraryModel()) let delegate = AppDelegate() @@ -79,7 +79,7 @@ struct DockMenuTests { #expect(secondModel.assistantQuery.isEmpty) } - @Test(arguments: [false, true]) + @Test(.requiresVisibleWindow, arguments: [false, true]) func dockNewWindowPreservesTheExistingPageAndWorkspaceEvenInFullScreen(fullScreen: Bool) throws { let host = Host(name: "Server", host: "server.example") let app = AppModel(hosts: HostLibraryModel(hosts: [host])) diff --git a/TFMTests/ExternalRequestTests.swift b/TFMTests/ExternalRequestTests.swift index 4ab9577d..a7c55ce1 100644 --- a/TFMTests/ExternalRequestTests.swift +++ b/TFMTests/ExternalRequestTests.swift @@ -17,7 +17,7 @@ import struct TermindDomain.Host @Suite(.serialized) @MainActor struct ExternalAuthCoordinatorTests { - @Test func interactiveResponsesAreScopedAndInformationalRoundsComplete() async throws { + @Test(.requiresVisibleWindow) func interactiveResponsesAreScopedAndInformationalRoundsComplete() async throws { let coordinator = ExternalAuthCoordinator() let challenge = SSHKeyboardInteractiveChallenge( connectionID: UUID(), hostID: UUID(), hostname: "server.example", port: 22, @@ -38,7 +38,7 @@ struct ExternalAuthCoordinatorTests { #expect(coordinator.current == nil) } - @Test func stoppingMCPPreservesAppAuthentication() async throws { + @Test(.requiresVisibleWindow) func stoppingMCPPreservesAppAuthentication() async throws { let coordinator = ExternalAuthCoordinator() let challenge = SSHKeyboardInteractiveChallenge( connectionID: UUID(), hostID: UUID(), hostname: "server.example", port: 22, @@ -89,7 +89,7 @@ struct ExternalAuthCoordinatorTests { #expect(window.standardWindowButton(.zoomButton)?.isHidden == false) } - @Test + @Test(.requiresVisibleWindow) func popupQueueResumesInFIFOOrder() async throws { let coordinator = ExternalAuthCoordinator() let runID = UUID() @@ -117,7 +117,7 @@ struct ExternalAuthCoordinatorTests { #expect(coordinator.pending.isEmpty) } - @Test + @Test(.requiresVisibleWindow) func protectedKeyCanRaisePassphraseImmediatelyAfterIdentity() async throws { let coordinator = ExternalAuthCoordinator() let handler = coordinator.handler(clientName: "Codex", runID: UUID()) @@ -170,7 +170,7 @@ struct ExternalAuthCoordinatorTests { #expect(coordinator.pending.isEmpty) } - @Test + @Test(.requiresVisibleWindow) func closingThePopupCancelsEveryWaiter() async throws { let coordinator = ExternalAuthCoordinator() let task = Task { @@ -186,7 +186,7 @@ struct ExternalAuthCoordinatorTests { #expect(coordinator.pending.isEmpty) } - @Test + @Test(.requiresVisibleWindow) func staleWindowDisappearanceCannotCancelANewerPassphraseRequest() async throws { let coordinator = ExternalAuthCoordinator() let firstRequest = authenticationRequest(name: "First") @@ -226,7 +226,7 @@ struct ExternalAuthCoordinatorTests { #expect(coordinator.pending.isEmpty) } - @Test + @Test(.requiresVisibleWindow) func secretRequestsUseTheSamePopupQueueAndRejectOrdinaryQuestions() async throws { let coordinator = ExternalAuthCoordinator() let runID = UUID() @@ -257,7 +257,7 @@ struct ExternalAuthCoordinatorTests { ) } - @Test + @Test(.requiresVisibleWindow) func cancellingAQueuedSessionDoesNotRepresentTheCurrentPopup() async throws { let coordinator = ExternalAuthCoordinator() let currentRunID = UUID() @@ -286,7 +286,7 @@ struct ExternalAuthCoordinatorTests { #expect(await current.value == .cancelled) } - @Test + @Test(.requiresVisibleWindow) func visibleSecretTimesOutAndLeavesNoPendingPopup() async throws { let coordinator = ExternalAuthCoordinator() let request = secretRequest(timeout: .milliseconds(40)) @@ -299,7 +299,7 @@ struct ExternalAuthCoordinatorTests { #expect(coordinator.pending.isEmpty) } - @Test + @Test(.requiresVisibleWindow) func queuedSecretDoesNotSpendItsTimeoutBehindSSHPrompt() async throws { let coordinator = ExternalAuthCoordinator() let sshRequest = authenticationRequest(name: "Current") @@ -324,7 +324,7 @@ struct ExternalAuthCoordinatorTests { #expect(coordinator.pending.isEmpty) } - @Test + @Test(.requiresVisibleWindow) func answeredSecretCannotLaterRetireANewerPopup() async throws { let coordinator = ExternalAuthCoordinator() let request = secretRequest(timeout: .milliseconds(40)) @@ -355,7 +355,7 @@ struct ExternalAuthCoordinatorTests { #expect(await ssh.value == .cancelled) } - @Test + @Test(.requiresVisibleWindow) func hostKeyUsesPopupQueueAndReturnsSelectedTrustScope() async throws { let coordinator = ExternalAuthCoordinator() let request = hostKeyRequest() @@ -376,7 +376,7 @@ struct ExternalAuthCoordinatorTests { #expect(coordinator.pending.isEmpty) } - @Test + @Test(.requiresVisibleWindow) func closingHostKeyPopupDeniesTheWaitingCall() async throws { let coordinator = ExternalAuthCoordinator() let task = Task { diff --git a/TFMTests/MCPServerSessionTests.swift b/TFMTests/MCPServerSessionTests.swift index 10fc775c..7ca7bd1f 100644 --- a/TFMTests/MCPServerSessionTests.swift +++ b/TFMTests/MCPServerSessionTests.swift @@ -208,7 +208,7 @@ struct MCPServerSessionTests { #expect(message.contains("\"id\":1")) } - @Test + @Test(.requiresVisibleWindow) func deletingASessionCancelsItsSecretPopupWithoutResurrectingTheSession() async throws { let authentication = ExternalAuthCoordinator() let protocolServer = MCPServerProtocol( diff --git a/TFMTests/MCPServerToolsTests.swift b/TFMTests/MCPServerToolsTests.swift index 4b54d3e4..0ef63d07 100644 --- a/TFMTests/MCPServerToolsTests.swift +++ b/TFMTests/MCPServerToolsTests.swift @@ -190,7 +190,7 @@ struct MCPServerToolsTests { #expect(!result.isError) } - @Test + @Test(.requiresVisibleWindow) func secretPromptReturnsOnlyASessionBoundOneShotHandle() async throws { let authentication = ExternalAuthCoordinator() let server = makeMCPServerTools(externalAuthentication: authentication) @@ -251,7 +251,7 @@ struct MCPServerToolsTests { } } - @Test + @Test(.requiresVisibleWindow) func timedOutSecretPromptReturnsAnErrorAndIssuesNoHandle() async throws { let authentication = ExternalAuthCoordinator() let server = makeMCPServerTools(externalAuthentication: authentication) @@ -286,7 +286,7 @@ struct MCPServerToolsTests { #expect(await broker.count == 0) } - @Test + @Test(.requiresVisibleWindow) func endingASessionCancelsItsPromptAndDiscardsSecrets() async throws { let authentication = ExternalAuthCoordinator() let server = makeMCPServerTools(externalAuthentication: authentication) diff --git a/TFMTests/SFTPApplicationOpenerTests.swift b/TFMTests/SFTPApplicationOpenerTests.swift index d79a64d8..4b443667 100644 --- a/TFMTests/SFTPApplicationOpenerTests.swift +++ b/TFMTests/SFTPApplicationOpenerTests.swift @@ -41,7 +41,8 @@ struct SFTPApplicationOpenerTests { #expect(SFTPApplicationOpener.appStoreSearchURL(for: "README") == nil) } - @Test func missingApplicationPropagatesLaunchFailure() async throws { + // Launch Services may show a system alert even though this open request is expected to fail. + @Test(.requiresVisibleWindow) func missingApplicationPropagatesLaunchFailure() async throws { let directory = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: directory) } diff --git a/TFMTests/VisibleWindowTestTrait.swift b/TFMTests/VisibleWindowTestTrait.swift index e555ac52..804ef4e8 100644 --- a/TFMTests/VisibleWindowTestTrait.swift +++ b/TFMTests/VisibleWindowTestTrait.swift @@ -3,6 +3,7 @@ import Testing extension Trait where Self == ConditionTrait { /// Window presentation and focus changes require an explicit opt-in, even on a machine with a display. + /// This includes Dock actions, external authentication requests, and Launch Services alerts. static var requiresVisibleWindow: Self { .enabled( if: ProcessInfo.processInfo.environment["TERMIND_WINDOW_TESTS"] == "1",