diff --git a/AGENTS.md b/AGENTS.md index da8326e8..a65ab054 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ Treat `Memory/` as durable engineering context. Read `ARCHITECTURE.md` before ch - `xcodebuild -project Termind.xcodeproj -scheme TFM -destination 'platform=macOS' -onlyUsePackageVersionsFromResolvedFile build` builds the app, the MCP bridge, and AgentKit. Keep the flag: resolving without it silently bumps pinned dependencies. - `xcodebuild -project Termind.xcodeproj -scheme TFI -destination 'generic/platform=iOS Simulator' -onlyUsePackageVersionsFromResolvedFile build` builds the mobile app and shared frameworks. Run its tests with an available iPhone or iPad simulator destination. - `cd ../AgentKit && swift build && swift test` builds and tests the agent package on its own. -- `xcodebuild -project Termind.xcodeproj -scheme TFM -destination 'platform=macOS' -onlyUsePackageVersionsFromResolvedFile test` runs all Swift tests. +- `xcodebuild -project Termind.xcodeproj -scheme TFM -destination 'platform=macOS' -onlyUsePackageVersionsFromResolvedFile test` runs the default macOS and shared Swift tests. Tests that show windows or take focus are opt-in. - `xcrun swift-format lint --strict --recursive Frameworks TFM TFI FrameworkTests TFMTests TFITests TermindMCPBridge` checks Swift formatting. AgentKit passes the same command over `Sources Tests` and must keep passing it. - `cd LandingPage && npm ci && npm run dev` installs locked web dependencies and starts Astro locally. - `cd LandingPage && npm run build` type-checks and emits the production site to `LandingPage/dist/`. @@ -39,7 +39,9 @@ Follow `.swift-format`: four spaces and 120 columns. Use PascalCase types, camel ## Testing Guidelines -Use Swift Testing (`@Suite`, `@Test`, `#expect`) in the three test directories. Add regression coverage and run all tests before submission. Web changes must pass `npm run build`; verify English and `/zh/` routes visually. +Use Swift Testing (`@Suite`, `@Test`, `#expect`) in the three test directories. Add regression coverage and run the default test suite before submission. Web changes must pass `npm run build`; verify English and `/zh/` routes visually. + +macOS tests that present windows or activate the app must use `.requiresVisibleWindow` on their suite or individual test. They are skipped by default; keep this opt-in off for routine verification. To opt in from the command line, prefix the test command with `TEST_RUNNER_TERMIND_WINDOW_TESTS=1` (Xcode forwards it as `TERMIND_WINDOW_TESTS=1` to the test host), preferably with `-only-testing:TFMTests/` to limit the run. In Xcode, set `TERMIND_WINDOW_TESTS=1` in the test action's environment. Screenshot tests with existing dedicated opt-in variables keep those gates. Offscreen layout and logic tests remain enabled by default. ## Localization Guidelines diff --git a/TFMTests/BroadcastInputTests.swift b/TFMTests/BroadcastInputTests.swift index fbabe262..41fbeb3a 100644 --- a/TFMTests/BroadcastInputTests.swift +++ b/TFMTests/BroadcastInputTests.swift @@ -324,6 +324,7 @@ struct BroadcastInputTests { #if canImport(AppKit) @Test( + .requiresVisibleWindow, .disabled( if: !ghosttyHasActiveDisplay, "Ghostty's Metal surface requires an active display" @@ -485,6 +486,7 @@ struct BroadcastInputTests { } @Test( + .requiresVisibleWindow, .disabled( if: !ghosttyHasActiveDisplay, "Ghostty's Metal surface requires an active display" diff --git a/TFMTests/ChooserInteractionTests.swift b/TFMTests/ChooserInteractionTests.swift index 493f11dc..44f4b211 100644 --- a/TFMTests/ChooserInteractionTests.swift +++ b/TFMTests/ChooserInteractionTests.swift @@ -9,7 +9,7 @@ import struct TermindDomain.Host @testable import Termind -@Suite(.serialized) @MainActor +@Suite(.requiresVisibleWindow, .serialized) @MainActor struct ChooserInteractionTests { @Test(arguments: ["toolbar", "ssh", "serial"]) func quickOpenTakesFocusAndHandlesTypingNavigationAndReopening(_ source: String) async throws { @@ -230,12 +230,12 @@ private final class ChooserFixture { print( "CHOOSER VIEWS", views.map { - "\(type(of: $0)): \(($0 as? NSButton)?.title ?? "") \($0.accessibilityIdentifier() ?? "")" + "\(Swift.type(of: $0)): \(($0 as? NSButton)?.title ?? "") \($0.accessibilityIdentifier())" }) print( "CHOOSER AX", elements.map { - "\(type(of: $0)): \($0.accessibilityLabel?() ?? "") \($0.accessibilityIdentifier?() ?? "")" + "\(Swift.type(of: $0)): \($0.accessibilityLabel?() ?? "") \($0.accessibilityIdentifier?() ?? "")" }) } return try #require(elements.first { $0.accessibilityIdentifier?() == identifier }, "Missing \(identifier)") diff --git a/TFMTests/HostIntegrationRenderingTests.swift b/TFMTests/HostIntegrationRenderingTests.swift index 850d401e..e51bc614 100644 --- a/TFMTests/HostIntegrationRenderingTests.swift +++ b/TFMTests/HostIntegrationRenderingTests.swift @@ -6,7 +6,7 @@ import Testing @testable import Termind -@Suite(.serialized) @MainActor +@Suite(.requiresVisibleWindow, .serialized) @MainActor struct HostIntegrationRenderingTests { @Test(arguments: ["en", "zh-Hans", "zh-Hant"], [320.0, 360.0, 480.0]) func hostEditorShowsIntegrationBelowGroupingAtNarrowWidth(language: String, width: CGFloat) async throws { diff --git a/TFMTests/MemorySettingsRenderingTests.swift b/TFMTests/MemorySettingsRenderingTests.swift index ef28e884..cbdd7a25 100644 --- a/TFMTests/MemorySettingsRenderingTests.swift +++ b/TFMTests/MemorySettingsRenderingTests.swift @@ -8,7 +8,7 @@ import Testing @testable import Termind -@Suite(.serialized) @MainActor +@Suite(.requiresVisibleWindow, .serialized) @MainActor struct MemorySettingsRenderingTests { @Test(arguments: ["en", "zh-Hans", "zh-Hant"], [false, true]) func memorySettingsRenderWithCapacity(_ language: String, dark: Bool) async throws { diff --git a/TFMTests/MetricsDetailToolbarTests.swift b/TFMTests/MetricsDetailToolbarTests.swift index 0f955d71..8124282d 100644 --- a/TFMTests/MetricsDetailToolbarTests.swift +++ b/TFMTests/MetricsDetailToolbarTests.swift @@ -8,7 +8,7 @@ import Testing @testable import Termind @MainActor -@Suite(.serialized) +@Suite(.requiresVisibleWindow, .serialized) struct MetricsDetailToolbarTests { @Test(arguments: [AppLanguage.english, .simplifiedChinese, .traditionalChinese]) func compactToolbarSwitchesPagesAndSearchRevealsProcesses(language: AppLanguage) async throws { diff --git a/TFMTests/MetricsJourneyRenderingTests.swift b/TFMTests/MetricsJourneyRenderingTests.swift index 02814a49..6cb9794a 100644 --- a/TFMTests/MetricsJourneyRenderingTests.swift +++ b/TFMTests/MetricsJourneyRenderingTests.swift @@ -8,7 +8,7 @@ import struct TermindDomain.Host @testable import Termind -@Suite(.serialized) @MainActor +@Suite(.requiresVisibleWindow, .serialized) @MainActor struct MetricsJourneyRenderingTests { @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) func metricsAuthenticateInsideTheirJourneyWithoutOpeningAnExternalWindow(_ language: String) async throws { diff --git a/TFMTests/ModelProviderRenderingTests.swift b/TFMTests/ModelProviderRenderingTests.swift index 12124f8d..3db71405 100644 --- a/TFMTests/ModelProviderRenderingTests.swift +++ b/TFMTests/ModelProviderRenderingTests.swift @@ -10,7 +10,7 @@ import Testing @Suite(.serialized) @MainActor struct ModelProviderRenderingTests { - @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) + @Test(.requiresVisibleWindow, arguments: ["en", "zh-Hans", "zh-Hant"]) func blankNameFormsPreviewTheSavedDefaults(_ language: String) async throws { let host = TermindDomain.Host(name: "AWS", host: "example.invalid") let enclave = SSHKeyRef(label: "", algorithm: .ecdsa256, storage: .secureEnclave) @@ -32,7 +32,7 @@ struct ModelProviderRenderingTests { language: language, name: "enclave-name-hint") } - @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) + @Test(.requiresVisibleWindow, arguments: ["en", "zh-Hans", "zh-Hant"]) func anonymousProviderSettingsAndModelsRender(_ language: String) async throws { let provider = ModelProvider( name: "Local Model", inferenceURL: "http://localhost:11434/v1/chat/completions", @@ -50,7 +50,7 @@ struct ModelProviderRenderingTests { language: language, name: "provider-list") } - @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) + @Test(.requiresVisibleWindow, arguments: ["en", "zh-Hans", "zh-Hant"]) func newProviderHintsAndVertexFieldsRender(_ language: String) async throws { let ai = AIConfigurationModel(persistsSecrets: false) let provider = ModelProvider(name: "") diff --git a/TFMTests/PortForwardingRenderingTests.swift b/TFMTests/PortForwardingRenderingTests.swift index 2a389986..4b3ab8d3 100644 --- a/TFMTests/PortForwardingRenderingTests.swift +++ b/TFMTests/PortForwardingRenderingTests.swift @@ -8,7 +8,7 @@ import struct TermindDomain.Host @testable import Termind -@Suite(.serialized) @MainActor +@Suite(.requiresVisibleWindow, .serialized) @MainActor struct PortForwardingRenderingTests { @Test(arguments: [false, true]) func saveClosesTheEditorOnlyAfterPersistenceSucceeds(_ fails: Bool) async throws { diff --git a/TFMTests/RemoteWindowTitleTests.swift b/TFMTests/RemoteWindowTitleTests.swift index d2668ddd..40726c4e 100644 --- a/TFMTests/RemoteWindowTitleTests.swift +++ b/TFMTests/RemoteWindowTitleTests.swift @@ -63,6 +63,7 @@ struct RemoteWindowTitleTests { } @Test( + .requiresVisibleWindow, .disabled( if: !ghosttyHasActiveDisplay, "Ghostty's Metal surface requires an active display" diff --git a/TFMTests/ResponsivePageLayoutTests.swift b/TFMTests/ResponsivePageLayoutTests.swift index c4881b33..63fc2f2b 100644 --- a/TFMTests/ResponsivePageLayoutTests.swift +++ b/TFMTests/ResponsivePageLayoutTests.swift @@ -9,7 +9,7 @@ import Testing @MainActor @Suite(.serialized) struct ResponsivePageLayoutTests { - @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) + @Test(.requiresVisibleWindow, arguments: ["en", "zh-Hans", "zh-Hant"]) func vaultEditorsKeepFieldsInsideNarrowColumns(_ language: String) async throws { let host = TermindDomain.Host(name: "Production Database Gateway with a Long Name", host: "server.invalid") let app = AppModel(hosts: HostLibraryModel(hosts: [host])) diff --git a/TFMTests/SFTPEditRenderingTests.swift b/TFMTests/SFTPEditRenderingTests.swift index bc860650..d81b7678 100644 --- a/TFMTests/SFTPEditRenderingTests.swift +++ b/TFMTests/SFTPEditRenderingTests.swift @@ -11,7 +11,7 @@ import Testing @Suite(.serialized) @MainActor struct SFTPEditRenderingTests { - @Test func menuHoverInspection() async throws { + @Test(.requiresVisibleWindow) func menuHoverInspection() async throws { let model = SFTPEditModel() let hostID = UUID() let file = RemoteFile( @@ -80,7 +80,7 @@ struct SFTPEditRenderingTests { }) } - @Test func floatingChangesFitNarrowPanesAndRenderOverFileRows() throws { + @Test(.requiresVisibleWindow) func floatingChangesFitNarrowPanesAndRenderOverFileRows() throws { let model = SFTPEditModel() let hostID = UUID() for index in 0..<5 { diff --git a/TFMTests/SFTPSettingsIntegrationTests.swift b/TFMTests/SFTPSettingsIntegrationTests.swift index bd6bef17..5595827a 100644 --- a/TFMTests/SFTPSettingsIntegrationTests.swift +++ b/TFMTests/SFTPSettingsIntegrationTests.swift @@ -23,7 +23,7 @@ struct SFTPSettingsIntegrationTests { #expect(app.sftpTransfers.perHostLimit == 2) } - @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) + @Test(.requiresVisibleWindow, arguments: ["en", "zh-Hans", "zh-Hant"]) func sftpSettingsRenderTheDefaultsAndMaximumLimits(language: String) async throws { let app = AppModel(hosts: HostLibraryModel()) let model = WindowModel(workspaces: []) diff --git a/TFMTests/SSHKeyboardInteractiveRenderingTests.swift b/TFMTests/SSHKeyboardInteractiveRenderingTests.swift index 8cdfc848..2bdd8318 100644 --- a/TFMTests/SSHKeyboardInteractiveRenderingTests.swift +++ b/TFMTests/SSHKeyboardInteractiveRenderingTests.swift @@ -4,7 +4,7 @@ import TermindApplication import TermindPresentation import Testing -@Suite(.serialized) @MainActor +@Suite(.requiresVisibleWindow, .serialized) @MainActor struct SSHKeyboardInteractiveRenderingTests { @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) func rendersEchoAndSecretPrompts(_ language: String) async throws { diff --git a/TFMTests/SkillSettingsRenderingTests.swift b/TFMTests/SkillSettingsRenderingTests.swift index 77a7bdec..7de58e67 100644 --- a/TFMTests/SkillSettingsRenderingTests.swift +++ b/TFMTests/SkillSettingsRenderingTests.swift @@ -5,7 +5,7 @@ import Testing @testable import Termind -@Suite(.serialized) @MainActor struct SkillSettingsRenderingTests { +@Suite(.requiresVisibleWindow, .serialized) @MainActor struct SkillSettingsRenderingTests { @Test(arguments: ["en", "zh-Hans"]) func aiSettingsShowUserSkillsAndMemoryAfterMCP(_ language: String) async throws { let app = AppModel(hosts: HostLibraryModel(), skills: SkillLibraryModel()) diff --git a/TFMTests/TerminalMouseInteractionTests.swift b/TFMTests/TerminalMouseInteractionTests.swift index b7081fe1..20d2d7a4 100644 --- a/TFMTests/TerminalMouseInteractionTests.swift +++ b/TFMTests/TerminalMouseInteractionTests.swift @@ -12,7 +12,8 @@ private nonisolated let terminalMouseHasDisplay: Bool = { return CGGetActiveDisplayList(0, nil, &count) == .success && count > 0 }() -@Suite(.serialized, .disabled(if: !terminalMouseHasDisplay, "Ghostty requires an active display")) +@Suite( + .requiresVisibleWindow, .serialized, .disabled(if: !terminalMouseHasDisplay, "Ghostty requires an active display")) @MainActor struct TerminalMouseInteractionTests { @Test func dragCopiesChineseAndMultipleLinesOnlyAfterRelease() async throws { diff --git a/TFMTests/TerminalSettingsRenderingTests.swift b/TFMTests/TerminalSettingsRenderingTests.swift index 2242448f..7d33535d 100644 --- a/TFMTests/TerminalSettingsRenderingTests.swift +++ b/TFMTests/TerminalSettingsRenderingTests.swift @@ -6,7 +6,7 @@ import Testing @testable import Termind @testable import TermindPresentation -@Suite(.serialized) @MainActor +@Suite(.requiresVisibleWindow, .serialized) @MainActor struct TerminalSettingsRenderingTests { @Test(arguments: ["en", "zh-Hans", "zh-Hant"], ["", "Menlo"]) func terminalSettingsRenderInSupportedLanguages(_ language: String, family: String) async throws { diff --git a/TFMTests/TerminalUploadNoticeTests.swift b/TFMTests/TerminalUploadNoticeTests.swift index 24bdad06..b501940b 100644 --- a/TFMTests/TerminalUploadNoticeTests.swift +++ b/TFMTests/TerminalUploadNoticeTests.swift @@ -7,7 +7,9 @@ import Testing @testable import Termind -@Suite(.enabled(if: ProcessInfo.processInfo.environment["TERMIND_SSH_FIXTURE"] != nil), .serialized) +@Suite( + .requiresVisibleWindow, .enabled(if: ProcessInfo.processInfo.environment["TERMIND_SSH_FIXTURE"] != nil), .serialized +) @MainActor struct TerminalUploadNoticeTests { @Test(arguments: ["kbd", "none"]) diff --git a/TFMTests/VisibleWindowTestTrait.swift b/TFMTests/VisibleWindowTestTrait.swift new file mode 100644 index 00000000..e555ac52 --- /dev/null +++ b/TFMTests/VisibleWindowTestTrait.swift @@ -0,0 +1,12 @@ +import Foundation +import Testing + +extension Trait where Self == ConditionTrait { + /// Window presentation and focus changes require an explicit opt-in, even on a machine with a display. + static var requiresVisibleWindow: Self { + .enabled( + if: ProcessInfo.processInfo.environment["TERMIND_WINDOW_TESTS"] == "1", + "Set TERMIND_WINDOW_TESTS=1 to run tests that show windows or take focus" + ) + } +} diff --git a/TFMTests/WelcomeQuickConnectRenderingTests.swift b/TFMTests/WelcomeQuickConnectRenderingTests.swift index f51c5db7..3678d08e 100644 --- a/TFMTests/WelcomeQuickConnectRenderingTests.swift +++ b/TFMTests/WelcomeQuickConnectRenderingTests.swift @@ -6,7 +6,7 @@ import Testing @testable import Termind -@Suite(.serialized) @MainActor +@Suite(.requiresVisibleWindow, .serialized) @MainActor struct WelcomeQuickConnectRenderingTests { @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) func welcomePreviewsCommandsAndRejectsRemoteCommands(_ language: String) async throws {