import XCTest import AppKit import SwiftUI import UniformTypeIdentifiers import WebKit import ObjectiveC.runtime import Bonsplit import UserNotifications import Combine #if canImport(cmux_DEV) @testable import cmux_DEV #elseif canImport(cmux) @testable import cmux #endif @MainActor func makeTemporaryBrowserProfile(named prefix: String) throws -> BrowserProfileDefinition { try XCTUnwrap( BrowserProfileStore.shared.createProfile( named: "\(prefix)-\(UUID().uuidString)" ) ) } final class SidebarSelectedWorkspaceColorTests: XCTestCase { func testLightModeUsesConfiguredSelectedWorkspaceBackgroundColor() { guard let color = sidebarSelectedWorkspaceBackgroundNSColor(for: .light).usingColorSpace(.sRGB) else { XCTFail("Expected sRGB-convertible color") return } XCTAssertEqual(color.redComponent, 0, accuracy: 0.001) XCTAssertEqual(color.greenComponent, 136.0 / 255.0, accuracy: 0.001) XCTAssertEqual(color.blueComponent, 1.0, accuracy: 0.001) XCTAssertEqual(color.alphaComponent, 1.0, accuracy: 0.001) } func testDarkModeUsesConfiguredSelectedWorkspaceBackgroundColor() { guard let color = sidebarSelectedWorkspaceBackgroundNSColor(for: .dark).usingColorSpace(.sRGB) else { XCTFail("Expected sRGB-convertible color") return } XCTAssertEqual(color.redComponent, 0, accuracy: 0.001) XCTAssertEqual(color.greenComponent, 145.0 / 255.0, accuracy: 0.001) XCTAssertEqual(color.blueComponent, 1.0, accuracy: 0.001) XCTAssertEqual(color.alphaComponent, 1.0, accuracy: 0.001) } func testSelectedWorkspaceForegroundAlwaysUsesWhiteWithRequestedOpacity() { guard let color = sidebarSelectedWorkspaceForegroundNSColor(opacity: 0.65).usingColorSpace(.sRGB) else { XCTFail("Expected sRGB-convertible color") return } XCTAssertEqual(color.redComponent, 1.0, accuracy: 0.001) XCTAssertEqual(color.greenComponent, 1.0, accuracy: 0.001) XCTAssertEqual(color.blueComponent, 1.0, accuracy: 0.001) XCTAssertEqual(color.alphaComponent, 0.65, accuracy: 0.001) } } final class WorkspaceRenameShortcutDefaultsTests: XCTestCase { func testRenameTabShortcutDefaultsAndMetadata() { XCTAssertEqual(KeyboardShortcutSettings.Action.renameTab.label, "Rename Tab") XCTAssertEqual(KeyboardShortcutSettings.Action.renameTab.defaultsKey, "shortcut.renameTab") let shortcut = KeyboardShortcutSettings.Action.renameTab.defaultShortcut XCTAssertEqual(shortcut.key, "r") XCTAssertTrue(shortcut.command) XCTAssertFalse(shortcut.shift) XCTAssertFalse(shortcut.option) XCTAssertFalse(shortcut.control) } func testCloseWindowShortcutDefaultsAndMetadata() { XCTAssertEqual(KeyboardShortcutSettings.Action.closeWindow.label, "Close Window") XCTAssertEqual(KeyboardShortcutSettings.Action.closeWindow.defaultsKey, "shortcut.closeWindow") let shortcut = KeyboardShortcutSettings.Action.closeWindow.defaultShortcut XCTAssertEqual(shortcut.key, "w") XCTAssertTrue(shortcut.command) XCTAssertFalse(shortcut.shift) XCTAssertFalse(shortcut.option) XCTAssertTrue(shortcut.control) } func testRenameWorkspaceShortcutDefaultsAndMetadata() { XCTAssertEqual(KeyboardShortcutSettings.Action.renameWorkspace.label, "Rename Workspace") XCTAssertEqual(KeyboardShortcutSettings.Action.renameWorkspace.defaultsKey, "shortcut.renameWorkspace") let shortcut = KeyboardShortcutSettings.Action.renameWorkspace.defaultShortcut XCTAssertEqual(shortcut.key, "r") XCTAssertTrue(shortcut.command) XCTAssertTrue(shortcut.shift) XCTAssertFalse(shortcut.option) XCTAssertFalse(shortcut.control) } func testRenameWorkspaceShortcutConvertsToMenuShortcut() { let shortcut = KeyboardShortcutSettings.Action.renameWorkspace.defaultShortcut XCTAssertNotNil(shortcut.keyEquivalent) XCTAssertTrue(shortcut.eventModifiers.contains(.command)) XCTAssertTrue(shortcut.eventModifiers.contains(.shift)) XCTAssertFalse(shortcut.eventModifiers.contains(.option)) XCTAssertFalse(shortcut.eventModifiers.contains(.control)) } func testCloseWorkspaceShortcutDefaultsAndMetadata() { XCTAssertEqual(KeyboardShortcutSettings.Action.closeWorkspace.label, "Close Workspace") XCTAssertEqual(KeyboardShortcutSettings.Action.closeWorkspace.defaultsKey, "shortcut.closeWorkspace") let shortcut = KeyboardShortcutSettings.Action.closeWorkspace.defaultShortcut XCTAssertEqual(shortcut.key, "w") XCTAssertTrue(shortcut.command) XCTAssertTrue(shortcut.shift) XCTAssertFalse(shortcut.option) XCTAssertFalse(shortcut.control) } func testCloseWorkspaceShortcutConvertsToMenuShortcut() { let shortcut = KeyboardShortcutSettings.Action.closeWorkspace.defaultShortcut XCTAssertNotNil(shortcut.keyEquivalent) XCTAssertTrue(shortcut.eventModifiers.contains(.command)) XCTAssertTrue(shortcut.eventModifiers.contains(.shift)) XCTAssertFalse(shortcut.eventModifiers.contains(.option)) XCTAssertFalse(shortcut.eventModifiers.contains(.control)) } func testNextPreviousWorkspaceShortcutDefaultsAndMetadata() { XCTAssertEqual(KeyboardShortcutSettings.Action.nextSidebarTab.label, "Next Workspace") XCTAssertEqual(KeyboardShortcutSettings.Action.prevSidebarTab.label, "Previous Workspace") XCTAssertEqual(KeyboardShortcutSettings.Action.nextSidebarTab.defaultsKey, "shortcut.nextSidebarTab") XCTAssertEqual(KeyboardShortcutSettings.Action.prevSidebarTab.defaultsKey, "shortcut.prevSidebarTab") let nextShortcut = KeyboardShortcutSettings.Action.nextSidebarTab.defaultShortcut XCTAssertEqual(nextShortcut.key, "]") XCTAssertTrue(nextShortcut.command) XCTAssertFalse(nextShortcut.shift) XCTAssertFalse(nextShortcut.option) XCTAssertTrue(nextShortcut.control) let prevShortcut = KeyboardShortcutSettings.Action.prevSidebarTab.defaultShortcut XCTAssertEqual(prevShortcut.key, "[") XCTAssertTrue(prevShortcut.command) XCTAssertFalse(prevShortcut.shift) XCTAssertFalse(prevShortcut.option) XCTAssertTrue(prevShortcut.control) } func testNextPreviousWorkspaceShortcutsConvertToMenuShortcut() { let nextShortcut = KeyboardShortcutSettings.Action.nextSidebarTab.defaultShortcut XCTAssertNotNil(nextShortcut.keyEquivalent) XCTAssertEqual(nextShortcut.menuItemKeyEquivalent, "]") XCTAssertTrue(nextShortcut.eventModifiers.contains(.command)) XCTAssertTrue(nextShortcut.eventModifiers.contains(.control)) let prevShortcut = KeyboardShortcutSettings.Action.prevSidebarTab.defaultShortcut XCTAssertNotNil(prevShortcut.keyEquivalent) XCTAssertEqual(prevShortcut.menuItemKeyEquivalent, "[") XCTAssertTrue(prevShortcut.eventModifiers.contains(.command)) XCTAssertTrue(prevShortcut.eventModifiers.contains(.control)) } func testToggleTerminalCopyModeShortcutDefaultsAndMetadata() { XCTAssertEqual(KeyboardShortcutSettings.Action.toggleTerminalCopyMode.label, "Toggle Terminal Copy Mode") XCTAssertEqual( KeyboardShortcutSettings.Action.toggleTerminalCopyMode.defaultsKey, "shortcut.toggleTerminalCopyMode" ) let shortcut = KeyboardShortcutSettings.Action.toggleTerminalCopyMode.defaultShortcut XCTAssertEqual(shortcut.key, "m") XCTAssertTrue(shortcut.command) XCTAssertTrue(shortcut.shift) XCTAssertFalse(shortcut.option) XCTAssertFalse(shortcut.control) } func testMenuItemKeyEquivalentHandlesArrowAndTabKeys() { XCTAssertNotNil(StoredShortcut(key: "←", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent) XCTAssertNotNil(StoredShortcut(key: "→", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent) XCTAssertNotNil(StoredShortcut(key: "↑", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent) XCTAssertNotNil(StoredShortcut(key: "↓", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent) XCTAssertEqual( StoredShortcut(key: "\t", command: true, shift: false, option: false, control: false).menuItemKeyEquivalent, "\t" ) } func testShortcutDefaultsKeysRemainUnique() { let keys = KeyboardShortcutSettings.Action.allCases.map(\.defaultsKey) XCTAssertEqual(Set(keys).count, keys.count) } } final class WorkspaceShortcutMapperTests: XCTestCase { func testCommandNineMapsToLastWorkspaceIndex() { XCTAssertEqual(WorkspaceShortcutMapper.workspaceIndex(forDigit: 9, workspaceCount: 1), 0) XCTAssertEqual(WorkspaceShortcutMapper.workspaceIndex(forDigit: 9, workspaceCount: 4), 3) XCTAssertEqual(WorkspaceShortcutMapper.workspaceIndex(forDigit: 9, workspaceCount: 12), 11) } func testCommandDigitBadgesUseNineForLastWorkspaceWhenNeeded() { XCTAssertEqual(WorkspaceShortcutMapper.digitForWorkspace(at: 0, workspaceCount: 12), 1) XCTAssertEqual(WorkspaceShortcutMapper.digitForWorkspace(at: 7, workspaceCount: 12), 8) XCTAssertEqual(WorkspaceShortcutMapper.digitForWorkspace(at: 11, workspaceCount: 12), 9) XCTAssertNil(WorkspaceShortcutMapper.digitForWorkspace(at: 8, workspaceCount: 12)) } } final class WorkspacePlacementSettingsTests: XCTestCase { func testCurrentPlacementDefaultsToAfterCurrentWhenUnset() { let suiteName = "WorkspacePlacementSettingsTests.Default.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } XCTAssertEqual(WorkspacePlacementSettings.current(defaults: defaults), .afterCurrent) } func testCurrentPlacementReadsStoredValidValueAndFallsBackForInvalid() { let suiteName = "WorkspacePlacementSettingsTests.Stored.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.set(NewWorkspacePlacement.top.rawValue, forKey: WorkspacePlacementSettings.placementKey) XCTAssertEqual(WorkspacePlacementSettings.current(defaults: defaults), .top) defaults.set("nope", forKey: WorkspacePlacementSettings.placementKey) XCTAssertEqual(WorkspacePlacementSettings.current(defaults: defaults), .afterCurrent) } func testInsertionIndexTopInsertsBeforeUnpinned() { let index = WorkspacePlacementSettings.insertionIndex( placement: .top, selectedIndex: 4, selectedIsPinned: false, pinnedCount: 2, totalCount: 7 ) XCTAssertEqual(index, 2) } func testInsertionIndexAfterCurrentHandlesPinnedAndUnpinnedSelection() { let afterUnpinned = WorkspacePlacementSettings.insertionIndex( placement: .afterCurrent, selectedIndex: 3, selectedIsPinned: false, pinnedCount: 2, totalCount: 6 ) XCTAssertEqual(afterUnpinned, 4) let afterPinned = WorkspacePlacementSettings.insertionIndex( placement: .afterCurrent, selectedIndex: 0, selectedIsPinned: true, pinnedCount: 2, totalCount: 6 ) XCTAssertEqual(afterPinned, 2) } func testInsertionIndexEndAndNoSelectionAppend() { let endIndex = WorkspacePlacementSettings.insertionIndex( placement: .end, selectedIndex: 1, selectedIsPinned: false, pinnedCount: 1, totalCount: 5 ) XCTAssertEqual(endIndex, 5) let noSelectionIndex = WorkspacePlacementSettings.insertionIndex( placement: .afterCurrent, selectedIndex: nil, selectedIsPinned: false, pinnedCount: 0, totalCount: 5 ) XCTAssertEqual(noSelectionIndex, 5) } } @MainActor final class WorkspaceCreationPlacementTests: XCTestCase { private final class SnapshotMutatingTabManager: TabManager { var afterCaptureWorkspaceCreationSnapshot: (() -> Void)? var beforeCreateWorkspace: (() -> Void)? override func didCaptureWorkspaceCreationSnapshot() { afterCaptureWorkspaceCreationSnapshot?() } override func makeWorkspaceForCreation( title: String, workingDirectory: String?, portOrdinal: Int, configTemplate: CmuxSurfaceConfigTemplate?, initialTerminalCommand: String?, initialTerminalEnvironment: [String: String] ) -> Workspace { beforeCreateWorkspace?() return super.makeWorkspaceForCreation( title: title, workingDirectory: workingDirectory, portOrdinal: portOrdinal, configTemplate: configTemplate, initialTerminalCommand: initialTerminalCommand, initialTerminalEnvironment: initialTerminalEnvironment ) } } func testAddWorkspaceDefaultPlacementMatchesCurrentSetting() { let currentPlacement = WorkspacePlacementSettings.current() let defaultManager = makeManagerWithThreeWorkspaces() let defaultBaselineOrder = defaultManager.tabs.map(\.id) let defaultInserted = defaultManager.addWorkspace() guard let defaultInsertedIndex = defaultManager.tabs.firstIndex(where: { $0.id == defaultInserted.id }) else { XCTFail("Expected inserted workspace in tab list") return } XCTAssertEqual(defaultManager.tabs.map(\.id).filter { $0 != defaultInserted.id }, defaultBaselineOrder) let explicitManager = makeManagerWithThreeWorkspaces() let explicitBaselineOrder = explicitManager.tabs.map(\.id) let explicitInserted = explicitManager.addWorkspace(placementOverride: currentPlacement) guard let explicitInsertedIndex = explicitManager.tabs.firstIndex(where: { $0.id == explicitInserted.id }) else { XCTFail("Expected inserted workspace in tab list") return } XCTAssertEqual(explicitManager.tabs.map(\.id).filter { $0 != explicitInserted.id }, explicitBaselineOrder) XCTAssertEqual(defaultInsertedIndex, explicitInsertedIndex) } func testAddWorkspaceEndOverrideAlwaysAppends() { let manager = makeManagerWithThreeWorkspaces() let baselineCount = manager.tabs.count guard baselineCount >= 3 else { XCTFail("Expected at least three workspaces for placement regression test") return } let inserted = manager.addWorkspace(placementOverride: .end) guard let insertedIndex = manager.tabs.firstIndex(where: { $0.id == inserted.id }) else { XCTFail("Expected inserted workspace in tab list") return } XCTAssertEqual(insertedIndex, baselineCount) } func testAddWorkspaceAfterCurrentOverrideAppendsAfterLastSelectedWorkspace() { let manager = TabManager() guard !manager.tabs.isEmpty else { XCTFail("Expected TabManager to initialise with at least one workspace") return } _ = manager.addWorkspace() _ = manager.addWorkspace() let fourth = manager.addWorkspace() let baselineOrder = manager.tabs.map(\.id) manager.selectWorkspace(fourth) let inserted = manager.addWorkspace(placementOverride: .afterCurrent) XCTAssertEqual(manager.tabs.map(\.id).filter { $0 != inserted.id }, baselineOrder) XCTAssertEqual(manager.tabs.last?.id, inserted.id) } func testAddWorkspaceAfterCurrentUsesPrecreationSnapshotWhenSelectionMutatesDuringBootstrap() { let manager = SnapshotMutatingTabManager() guard let first = manager.tabs.first else { XCTFail("Expected initial workspace") return } manager.setPinned(first, pinned: true) let second = manager.addWorkspace() let third = manager.addWorkspace() manager.selectWorkspace(third) let baselineOrder = manager.tabs.map(\.id) manager.beforeCreateWorkspace = { manager.selectWorkspace(first) } let inserted = manager.addWorkspace(placementOverride: .afterCurrent) XCTAssertEqual(manager.tabs.map(\.id).filter { $0 != inserted.id }, baselineOrder) XCTAssertEqual(manager.tabs.map(\.id), [first.id, second.id, third.id, inserted.id]) XCTAssertEqual(manager.selectedTabId, inserted.id) } func testAddWorkspaceAfterCurrentDoesNotReinsertClosedWorkspaceCapturedInSnapshot() { let manager = SnapshotMutatingTabManager() guard let first = manager.tabs.first else { XCTFail("Expected initial workspace") return } let second = manager.addWorkspace() let third = manager.addWorkspace() manager.selectWorkspace(third) manager.afterCaptureWorkspaceCreationSnapshot = { manager.closeWorkspace(second) } let inserted = manager.addWorkspace(placementOverride: .afterCurrent) XCTAssertEqual(manager.tabs.map(\.id), [first.id, third.id, inserted.id]) XCTAssertFalse(manager.tabs.contains(where: { $0.id == second.id })) XCTAssertEqual(manager.selectedTabId, inserted.id) } func testAddWorkspaceSurvivesSelectedWorkspaceClosingAfterSnapshot() { let manager = SnapshotMutatingTabManager() guard let first = manager.tabs.first else { XCTFail("Expected initial workspace") return } let second = manager.addWorkspace() let third = manager.addWorkspace() manager.selectWorkspace(third) manager.afterCaptureWorkspaceCreationSnapshot = { manager.closeWorkspace(third) } let inserted = manager.addWorkspace(placementOverride: .afterCurrent) XCTAssertEqual(manager.tabs.map(\.id), [first.id, second.id, inserted.id]) XCTAssertFalse(manager.tabs.contains(where: { $0.id == third.id })) XCTAssertEqual(manager.selectedTabId, inserted.id) } func testAddWorkspaceSurvivesMidCreationClose() { let manager = SnapshotMutatingTabManager() guard let first = manager.tabs.first else { XCTFail("Expected initial workspace") return } let closingWorkspace = manager.addWorkspace() let third = manager.addWorkspace() manager.selectWorkspace(third) let closingWorkspaceId = closingWorkspace.id XCTAssertEqual(manager.tabs.map(\.id), [first.id, closingWorkspaceId, third.id]) manager.afterCaptureWorkspaceCreationSnapshot = { guard let liveWorkspace = manager.tabs.first(where: { $0.id == closingWorkspaceId }) else { XCTFail("Expected captured workspace to still be present when closing after snapshot") return } manager.closeWorkspace(liveWorkspace) } let inserted = manager.addWorkspace(placementOverride: .afterCurrent) XCTAssertFalse(manager.tabs.contains(where: { $0.id == closingWorkspaceId })) XCTAssertEqual(manager.tabs.map(\.id), [first.id, third.id, inserted.id]) XCTAssertEqual(manager.selectedTabId, inserted.id) } func testAddWorkspaceAfterCurrentUsesSnapshotPinnedStateWhenPinningMutatesAfterSnapshot() { let manager = SnapshotMutatingTabManager() guard let first = manager.tabs.first else { XCTFail("Expected initial workspace") return } manager.setPinned(first, pinned: true) let second = manager.addWorkspace() let third = manager.addWorkspace() manager.selectWorkspace(first) let baselineOrder = manager.tabs.map(\.id) manager.afterCaptureWorkspaceCreationSnapshot = { manager.setPinned(first, pinned: false) } let inserted = manager.addWorkspace(placementOverride: .afterCurrent) XCTAssertEqual(manager.tabs.map(\.id).filter { $0 != inserted.id }, baselineOrder) XCTAssertEqual(manager.tabs.map(\.id), [first.id, inserted.id, second.id, third.id]) XCTAssertEqual(manager.selectedTabId, inserted.id) } func testAddWorkspaceAfterCurrentFollowsLiveReorderUsingSnapshotTabValues() { let manager = SnapshotMutatingTabManager() guard let first = manager.tabs.first else { XCTFail("Expected initial workspace") return } let second = manager.addWorkspace() let third = manager.addWorkspace() manager.selectWorkspace(second) manager.afterCaptureWorkspaceCreationSnapshot = { XCTAssertTrue( manager.reorderWorkspace(tabId: third.id, toIndex: 0), "Expected to reorder live workspaces after the snapshot is captured" ) } let inserted = manager.addWorkspace(placementOverride: .afterCurrent) XCTAssertEqual( manager.tabs.map(\.id).filter { $0 != inserted.id }, [third.id, first.id, second.id] ) XCTAssertEqual(manager.tabs.map(\.id), [third.id, first.id, second.id, inserted.id]) XCTAssertEqual(manager.selectedTabId, inserted.id) } private func makeManagerWithThreeWorkspaces() -> TabManager { let manager = TabManager() _ = manager.addWorkspace() _ = manager.addWorkspace() if let first = manager.tabs.first { manager.selectWorkspace(first) } return manager } } @MainActor final class WorkspaceCreationConfigSanitizationTests: XCTestCase { private final class UnsafeConfigSnapshotTabManager: TabManager { private var injectedConfig: CmuxSurfaceConfigTemplate? var capturedConfigTemplate: CmuxSurfaceConfigTemplate? func installInjectedConfig(fontSize: Float) { var config = CmuxSurfaceConfigTemplate() config.fontSize = fontSize config.workingDirectory = "/tmp/cmux-workspace-snapshot" config.command = "echo snapshot" config.environmentVariables = ["CMUX_INHERITED_ENV": "1"] injectedConfig = config } override func inheritedTerminalConfigForNewWorkspace( workspace: Workspace? ) -> CmuxSurfaceConfigTemplate? { injectedConfig ?? super.inheritedTerminalConfigForNewWorkspace(workspace: workspace) } override func makeWorkspaceForCreation( title: String, workingDirectory: String?, portOrdinal: Int, configTemplate: CmuxSurfaceConfigTemplate?, initialTerminalCommand: String?, initialTerminalEnvironment: [String: String] ) -> Workspace { capturedConfigTemplate = configTemplate return super.makeWorkspaceForCreation( title: title, workingDirectory: workingDirectory, portOrdinal: portOrdinal, configTemplate: configTemplate, initialTerminalCommand: initialTerminalCommand, initialTerminalEnvironment: initialTerminalEnvironment ) } } func testAddWorkspacePassesSanitizedInheritedConfigTemplate() { let manager = UnsafeConfigSnapshotTabManager() manager.installInjectedConfig(fontSize: 19) _ = manager.addWorkspace() guard let capturedConfig = manager.capturedConfigTemplate else { XCTFail("Expected captured config template for new workspace") return } XCTAssertEqual(capturedConfig.fontSize, 19, accuracy: 0.001) XCTAssertNil(capturedConfig.workingDirectory) XCTAssertNil(capturedConfig.command) XCTAssertTrue(capturedConfig.environmentVariables.isEmpty) } } final class WorkspaceTabColorSettingsTests: XCTestCase { func testNormalizedHexAcceptsAndNormalizesValidInput() { XCTAssertEqual(WorkspaceTabColorSettings.normalizedHex("#abc123"), "#ABC123") XCTAssertEqual(WorkspaceTabColorSettings.normalizedHex(" aBcDeF "), "#ABCDEF") XCTAssertNil(WorkspaceTabColorSettings.normalizedHex("#1234")) XCTAssertNil(WorkspaceTabColorSettings.normalizedHex("#GG1234")) } func testBuiltInPaletteMatchesOriginalPRPalette() { let suiteName = "WorkspaceTabColorSettingsTests.BuiltInPalette.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } let palette = WorkspaceTabColorSettings.defaultPaletteWithOverrides(defaults: defaults) XCTAssertEqual(palette.count, 16) XCTAssertEqual(palette.first?.name, "Red") XCTAssertEqual(palette.first?.hex, "#C0392B") XCTAssertEqual(palette.last?.name, "Charcoal") XCTAssertFalse(palette.contains(where: { $0.name == "Gold" })) } func testDefaultOverrideRoundTripFallsBackWhenResetToBase() { let suiteName = "WorkspaceTabColorSettingsTests.DefaultOverride.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } let first = WorkspaceTabColorSettings.defaultPalette[0] XCTAssertEqual( WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults), first.hex ) WorkspaceTabColorSettings.setDefaultColor(named: first.name, hex: "#00aa33", defaults: defaults) XCTAssertEqual( WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults), "#00AA33" ) WorkspaceTabColorSettings.setDefaultColor(named: first.name, hex: first.hex, defaults: defaults) XCTAssertEqual( WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults), first.hex ) } func testAddCustomColorPersistsAndDeduplicatesByMostRecent() { let suiteName = "WorkspaceTabColorSettingsTests.CustomColors.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } XCTAssertEqual( WorkspaceTabColorSettings.addCustomColor(" #00aa33 ", defaults: defaults), "#00AA33" ) XCTAssertEqual( WorkspaceTabColorSettings.addCustomColor("#112233", defaults: defaults), "#112233" ) XCTAssertEqual( WorkspaceTabColorSettings.addCustomColor("#00AA33", defaults: defaults), "#00AA33" ) XCTAssertNil(WorkspaceTabColorSettings.addCustomColor("nope", defaults: defaults)) XCTAssertEqual( WorkspaceTabColorSettings.customColors(defaults: defaults), ["#00AA33", "#112233"] ) } func testPaletteIncludesCustomEntriesAndResetClearsAll() { let suiteName = "WorkspaceTabColorSettingsTests.Reset.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } let first = WorkspaceTabColorSettings.defaultPalette[0] WorkspaceTabColorSettings.setDefaultColor(named: first.name, hex: "#334455", defaults: defaults) _ = WorkspaceTabColorSettings.addCustomColor("#778899", defaults: defaults) let paletteBeforeReset = WorkspaceTabColorSettings.palette(defaults: defaults) XCTAssertEqual(paletteBeforeReset.count, WorkspaceTabColorSettings.defaultPalette.count + 1) XCTAssertEqual(paletteBeforeReset[0].hex, "#334455") XCTAssertEqual(paletteBeforeReset.last?.name, "Custom 1") XCTAssertEqual(paletteBeforeReset.last?.hex, "#778899") WorkspaceTabColorSettings.reset(defaults: defaults) XCTAssertEqual(WorkspaceTabColorSettings.customColors(defaults: defaults), []) XCTAssertEqual( WorkspaceTabColorSettings.defaultColorHex(named: first.name, defaults: defaults), first.hex ) } func testDisplayColorLightModeKeepsOriginalHex() { let originalHex = "#1A5276" let rendered = WorkspaceTabColorSettings.displayNSColor( hex: originalHex, colorScheme: .light ) XCTAssertEqual(rendered?.hexString(), originalHex) } func testDisplayColorDarkModeBrightensColor() { let originalHex = "#1A5276" guard let base = NSColor(hex: originalHex), let rendered = WorkspaceTabColorSettings.displayNSColor( hex: originalHex, colorScheme: .dark ) else { XCTFail("Expected valid color conversion") return } XCTAssertNotEqual(rendered.hexString(), originalHex) XCTAssertGreaterThan(rendered.luminance, base.luminance) } func testDisplayColorDarkModeKeepsGrayscaleNeutral() { let originalHex = "#808080" guard let base = NSColor(hex: originalHex), let rendered = WorkspaceTabColorSettings.displayNSColor( hex: originalHex, colorScheme: .dark ), let renderedSRGB = rendered.usingColorSpace(.sRGB) else { XCTFail("Expected valid color conversion") return } XCTAssertGreaterThan(rendered.luminance, base.luminance) XCTAssertLessThan(abs(renderedSRGB.redComponent - renderedSRGB.greenComponent), 0.003) XCTAssertLessThan(abs(renderedSRGB.greenComponent - renderedSRGB.blueComponent), 0.003) } func testDisplayColorForceBrightensInLightMode() { let originalHex = "#1A5276" guard let base = NSColor(hex: originalHex), let rendered = WorkspaceTabColorSettings.displayNSColor( hex: originalHex, colorScheme: .light, forceBright: true ) else { XCTFail("Expected valid color conversion") return } XCTAssertNotEqual(rendered.hexString(), originalHex) XCTAssertGreaterThan(rendered.luminance, base.luminance) } } final class WorkspaceAutoReorderSettingsTests: XCTestCase { func testDefaultIsEnabled() { let suiteName = "WorkspaceAutoReorderSettingsTests.Default.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } XCTAssertTrue(WorkspaceAutoReorderSettings.isEnabled(defaults: defaults)) } func testDisabledWhenSetToFalse() { let suiteName = "WorkspaceAutoReorderSettingsTests.Disabled.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.set(false, forKey: WorkspaceAutoReorderSettings.key) XCTAssertFalse(WorkspaceAutoReorderSettings.isEnabled(defaults: defaults)) } func testEnabledWhenSetToTrue() { let suiteName = "WorkspaceAutoReorderSettingsTests.Enabled.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.set(true, forKey: WorkspaceAutoReorderSettings.key) XCTAssertTrue(WorkspaceAutoReorderSettings.isEnabled(defaults: defaults)) } } final class SidebarWorkspaceDetailSettingsTests: XCTestCase { func testDefaultPreferencesWhenUnset() { let suiteName = "SidebarWorkspaceDetailSettingsTests.Default.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } XCTAssertFalse(SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults)) XCTAssertTrue(SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults)) XCTAssertTrue( SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility( showNotificationMessage: SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults), hideAllDetails: SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults) ) ) } func testStoredPreferencesOverrideDefaults() { let suiteName = "SidebarWorkspaceDetailSettingsTests.Stored.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.set(true, forKey: SidebarWorkspaceDetailSettings.hideAllDetailsKey) defaults.set(false, forKey: SidebarWorkspaceDetailSettings.showNotificationMessageKey) XCTAssertTrue(SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults)) XCTAssertFalse(SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults)) XCTAssertFalse( SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility( showNotificationMessage: SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults), hideAllDetails: false ) ) XCTAssertFalse( SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility( showNotificationMessage: true, hideAllDetails: SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults) ) ) } } final class SidebarWorkspaceAuxiliaryDetailVisibilityTests: XCTestCase { func testResolvedVisibilityPreservesPerRowTogglesWhenDetailsAreShown() { XCTAssertEqual( SidebarWorkspaceAuxiliaryDetailVisibility.resolved( showMetadata: true, showLog: false, showProgress: true, showBranchDirectory: false, showPullRequests: true, showPorts: false, hideAllDetails: false ), SidebarWorkspaceAuxiliaryDetailVisibility( showsMetadata: true, showsLog: false, showsProgress: true, showsBranchDirectory: false, showsPullRequests: true, showsPorts: false ) ) } func testResolvedVisibilityHidesAllAuxiliaryRowsWhenDetailsAreHidden() { XCTAssertEqual( SidebarWorkspaceAuxiliaryDetailVisibility.resolved( showMetadata: true, showLog: true, showProgress: true, showBranchDirectory: true, showPullRequests: true, showPorts: true, hideAllDetails: true ), .hidden ) } } final class WorkspaceReorderTests: XCTestCase { @MainActor func testReorderWorkspaceMovesWorkspaceToRequestedIndex() { let manager = TabManager() let first = manager.tabs[0] let second = manager.addWorkspace() let third = manager.addWorkspace() manager.selectWorkspace(second) XCTAssertEqual(manager.selectedTabId, second.id) XCTAssertTrue(manager.reorderWorkspace(tabId: second.id, toIndex: 0)) XCTAssertEqual(manager.tabs.map(\.id), [second.id, first.id, third.id]) XCTAssertEqual(manager.selectedTabId, second.id) } @MainActor func testReorderWorkspaceClampsOutOfRangeTargetIndex() { let manager = TabManager() let first = manager.tabs[0] let second = manager.addWorkspace() let third = manager.addWorkspace() XCTAssertTrue(manager.reorderWorkspace(tabId: first.id, toIndex: 999)) XCTAssertEqual(manager.tabs.map(\.id), [second.id, third.id, first.id]) } @MainActor func testReorderWorkspaceReturnsFalseForUnknownWorkspace() { let manager = TabManager() XCTAssertFalse(manager.reorderWorkspace(tabId: UUID(), toIndex: 0)) } @MainActor func testReorderWorkspaceKeepsUnpinnedWorkspaceBelowPinnedSegment() { let manager = TabManager() let firstPinned = manager.tabs[0] manager.setPinned(firstPinned, pinned: true) let secondPinned = manager.addWorkspace() manager.setPinned(secondPinned, pinned: true) let unpinned = manager.addWorkspace() XCTAssertTrue(manager.reorderWorkspace(tabId: unpinned.id, toIndex: 0)) XCTAssertEqual(manager.tabs.map(\.id), [firstPinned.id, secondPinned.id, unpinned.id]) } @MainActor func testReorderWorkspaceKeepsPinnedWorkspaceInsidePinnedSegment() { let manager = TabManager() let firstPinned = manager.tabs[0] manager.setPinned(firstPinned, pinned: true) let secondPinned = manager.addWorkspace() manager.setPinned(secondPinned, pinned: true) let unpinned = manager.addWorkspace() XCTAssertTrue(manager.reorderWorkspace(tabId: firstPinned.id, toIndex: 999)) XCTAssertEqual(manager.tabs.map(\.id), [secondPinned.id, firstPinned.id, unpinned.id]) } } @MainActor final class WorkspaceNotificationReorderTests: XCTestCase { func testNotificationAutoReorderDoesNotMovePinnedWorkspace() { let appDelegate = AppDelegate.shared ?? AppDelegate() let manager = TabManager() let notificationStore = TerminalNotificationStore.shared let originalTabManager = appDelegate.tabManager let originalNotificationStore = appDelegate.notificationStore let defaults = UserDefaults.standard let originalAutoReorderSetting = defaults.object(forKey: WorkspaceAutoReorderSettings.key) let originalAppFocusOverride = AppFocusState.overrideIsFocused notificationStore.replaceNotificationsForTesting([]) notificationStore.configureNotificationDeliveryHandlerForTesting { _, _ in } appDelegate.tabManager = manager appDelegate.notificationStore = notificationStore defaults.set(true, forKey: WorkspaceAutoReorderSettings.key) AppFocusState.overrideIsFocused = false defer { notificationStore.replaceNotificationsForTesting([]) notificationStore.resetNotificationDeliveryHandlerForTesting() appDelegate.tabManager = originalTabManager appDelegate.notificationStore = originalNotificationStore AppFocusState.overrideIsFocused = originalAppFocusOverride if let originalAutoReorderSetting { defaults.set(originalAutoReorderSetting, forKey: WorkspaceAutoReorderSettings.key) } else { defaults.removeObject(forKey: WorkspaceAutoReorderSettings.key) } } let firstPinned = manager.tabs[0] manager.setPinned(firstPinned, pinned: true) let secondPinned = manager.addWorkspace() manager.setPinned(secondPinned, pinned: true) let unpinned = manager.addWorkspace() let expectedOrder = [firstPinned.id, secondPinned.id, unpinned.id] notificationStore.addNotification( tabId: secondPinned.id, surfaceId: nil, title: "Build finished", subtitle: "", body: "Pinned workspaces should stay put" ) XCTAssertEqual(manager.tabs.map(\.id), expectedOrder) } } @MainActor final class WorkspaceTeardownTests: XCTestCase { func testTeardownAllPanelsClearsPanelMetadataCaches() { let workspace = Workspace() guard let initialPanelId = workspace.focusedPanelId else { XCTFail("Expected focused panel in new workspace") return } workspace.setPanelCustomTitle(panelId: initialPanelId, title: "Initial custom title") workspace.setPanelPinned(panelId: initialPanelId, pinned: true) guard let splitPanel = workspace.newTerminalSplit(from: initialPanelId, orientation: .horizontal) else { XCTFail("Expected split panel to be created") return } workspace.setPanelCustomTitle(panelId: splitPanel.id, title: "Split custom title") workspace.setPanelPinned(panelId: splitPanel.id, pinned: true) workspace.markPanelUnread(initialPanelId) XCTAssertFalse(workspace.panels.isEmpty) XCTAssertFalse(workspace.panelTitles.isEmpty) XCTAssertFalse(workspace.panelCustomTitles.isEmpty) XCTAssertFalse(workspace.pinnedPanelIds.isEmpty) XCTAssertFalse(workspace.manualUnreadPanelIds.isEmpty) workspace.teardownAllPanels() XCTAssertTrue(workspace.panels.isEmpty) XCTAssertTrue(workspace.panelTitles.isEmpty) XCTAssertTrue(workspace.panelCustomTitles.isEmpty) XCTAssertTrue(workspace.pinnedPanelIds.isEmpty) XCTAssertTrue(workspace.manualUnreadPanelIds.isEmpty) } } @MainActor final class WorkspaceSplitWorkingDirectoryTests: XCTestCase { private func waitForCondition( timeout: TimeInterval = 2, pollInterval: TimeInterval = 0.01, _ condition: () -> Bool ) -> Bool { let deadline = Date().addingTimeInterval(timeout) while Date() < deadline { if condition() { return true } RunLoop.current.run(until: Date().addingTimeInterval(pollInterval)) } return condition() } private func hostTerminalPanelInWindow(_ panel: TerminalPanel) throws -> NSWindow { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 420, height: 280), styleMask: [.titled, .closable], backing: .buffered, defer: false ) let contentView = try XCTUnwrap(window.contentView, "Expected content view") let hostedView = panel.hostedView hostedView.frame = contentView.bounds hostedView.autoresizingMask = [.width, .height] contentView.addSubview(hostedView) window.makeKeyAndOrderFront(nil) window.displayIfNeeded() contentView.layoutSubtreeIfNeeded() XCTAssertTrue( waitForCondition { panel.surface.surface != nil }, "Expected runtime surface to materialize after hosting panel in a window" ) return window } func testNewTerminalSplitFallsBackToRequestedWorkingDirectoryWhenReportedDirectoryIsStale() { let workspace = Workspace() guard let sourcePaneId = workspace.bonsplitController.focusedPaneId else { XCTFail("Expected focused pane in new workspace") return } let staleCurrentDirectory = workspace.currentDirectory let requestedDirectory = "/tmp/cmux-requested-split-cwd-\(UUID().uuidString)" guard let sourcePanel = workspace.newTerminalSurface( inPane: sourcePaneId, focus: false, workingDirectory: requestedDirectory ) else { XCTFail("Expected source terminal panel to be created") return } XCTAssertEqual(sourcePanel.requestedWorkingDirectory, requestedDirectory) XCTAssertNil( workspace.panelDirectories[sourcePanel.id], "Expected requested cwd to exist before shell integration reports a live cwd" ) XCTAssertEqual( workspace.currentDirectory, staleCurrentDirectory, "Expected focused workspace cwd to remain stale before panel directory updates" ) guard let splitPanel = workspace.newTerminalSplit( from: sourcePanel.id, orientation: .horizontal, focus: false ) else { XCTFail("Expected split terminal panel to be created") return } XCTAssertEqual( splitPanel.requestedWorkingDirectory, requestedDirectory, "Expected split to inherit the source terminal's requested cwd when no reported cwd exists yet" ) } func testNewTerminalSplitSkipsFreedInheritedSurfacePointer() throws { #if DEBUG let workspace = Workspace() guard let sourcePanelId = workspace.focusedPanelId, let sourcePanel = workspace.terminalPanel(for: sourcePanelId) else { XCTFail("Expected focused terminal panel") return } let window = try hostTerminalPanelInWindow(sourcePanel) defer { window.orderOut(nil) } XCTAssertNotNil(sourcePanel.surface.surface, "Expected runtime surface before forcing stale pointer") sourcePanel.surface.replaceSurfaceWithFreedPointerForTesting() XCTAssertNotNil( sourcePanel.surface.surface, "Expected Swift wrapper to remain non-nil while simulating a stale native surface" ) let splitPanel = workspace.newTerminalSplit( from: sourcePanelId, orientation: .horizontal, focus: false ) XCTAssertNotNil(splitPanel, "Expected split creation to survive a stale inherited surface pointer") XCTAssertNil(sourcePanel.surface.surface, "Expected stale surface pointer to be quarantined") #else throw XCTSkip("Debug-only regression test") #endif } func testNewTerminalSurfaceSkipsFreedInheritedSurfacePointer() throws { #if DEBUG let workspace = Workspace() guard let sourcePanelId = workspace.focusedPanelId, let sourcePanel = workspace.terminalPanel(for: sourcePanelId), let sourcePaneId = workspace.paneId(forPanelId: sourcePanelId) else { XCTFail("Expected focused terminal panel and pane") return } let window = try hostTerminalPanelInWindow(sourcePanel) defer { window.orderOut(nil) } XCTAssertNotNil(sourcePanel.surface.surface, "Expected runtime surface before forcing stale pointer") sourcePanel.surface.replaceSurfaceWithFreedPointerForTesting() XCTAssertNotNil( sourcePanel.surface.surface, "Expected Swift wrapper to remain non-nil while simulating a stale native surface" ) let createdPanel = workspace.newTerminalSurface( inPane: sourcePaneId, focus: false ) XCTAssertNotNil(createdPanel, "Expected terminal creation to survive a stale inherited surface pointer") XCTAssertNil(sourcePanel.surface.surface, "Expected stale surface pointer to be quarantined") #else throw XCTSkip("Debug-only regression test") #endif } } @MainActor final class WorkspaceTerminalFocusRecoveryTests: XCTestCase { private func makeWindow() -> NSWindow { NSWindow( contentRect: NSRect(x: 0, y: 0, width: 360, height: 220), styleMask: [.titled, .closable], backing: .buffered, defer: false ) } private func makeMouseEvent( type: NSEvent.EventType, location: NSPoint, window: NSWindow ) -> NSEvent { guard let event = NSEvent.mouseEvent( with: type, location: location, modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber, context: nil, eventNumber: 0, clickCount: 1, pressure: 1.0 ) else { fatalError("Failed to create \(type) mouse event") } return event } private func surfaceView(in hostedView: GhosttySurfaceScrollView) -> GhosttyNSView? { var stack: [NSView] = [hostedView] while let current = stack.popLast() { if let surfaceView = current as? GhosttyNSView { return surfaceView } stack.append(contentsOf: current.subviews) } return nil } func testTerminalFirstResponderConvergesSplitActiveStateWhenSelectionAlreadyMatches() { let workspace = Workspace() guard let leftPanelId = workspace.focusedPanelId, let leftPanel = workspace.terminalPanel(for: leftPanelId), let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else { XCTFail("Expected split terminal panels") return } XCTAssertEqual( workspace.focusedPanelId, rightPanel.id, "Expected the new split panel to be selected before simulating stale focus state" ) // Simulate the split-pane failure mode: Bonsplit already points at the right panel, // but the active terminal state is still stale on the left panel. leftPanel.surface.setFocus(true) leftPanel.hostedView.setActive(true) rightPanel.surface.setFocus(false) rightPanel.hostedView.setActive(false) workspace.focusPanel(rightPanel.id, trigger: .terminalFirstResponder) XCTAssertFalse( leftPanel.hostedView.debugRenderStats().isActive, "Expected stale left-pane active state to be cleared" ) XCTAssertTrue( rightPanel.hostedView.debugRenderStats().isActive, "Expected terminal-first-responder recovery to reactivate the selected split pane" ) } func testTerminalClickRecoversSplitActiveStateWhenFocusCallbackIsSuppressed() { let workspace = Workspace() guard let leftPanelId = workspace.focusedPanelId, let leftPanel = workspace.terminalPanel(for: leftPanelId), let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else { XCTFail("Expected split terminal panels") return } let window = makeWindow() defer { window.orderOut(nil) } guard let contentView = window.contentView else { XCTFail("Expected content view") return } leftPanel.hostedView.frame = NSRect(x: 0, y: 0, width: 180, height: 220) rightPanel.hostedView.frame = NSRect(x: 180, y: 0, width: 180, height: 220) contentView.addSubview(leftPanel.hostedView) contentView.addSubview(rightPanel.hostedView) leftPanel.hostedView.setVisibleInUI(true) rightPanel.hostedView.setVisibleInUI(true) leftPanel.hostedView.setFocusHandler { workspace.focusPanel(leftPanel.id, trigger: .terminalFirstResponder) } rightPanel.hostedView.setFocusHandler { workspace.focusPanel(rightPanel.id, trigger: .terminalFirstResponder) } window.makeKeyAndOrderFront(nil) window.displayIfNeeded() contentView.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) XCTAssertEqual( workspace.focusedPanelId, rightPanel.id, "Expected the clicked split pane to already be selected before simulating stale focus state" ) // Simulate the ghost-terminal race: the right pane is selected in Bonsplit, but stale // active state remains on the left and the right pane's AppKit focus callback never fires // after split reparent/layout churn. leftPanel.surface.setFocus(true) leftPanel.hostedView.setActive(true) rightPanel.surface.setFocus(false) rightPanel.hostedView.setActive(false) rightPanel.hostedView.suppressReparentFocus() guard let rightSurfaceView = surfaceView(in: rightPanel.hostedView) else { XCTFail("Expected right terminal surface view") return } let pointInWindow = rightSurfaceView.convert(NSPoint(x: 24, y: 24), to: nil) let event = makeMouseEvent(type: .leftMouseDown, location: pointInWindow, window: window) rightSurfaceView.mouseDown(with: event) RunLoop.current.run(until: Date().addingTimeInterval(0.05)) XCTAssertFalse( leftPanel.hostedView.debugRenderStats().isActive, "Expected clicking the selected split pane to clear stale sibling active state even when AppKit focus callbacks are suppressed" ) XCTAssertTrue( rightPanel.hostedView.debugRenderStats().isActive, "Expected clicking the selected split pane to reactivate terminal input when focus callbacks are suppressed" ) XCTAssertTrue( rightPanel.hostedView.isSurfaceViewFirstResponder(), "Expected the clicked split pane to become first responder" ) } func testClearSuppressReparentFocusReassertsGhosttyFocusForCurrentFirstResponder() throws { #if DEBUG let workspace = Workspace() guard let leftPanelId = workspace.focusedPanelId, let leftPanel = workspace.terminalPanel(for: leftPanelId), let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else { XCTFail("Expected split terminal panels") return } let window = makeWindow() defer { window.orderOut(nil) } guard let contentView = window.contentView else { XCTFail("Expected content view") return } leftPanel.hostedView.frame = NSRect(x: 0, y: 0, width: 180, height: 220) rightPanel.hostedView.frame = NSRect(x: 180, y: 0, width: 180, height: 220) contentView.addSubview(leftPanel.hostedView) contentView.addSubview(rightPanel.hostedView) leftPanel.hostedView.setVisibleInUI(true) rightPanel.hostedView.setVisibleInUI(true) window.makeKeyAndOrderFront(nil) window.displayIfNeeded() contentView.layoutSubtreeIfNeeded() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) guard let leftSurfaceView = surfaceView(in: leftPanel.hostedView) else { XCTFail("Expected left terminal surface view") return } leftPanel.surface.setFocus(false) rightPanel.surface.setFocus(true) leftPanel.hostedView.suppressReparentFocus() XCTAssertTrue(window.makeFirstResponder(leftSurfaceView)) RunLoop.current.run(until: Date().addingTimeInterval(0.05)) XCTAssertFalse( leftPanel.surface.debugDesiredFocusState(), "Suppressed reparent focus should not immediately flip the Ghostty focus bit" ) leftPanel.hostedView.clearSuppressReparentFocus() RunLoop.current.run(until: Date().addingTimeInterval(0.05)) XCTAssertTrue( leftPanel.surface.debugDesiredFocusState(), "Clearing reparent-focus suppression should reassert Ghostty focus when the surface still owns first responder" ) #else throw XCTSkip("Debug-only regression test") #endif } } @MainActor final class WorkspaceTerminalConfigInheritanceSelectionTests: XCTestCase { func testPrefersSelectedTerminalInTargetPaneOverFocusedTerminalElsewhere() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let leftPanelId = workspace.focusedPanelId, let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal), let leftPaneId = workspace.paneId(forPanelId: leftPanelId) else { XCTFail("Expected workspace split setup to succeed") return } // Programmatic split focuses the new right panel by default. XCTAssertEqual(workspace.focusedPanelId, rightPanel.id) let sourcePanel = workspace.terminalPanelForConfigInheritance(inPane: leftPaneId) XCTAssertEqual( sourcePanel?.id, leftPanelId, "Expected inheritance to use the selected terminal in the target pane" ) } func testFallsBackToAnotherTerminalInPaneWhenSelectedTabIsBrowser() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let terminalPanelId = workspace.focusedPanelId, let paneId = workspace.paneId(forPanelId: terminalPanelId), let browserPanel = workspace.newBrowserSurface(inPane: paneId, focus: true) else { XCTFail("Expected workspace browser setup to succeed") return } XCTAssertEqual(workspace.focusedPanelId, browserPanel.id) let sourcePanel = workspace.terminalPanelForConfigInheritance(inPane: paneId) XCTAssertEqual( sourcePanel?.id, terminalPanelId, "Expected inheritance to fall back to a terminal in the pane when browser is selected" ) } func testPreferredTerminalPanelWinsWhenProvided() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let terminalPanelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace with a terminal panel") return } let sourcePanel = workspace.terminalPanelForConfigInheritance(preferredPanelId: terminalPanelId) XCTAssertEqual(sourcePanel?.id, terminalPanelId) } func testPrefersLastFocusedTerminalWhenBrowserFocusedInDifferentPane() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let leftTerminalPanelId = workspace.focusedPanelId, let rightTerminalPanel = workspace.newTerminalSplit(from: leftTerminalPanelId, orientation: .horizontal), let rightPaneId = workspace.paneId(forPanelId: rightTerminalPanel.id) else { XCTFail("Expected split setup to succeed") return } workspace.focusPanel(leftTerminalPanelId) _ = workspace.newBrowserSurface(inPane: rightPaneId, focus: true) XCTAssertNotEqual(workspace.focusedPanelId, leftTerminalPanelId) let sourcePanel = workspace.terminalPanelForConfigInheritance(inPane: rightPaneId) XCTAssertEqual( sourcePanel?.id, leftTerminalPanelId, "Expected inheritance to prefer last focused terminal when browser is focused in another pane" ) } } @MainActor final class WorkspaceAttentionFlashTests: XCTestCase { func testMoveFocusDoesNotTriggerWholePaneFlashTokenWhenWholePaneModeEnabled() { let defaults = UserDefaults.standard let originalExperimentEnabled = defaults.object(forKey: TmuxOverlayExperimentSettings.enabledKey) let originalExperimentTarget = defaults.object(forKey: TmuxOverlayExperimentSettings.targetKey) defer { if let originalExperimentEnabled { defaults.set(originalExperimentEnabled, forKey: TmuxOverlayExperimentSettings.enabledKey) } else { defaults.removeObject(forKey: TmuxOverlayExperimentSettings.enabledKey) } if let originalExperimentTarget { defaults.set(originalExperimentTarget, forKey: TmuxOverlayExperimentSettings.targetKey) } else { defaults.removeObject(forKey: TmuxOverlayExperimentSettings.targetKey) } } defaults.set(true, forKey: TmuxOverlayExperimentSettings.enabledKey) defaults.set(TmuxOverlayExperimentTarget.bonsplitPane.rawValue, forKey: TmuxOverlayExperimentSettings.targetKey) let manager = TabManager() guard let workspace = manager.selectedWorkspace, let leftPanelId = workspace.focusedPanelId, let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else { XCTFail("Expected split terminal panels") return } XCTAssertEqual(workspace.focusedPanelId, rightPanel.id) XCTAssertEqual(workspace.tmuxWorkspaceFlashToken, 0) XCTAssertNil(workspace.tmuxWorkspaceFlashPanelId) workspace.moveFocus(direction: .left) XCTAssertEqual(workspace.focusedPanelId, leftPanelId) XCTAssertEqual( workspace.tmuxWorkspaceFlashToken, 0, "Expected moving focus left to avoid any workspace-pane flash" ) XCTAssertNil(workspace.tmuxWorkspaceFlashPanelId) workspace.moveFocus(direction: .right) XCTAssertEqual(workspace.focusedPanelId, rightPanel.id) XCTAssertEqual( workspace.tmuxWorkspaceFlashToken, 0, "Expected moving focus right to avoid any workspace-pane flash" ) XCTAssertNil(workspace.tmuxWorkspaceFlashPanelId) } func testMoveFocusSuppressesWorkspacePaneFlashWhenAnotherPaneOwnsUnreadAttention() { let appDelegate = AppDelegate.shared ?? AppDelegate() let manager = TabManager() let notificationStore = TerminalNotificationStore.shared let defaults = UserDefaults.standard let originalTabManager = appDelegate.tabManager let originalNotificationStore = appDelegate.notificationStore let originalExperimentEnabled = defaults.object(forKey: TmuxOverlayExperimentSettings.enabledKey) let originalExperimentTarget = defaults.object(forKey: TmuxOverlayExperimentSettings.targetKey) let originalAppFocusOverride = AppFocusState.overrideIsFocused defer { notificationStore.replaceNotificationsForTesting([]) notificationStore.resetNotificationDeliveryHandlerForTesting() appDelegate.tabManager = originalTabManager appDelegate.notificationStore = originalNotificationStore AppFocusState.overrideIsFocused = originalAppFocusOverride if let originalExperimentEnabled { defaults.set(originalExperimentEnabled, forKey: TmuxOverlayExperimentSettings.enabledKey) } else { defaults.removeObject(forKey: TmuxOverlayExperimentSettings.enabledKey) } if let originalExperimentTarget { defaults.set(originalExperimentTarget, forKey: TmuxOverlayExperimentSettings.targetKey) } else { defaults.removeObject(forKey: TmuxOverlayExperimentSettings.targetKey) } } notificationStore.replaceNotificationsForTesting([]) notificationStore.configureNotificationDeliveryHandlerForTesting { _, _ in } appDelegate.tabManager = manager appDelegate.notificationStore = notificationStore AppFocusState.overrideIsFocused = true defaults.set(true, forKey: TmuxOverlayExperimentSettings.enabledKey) defaults.set(TmuxOverlayExperimentTarget.bonsplitPane.rawValue, forKey: TmuxOverlayExperimentSettings.targetKey) guard let workspace = manager.selectedWorkspace, let leftPanelId = workspace.focusedPanelId, let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else { XCTFail("Expected split terminal panels") return } workspace.moveFocus(direction: .left) notificationStore.addNotification( tabId: workspace.id, surfaceId: leftPanelId, title: "Unread", subtitle: "", body: "Left pane owns notification attention" ) XCTAssertTrue( notificationStore.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: leftPanelId), "Expected the left pane to own visible notification attention before moving focus" ) let flashTokenBeforeNavigation = workspace.tmuxWorkspaceFlashToken workspace.moveFocus(direction: .right) XCTAssertEqual(workspace.focusedPanelId, rightPanel.id) XCTAssertEqual( workspace.tmuxWorkspaceFlashToken, flashTokenBeforeNavigation, "Expected navigation flash to be suppressed while another pane owns notification attention" ) } } @MainActor final class WorkspaceBrowserProfileSelectionTests: XCTestCase { private final class RejectingCreateTabDelegate: BonsplitDelegate { func splitTabBar(_ controller: BonsplitController, shouldCreateTab tab: Bonsplit.Tab, inPane pane: PaneID) -> Bool { false } } private final class RejectingSplitPaneDelegate: BonsplitDelegate { func splitTabBar(_ controller: BonsplitController, shouldSplitPane pane: PaneID, orientation: SplitOrientation) -> Bool { false } } func testNewBrowserSurfacePrefersSelectedBrowserProfileInTargetPane() throws { let workspace = Workspace() let profileA = try makeTemporaryBrowserProfile(named: "Alpha") let profileB = try makeTemporaryBrowserProfile(named: "Beta") let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) let browserA = try XCTUnwrap( workspace.newBrowserSurface( inPane: paneId, focus: true, preferredProfileID: profileA.id ) ) _ = try XCTUnwrap( workspace.newBrowserSplit( from: browserA.id, orientation: .horizontal, preferredProfileID: profileB.id, focus: true ) ) XCTAssertEqual( workspace.preferredBrowserProfileID, profileB.id, "Expected workspace preference to drift to the most recently created browser profile" ) let leftSurfaceId = try XCTUnwrap(workspace.surfaceIdFromPanelId(browserA.id)) workspace.bonsplitController.focusPane(paneId) workspace.bonsplitController.selectTab(leftSurfaceId) let created = try XCTUnwrap( workspace.newBrowserSurface( inPane: paneId, focus: false ) ) XCTAssertEqual( created.profileID, profileA.id, "Expected new browser creation to inherit the selected browser profile from the target pane" ) } func testNewBrowserSurfaceFailureDoesNotMutatePreferredProfile() throws { let workspace = Workspace() let preferredProfile = try makeTemporaryBrowserProfile(named: "Preferred") let unexpectedProfile = try makeTemporaryBrowserProfile(named: "Unexpected") let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) _ = try XCTUnwrap( workspace.newBrowserSurface( inPane: paneId, focus: false, preferredProfileID: preferredProfile.id ) ) XCTAssertEqual(workspace.preferredBrowserProfileID, preferredProfile.id) let rejectingDelegate = RejectingCreateTabDelegate() workspace.bonsplitController.delegate = rejectingDelegate let created = workspace.newBrowserSurface( inPane: paneId, focus: false, preferredProfileID: unexpectedProfile.id ) XCTAssertNil(created) XCTAssertEqual( workspace.preferredBrowserProfileID, preferredProfile.id, "Expected a failed browser creation to leave the workspace preferred profile unchanged" ) } func testNewBrowserSplitFailureDoesNotMutatePreferredProfile() throws { let workspace = Workspace() let preferredProfile = try makeTemporaryBrowserProfile(named: "Preferred") let unexpectedProfile = try makeTemporaryBrowserProfile(named: "Unexpected") let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) let browser = try XCTUnwrap( workspace.newBrowserSurface( inPane: paneId, focus: true, preferredProfileID: preferredProfile.id ) ) XCTAssertEqual(workspace.preferredBrowserProfileID, preferredProfile.id) let rejectingDelegate = RejectingSplitPaneDelegate() workspace.bonsplitController.delegate = rejectingDelegate let created = workspace.newBrowserSplit( from: browser.id, orientation: .horizontal, preferredProfileID: unexpectedProfile.id, focus: false ) XCTAssertNil(created) XCTAssertEqual( workspace.preferredBrowserProfileID, preferredProfile.id, "Expected a failed browser split to leave the workspace preferred profile unchanged" ) } } @MainActor final class WorkspacePanelGitBranchTests: XCTestCase { private func drainMainQueue() { let expectation = expectation(description: "drain main queue") DispatchQueue.main.async { expectation.fulfill() } wait(for: [expectation], timeout: 1.0) } func testBrowserSplitWithFocusFalsePreservesOriginalFocusedPanel() { let workspace = Workspace() guard let originalFocusedPanelId = workspace.focusedPanelId else { XCTFail("Expected initial focused panel") return } guard let browserSplitPanel = workspace.newBrowserSplit( from: originalFocusedPanelId, orientation: .horizontal, focus: false ) else { XCTFail("Expected browser split panel to be created") return } drainMainQueue() XCTAssertNotEqual(browserSplitPanel.id, originalFocusedPanelId) XCTAssertEqual( workspace.focusedPanelId, originalFocusedPanelId, "Expected non-focus browser split to preserve pre-split focus" ) } func testTerminalSplitWithFocusFalsePreservesOriginalFocusedPanel() { let workspace = Workspace() guard let originalFocusedPanelId = workspace.focusedPanelId else { XCTFail("Expected initial focused panel") return } guard let terminalSplitPanel = workspace.newTerminalSplit( from: originalFocusedPanelId, orientation: .horizontal, focus: false ) else { XCTFail("Expected terminal split panel to be created") return } drainMainQueue() XCTAssertNotEqual(terminalSplitPanel.id, originalFocusedPanelId) XCTAssertEqual( workspace.focusedPanelId, originalFocusedPanelId, "Expected non-focus terminal split to preserve pre-split focus" ) } func testDetachLastSurfaceLeavesWorkspaceTemporarilyEmptyForMoveFlow() { let workspace = Workspace() guard let panelId = workspace.focusedPanelId, let paneId = workspace.paneId(forPanelId: panelId) else { XCTFail("Expected initial panel and pane") return } XCTAssertEqual(workspace.panels.count, 1) #if DEBUG let baselineFocusReconcileDuringDetach = workspace.debugFocusReconcileScheduledDuringDetachCount #endif guard let detached = workspace.detachSurface(panelId: panelId) else { XCTFail("Expected detach of last surface to succeed") return } XCTAssertEqual(detached.panelId, panelId) XCTAssertTrue( workspace.panels.isEmpty, "Detaching the last surface should not auto-create a replacement panel" ) XCTAssertNil(workspace.surfaceIdFromPanelId(panelId)) XCTAssertEqual(workspace.bonsplitController.tabs(inPane: paneId).count, 0) drainMainQueue() drainMainQueue() #if DEBUG XCTAssertEqual( workspace.debugFocusReconcileScheduledDuringDetachCount, baselineFocusReconcileDuringDetach, "Detaching during cross-workspace moves should not schedule delayed source focus reconciliation" ) #endif let restoredPanelId = workspace.attachDetachedSurface(detached, inPane: paneId, focus: false) XCTAssertEqual(restoredPanelId, panelId) XCTAssertEqual(workspace.panels.count, 1) } func testDetachSurfaceWithRemainingPanelsSkipsDelayedFocusReconcile() { let workspace = Workspace() guard let originalPanelId = workspace.focusedPanelId, let movedPanel = workspace.newTerminalSplit(from: originalPanelId, orientation: .horizontal) else { XCTFail("Expected two panels before detach") return } drainMainQueue() drainMainQueue() #if DEBUG let baselineFocusReconcileDuringDetach = workspace.debugFocusReconcileScheduledDuringDetachCount #endif guard let detached = workspace.detachSurface(panelId: movedPanel.id) else { XCTFail("Expected detach to succeed") return } XCTAssertEqual(detached.panelId, movedPanel.id) XCTAssertEqual(workspace.panels.count, 1, "Expected source workspace to retain only the surviving panel") XCTAssertNotNil(workspace.panels[originalPanelId], "Expected the original panel to remain after detach") drainMainQueue() drainMainQueue() #if DEBUG XCTAssertEqual( workspace.debugFocusReconcileScheduledDuringDetachCount, baselineFocusReconcileDuringDetach, "Detaching into another workspace should not enqueue delayed source focus reconciliation" ) #endif } func testDetachAttachAcrossWorkspacesPreservesNonCustomPanelTitle() { let source = Workspace() guard let panelId = source.focusedPanelId else { XCTFail("Expected source focused panel") return } XCTAssertTrue(source.updatePanelTitle(panelId: panelId, title: "detached-runtime-title")) guard let detached = source.detachSurface(panelId: panelId) else { XCTFail("Expected detach to succeed") return } XCTAssertEqual(detached.cachedTitle, "detached-runtime-title") XCTAssertNil(detached.customTitle) XCTAssertEqual( detached.title, "detached-runtime-title", "Detached transfer should carry the cached non-custom title" ) let destination = Workspace() guard let destinationPane = destination.bonsplitController.allPaneIds.first else { XCTFail("Expected destination pane") return } let attachedPanelId = destination.attachDetachedSurface( detached, inPane: destinationPane, focus: false ) XCTAssertEqual(attachedPanelId, panelId) XCTAssertEqual(destination.panelTitle(panelId: panelId), "detached-runtime-title") guard let attachedTabId = destination.surfaceIdFromPanelId(panelId), let attachedTab = destination.bonsplitController.tab(attachedTabId) else { XCTFail("Expected attached tab mapping") return } XCTAssertEqual(attachedTab.title, "detached-runtime-title") XCTAssertFalse(attachedTab.hasCustomTitle) } func testBrowserSplitWithFocusFalseRecoversFromDelayedStaleSelection() { let workspace = Workspace() guard let originalFocusedPanelId = workspace.focusedPanelId else { XCTFail("Expected initial focused panel") return } guard let originalPaneId = workspace.paneId(forPanelId: originalFocusedPanelId) else { XCTFail("Expected focused pane for initial panel") return } guard let browserSplitPanel = workspace.newBrowserSplit( from: originalFocusedPanelId, orientation: .horizontal, focus: false ) else { XCTFail("Expected browser split panel to be created") return } guard let splitPaneId = workspace.paneId(forPanelId: browserSplitPanel.id), let splitTabId = workspace.surfaceIdFromPanelId(browserSplitPanel.id), let splitTab = workspace.bonsplitController .tabs(inPane: splitPaneId) .first(where: { $0.id == splitTabId }) else { XCTFail("Expected split pane/tab mapping") return } // Simulate one delayed stale split-selection callback from bonsplit. DispatchQueue.main.async { workspace.splitTabBar(workspace.bonsplitController, didSelectTab: splitTab, inPane: splitPaneId) } drainMainQueue() drainMainQueue() drainMainQueue() XCTAssertEqual( workspace.focusedPanelId, originalFocusedPanelId, "Expected non-focus split to reassert the pre-split focused panel" ) XCTAssertEqual( workspace.bonsplitController.focusedPaneId, originalPaneId, "Expected focused pane to converge back to the pre-split pane" ) XCTAssertEqual( workspace.bonsplitController.selectedTab(inPane: originalPaneId)?.id, workspace.surfaceIdFromPanelId(originalFocusedPanelId), "Expected selected tab to converge back to the pre-split focused panel" ) } func testBrowserSplitWithFocusFalseAllowsSubsequentExplicitFocusOnSplitPanel() { let workspace = Workspace() guard let originalFocusedPanelId = workspace.focusedPanelId else { XCTFail("Expected initial focused panel") return } guard let browserSplitPanel = workspace.newBrowserSplit( from: originalFocusedPanelId, orientation: .horizontal, focus: false ) else { XCTFail("Expected browser split panel to be created") return } workspace.focusPanel(browserSplitPanel.id) drainMainQueue() drainMainQueue() drainMainQueue() XCTAssertEqual( workspace.focusedPanelId, browserSplitPanel.id, "Expected explicit focus intent to keep the split panel focused" ) } func testNewTerminalSurfaceWithFocusFalsePreservesFocusedPanel() { let workspace = Workspace() guard let originalFocusedPanelId = workspace.focusedPanelId, let originalPaneId = workspace.paneId(forPanelId: originalFocusedPanelId) else { XCTFail("Expected initial focused panel and pane") return } guard let newPanel = workspace.newTerminalSurface(inPane: originalPaneId, focus: false) else { XCTFail("Expected terminal surface to be created") return } drainMainQueue() drainMainQueue() drainMainQueue() XCTAssertNotEqual(newPanel.id, originalFocusedPanelId) XCTAssertEqual( workspace.focusedPanelId, originalFocusedPanelId, "Expected non-focus terminal surface creation to preserve the existing focused panel" ) XCTAssertEqual( workspace.bonsplitController.selectedTab(inPane: originalPaneId)?.id, workspace.surfaceIdFromPanelId(originalFocusedPanelId), "Expected selected tab to stay on the original focused panel" ) } func testNewBrowserSurfaceWithFocusFalsePreservesFocusedPanel() { let workspace = Workspace() guard let originalFocusedPanelId = workspace.focusedPanelId, let originalPaneId = workspace.paneId(forPanelId: originalFocusedPanelId) else { XCTFail("Expected initial focused panel and pane") return } guard let newPanel = workspace.newBrowserSurface(inPane: originalPaneId, focus: false) else { XCTFail("Expected browser surface to be created") return } drainMainQueue() drainMainQueue() drainMainQueue() XCTAssertNotEqual(newPanel.id, originalFocusedPanelId) XCTAssertEqual( workspace.focusedPanelId, originalFocusedPanelId, "Expected non-focus browser surface creation to preserve the existing focused panel" ) XCTAssertEqual( workspace.bonsplitController.selectedTab(inPane: originalPaneId)?.id, workspace.surfaceIdFromPanelId(originalFocusedPanelId), "Expected selected tab to stay on the original focused panel" ) } func testClosingFocusedSplitRestoresBranchForRemainingFocusedPanel() { let workspace = Workspace() guard let firstPanelId = workspace.focusedPanelId else { XCTFail("Expected initial focused panel") return } workspace.updatePanelGitBranch(panelId: firstPanelId, branch: "main", isDirty: false) guard let secondPanel = workspace.newTerminalSplit(from: firstPanelId, orientation: .horizontal) else { XCTFail("Expected split panel to be created") return } workspace.updatePanelGitBranch(panelId: secondPanel.id, branch: "feature/bugfix", isDirty: true) XCTAssertEqual(workspace.focusedPanelId, secondPanel.id, "Expected split panel to be focused") XCTAssertEqual(workspace.gitBranch?.branch, "feature/bugfix") XCTAssertEqual(workspace.gitBranch?.isDirty, true) XCTAssertTrue(workspace.closePanel(secondPanel.id, force: true), "Expected split panel close to succeed") XCTAssertEqual(workspace.focusedPanelId, firstPanelId, "Expected surviving panel to become focused") XCTAssertEqual(workspace.gitBranch?.branch, "main") XCTAssertEqual(workspace.gitBranch?.isDirty, false) } func testSidebarGitBranchesFollowLeftToRightSplitOrder() { let workspace = Workspace() guard let leftPanelId = workspace.focusedPanelId else { XCTFail("Expected initial focused panel") return } workspace.updatePanelGitBranch(panelId: leftPanelId, branch: "main", isDirty: false) guard let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else { XCTFail("Expected split panel to be created") return } workspace.updatePanelGitBranch(panelId: rightPanel.id, branch: "feature/sidebar", isDirty: true) let ordered = workspace.sidebarGitBranchesInDisplayOrder() XCTAssertEqual(ordered.map(\.branch), ["main", "feature/sidebar"]) XCTAssertEqual(ordered.map(\.isDirty), [false, true]) } func testUpdatingFocusedPanelGitBranchWithSameStateDoesNotRepublishWorkspace() { let workspace = Workspace() guard let panelId = workspace.focusedPanelId else { XCTFail("Expected initial focused panel") return } var publishCount = 0 let cancellable = workspace.objectWillChange.sink { _ in publishCount += 1 } defer { cancellable.cancel() } workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false) let baselinePublishCount = publishCount XCTAssertGreaterThan( baselinePublishCount, 0, "Expected the first focused branch update to publish workspace changes" ) workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false) XCTAssertEqual( publishCount, baselinePublishCount, "Expected identical focused branch refreshes to avoid extra workspace publishes" ) } func testUpdatingFocusedPanelPullRequestWithSameStateDoesNotRepublishWorkspace() { let workspace = Workspace() guard let panelId = workspace.focusedPanelId else { XCTFail("Expected initial focused panel") return } workspace.updatePanelGitBranch(panelId: panelId, branch: "feature/sidebar-pr", isDirty: false) var publishCount = 0 let cancellable = workspace.objectWillChange.sink { _ in publishCount += 1 } defer { cancellable.cancel() } let pullRequestURL = URL(string: "https://github.com/manaflow-ai/cmux/pull/2388")! workspace.updatePanelPullRequest( panelId: panelId, number: 2388, label: "PR", url: pullRequestURL, status: .open, branch: "feature/sidebar-pr" ) let baselinePublishCount = publishCount XCTAssertGreaterThan( baselinePublishCount, 0, "Expected the first focused pull request update to publish workspace changes" ) workspace.updatePanelPullRequest( panelId: panelId, number: 2388, label: "PR", url: pullRequestURL, status: .open, branch: "feature/sidebar-pr" ) XCTAssertEqual( publishCount, baselinePublishCount, "Expected identical focused pull request refreshes to avoid extra workspace publishes" ) } func testSidebarObservationPublisherEmitsForFocusedGitBranchChangesOnlyOncePerState() { let workspace = Workspace() guard let panelId = workspace.focusedPanelId else { XCTFail("Expected initial focused panel") return } var publishCount = 0 let cancellable = workspace.sidebarObservationPublisher.sink { publishCount += 1 } defer { cancellable.cancel() } workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false) let baselinePublishCount = publishCount XCTAssertGreaterThan( baselinePublishCount, 0, "Expected focused git branch updates to invalidate sidebar rows" ) workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false) XCTAssertEqual( publishCount, baselinePublishCount, "Expected identical git metadata refreshes to be ignored by sidebar rows" ) } func testSidebarObservationPublisherIgnoresRemoteHeartbeatOnlyChanges() { let workspace = Workspace() var publishCount = 0 let cancellable = workspace.sidebarObservationPublisher.sink { publishCount += 1 } defer { cancellable.cancel() } workspace.remoteHeartbeatCount = 1 workspace.remoteLastHeartbeatAt = Date() XCTAssertEqual( publishCount, 0, "Expected non-visible remote heartbeat updates to avoid invalidating sidebar rows" ) } @MainActor func testSidebarPullRequestsTrackFocusedPanelOnly() { let workspace = Workspace() guard let firstPanelId = workspace.focusedPanelId, let paneId = workspace.paneId(forPanelId: firstPanelId), let secondPanel = workspace.newTerminalSurface(inPane: paneId, focus: false) else { XCTFail("Expected focused panel and a second panel") return } workspace.updatePanelGitBranch(panelId: firstPanelId, branch: "main", isDirty: false) workspace.updatePanelGitBranch(panelId: secondPanel.id, branch: "feature/sidebar-pr", isDirty: false) workspace.updatePanelPullRequest( panelId: secondPanel.id, number: 1629, label: "PR", url: URL(string: "https://github.com/manaflow-ai/cmux/pull/1629")!, status: .open ) XCTAssertNil(workspace.pullRequest) XCTAssertTrue( workspace.sidebarPullRequestsInDisplayOrder().isEmpty, "Expected background panel PRs to stay hidden while the focused panel has no PR" ) workspace.focusPanel(secondPanel.id) XCTAssertEqual( workspace.sidebarPullRequestsInDisplayOrder().map(\.number), [1629] ) } func testSidebarOrderingUsesPaneOrderThenTabOrderWithBranchDeduping() { let workspace = Workspace() guard let leftFirstPanelId = workspace.focusedPanelId, let leftPaneId = workspace.paneId(forPanelId: leftFirstPanelId), let rightFirstPanel = workspace.newTerminalSplit(from: leftFirstPanelId, orientation: .horizontal), let rightPaneId = workspace.paneId(forPanelId: rightFirstPanel.id), let leftSecondPanel = workspace.newTerminalSurface(inPane: leftPaneId, focus: false), let rightSecondPanel = workspace.newTerminalSurface(inPane: rightPaneId, focus: false) else { XCTFail("Expected panes and panels for ordering test") return } XCTAssertTrue(workspace.reorderSurface(panelId: leftFirstPanelId, toIndex: 0)) XCTAssertTrue(workspace.reorderSurface(panelId: leftSecondPanel.id, toIndex: 1)) XCTAssertTrue(workspace.reorderSurface(panelId: rightFirstPanel.id, toIndex: 0)) XCTAssertTrue(workspace.reorderSurface(panelId: rightSecondPanel.id, toIndex: 1)) workspace.updatePanelGitBranch(panelId: leftFirstPanelId, branch: "main", isDirty: false) workspace.updatePanelGitBranch(panelId: leftSecondPanel.id, branch: "feature/left", isDirty: false) workspace.updatePanelGitBranch(panelId: rightFirstPanel.id, branch: "main", isDirty: true) workspace.updatePanelGitBranch(panelId: rightSecondPanel.id, branch: "feature/right", isDirty: false) XCTAssertEqual( workspace.sidebarOrderedPanelIds(), [leftFirstPanelId, leftSecondPanel.id, rightFirstPanel.id, rightSecondPanel.id] ) let branches = workspace.sidebarGitBranchesInDisplayOrder() XCTAssertEqual(branches.map(\.branch), ["main", "feature/left", "feature/right"]) XCTAssertEqual(branches.map(\.isDirty), [true, false, false]) } func testSidebarBranchDirectoryEntriesStayStableAcrossFocusedSplitChanges() { let workspace = Workspace() let leftLiveDirectory = "/repo/left/live" let rightFocusedDirectory = "/repo/right/focused" let leftFocusedDirectory = "/repo/left/focused" let rightRequestedDirectory = "/repo/right/requested" guard let leftPanelId = workspace.focusedPanelId else { XCTFail("Expected initial focused panel") return } workspace.updatePanelDirectory(panelId: leftPanelId, directory: leftLiveDirectory) guard let rightSplitPanel = workspace.newTerminalSplit( from: leftPanelId, orientation: .horizontal, focus: false ), let rightPaneId = workspace.paneId(forPanelId: rightSplitPanel.id), let rightRequestedPanel = workspace.newTerminalSurface( inPane: rightPaneId, focus: false, workingDirectory: rightRequestedDirectory ) else { XCTFail("Expected right split panes for sidebar directory ordering test") return } let orderedPanelIds = workspace.sidebarOrderedPanelIds() XCTAssertEqual(orderedPanelIds, [leftPanelId, rightSplitPanel.id, rightRequestedPanel.id]) workspace.currentDirectory = rightFocusedDirectory let entriesWhenRightLooksFocused = workspace.sidebarBranchDirectoryEntriesInDisplayOrder( orderedPanelIds: orderedPanelIds ) workspace.currentDirectory = leftFocusedDirectory let entriesWhenLeftLooksFocused = workspace.sidebarBranchDirectoryEntriesInDisplayOrder( orderedPanelIds: orderedPanelIds ) XCTAssertEqual( entriesWhenRightLooksFocused, entriesWhenLeftLooksFocused, "Expected sidebar directory ordering to ignore focused-workspace cwd churn when panel-specific directories are available" ) XCTAssertEqual( entriesWhenRightLooksFocused.map(\.directory), [leftLiveDirectory, rightRequestedDirectory] ) } func testRemoteSidebarDirectoryCanonicalizationDedupesTildeAndAbsoluteHomePaths() { let workspace = Workspace() workspace.configureRemoteConnection( WorkspaceRemoteConfiguration( destination: "cmux-macmini", port: nil, identityFile: nil, sshOptions: [], localProxyPort: nil, relayPort: 64007, relayID: String(repeating: "a", count: 16), relayToken: String(repeating: "b", count: 64), localSocketPath: "/tmp/cmux-debug-test.sock", terminalStartupCommand: "ssh cmux-macmini" ), autoConnect: false ) let liveDirectory = "/home/remoteuser/project" let requestedDirectory = "~/project" guard let firstPanelId = workspace.focusedPanelId, let paneId = workspace.paneId(forPanelId: firstPanelId), let requestedPanel = workspace.newTerminalSurface( inPane: paneId, focus: false, workingDirectory: requestedDirectory ) else { XCTFail("Expected remote panels for sidebar directory canonicalization test") return } workspace.updatePanelDirectory(panelId: firstPanelId, directory: liveDirectory) let orderedPanelIds = workspace.sidebarOrderedPanelIds() XCTAssertEqual(orderedPanelIds, [firstPanelId, requestedPanel.id]) XCTAssertEqual( workspace.sidebarDirectoriesInDisplayOrder(orderedPanelIds: orderedPanelIds), [liveDirectory] ) XCTAssertEqual( workspace.sidebarBranchDirectoryEntriesInDisplayOrder(orderedPanelIds: orderedPanelIds).map(\.directory), [liveDirectory] ) } func testSidebarDerivedCollectionsMatchWhenUsingPrecomputedPanelOrder() { let workspace = Workspace() guard let leftFirstPanelId = workspace.focusedPanelId, let leftPaneId = workspace.paneId(forPanelId: leftFirstPanelId), let rightFirstPanel = workspace.newTerminalSplit(from: leftFirstPanelId, orientation: .horizontal), let rightPaneId = workspace.paneId(forPanelId: rightFirstPanel.id), let leftSecondPanel = workspace.newTerminalSurface(inPane: leftPaneId, focus: false), let rightSecondPanel = workspace.newTerminalSurface(inPane: rightPaneId, focus: false) else { XCTFail("Expected panes and panels for precomputed ordering test") return } workspace.updatePanelGitBranch(panelId: leftFirstPanelId, branch: "main", isDirty: false) workspace.updatePanelGitBranch(panelId: leftSecondPanel.id, branch: "feature/left", isDirty: true) workspace.updatePanelGitBranch(panelId: rightFirstPanel.id, branch: "release/right", isDirty: false) workspace.updatePanelDirectory(panelId: leftFirstPanelId, directory: "/repo/left/root") workspace.updatePanelDirectory(panelId: leftSecondPanel.id, directory: "/repo/left/feature") workspace.updatePanelDirectory(panelId: rightFirstPanel.id, directory: "/repo/right/root") workspace.updatePanelDirectory(panelId: rightSecondPanel.id, directory: "/repo/right/extra") workspace.updatePanelPullRequest( panelId: leftFirstPanelId, number: 101, label: "PR", url: URL(string: "https://github.com/manaflow-ai/cmux/pull/101")!, status: .open ) workspace.updatePanelPullRequest( panelId: rightFirstPanel.id, number: 18, label: "MR", url: URL(string: "https://gitlab.com/manaflow/cmux/-/merge_requests/18")!, status: .merged ) let orderedPanelIds = workspace.sidebarOrderedPanelIds() XCTAssertEqual( workspace.sidebarGitBranchesInDisplayOrder(orderedPanelIds: orderedPanelIds).map { "\($0.branch)|\($0.isDirty)" }, workspace.sidebarGitBranchesInDisplayOrder().map { "\($0.branch)|\($0.isDirty)" } ) XCTAssertEqual( workspace.sidebarBranchDirectoryEntriesInDisplayOrder(orderedPanelIds: orderedPanelIds), workspace.sidebarBranchDirectoryEntriesInDisplayOrder() ) XCTAssertEqual( workspace.sidebarPullRequestsInDisplayOrder(orderedPanelIds: orderedPanelIds), workspace.sidebarPullRequestsInDisplayOrder() ) } func testClosingPaneDropsBranchesFromClosedSide() { let workspace = Workspace() guard let leftPanelId = workspace.focusedPanelId, let leftPaneId = workspace.paneId(forPanelId: leftPanelId), let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else { XCTFail("Expected left/right split panes") return } workspace.updatePanelGitBranch(panelId: leftPanelId, branch: "branch1", isDirty: false) workspace.updatePanelGitBranch(panelId: rightPanel.id, branch: "branch2", isDirty: false) XCTAssertEqual(workspace.sidebarGitBranchesInDisplayOrder().map(\.branch), ["branch1", "branch2"]) XCTAssertTrue(workspace.bonsplitController.closePane(leftPaneId)) XCTAssertEqual(workspace.sidebarGitBranchesInDisplayOrder().map(\.branch), ["branch2"]) } } final class WorkspaceMountPolicyTests: XCTestCase { func testDefaultPolicyMountsOnlySelectedWorkspace() { let a = UUID() let b = UUID() let orderedTabIds: [UUID] = [a, b] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a], selected: b, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: false, maxMounted: WorkspaceMountPolicy.maxMountedWorkspaces ) XCTAssertEqual(next, [b]) } func testSelectedWorkspaceMovesToFrontAndMountCountIsBounded() { let a = UUID() let b = UUID() let c = UUID() let orderedTabIds: [UUID] = [a, b, c] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a, b, c], selected: c, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: false, maxMounted: 2 ) XCTAssertEqual(next, [c, a]) } func testMissingWorkspacesArePruned() { let a = UUID() let b = UUID() let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [b, a], selected: nil, pinnedIds: [], orderedTabIds: [a], isCycleHot: false, maxMounted: 2 ) XCTAssertEqual(next, [a]) } func testSelectedWorkspaceIsInsertedWhenAbsentFromCurrentCache() { let a = UUID() let b = UUID() let orderedTabIds: [UUID] = [a, b] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a], selected: b, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: false, maxMounted: 2 ) XCTAssertEqual(next, [b, a]) } func testMaxMountedIsClampedToAtLeastOne() { let a = UUID() let b = UUID() let orderedTabIds: [UUID] = [a, b] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a, b], selected: nil, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: false, maxMounted: 0 ) XCTAssertEqual(next, [a]) } func testCycleHotModeKeepsOnlySelectedWhenNoPinnedHandoff() { let a = UUID() let b = UUID() let c = UUID() let d = UUID() let orderedTabIds: [UUID] = [a, b, c, d] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a], selected: c, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: true, maxMounted: WorkspaceMountPolicy.maxMountedWorkspacesDuringCycle ) XCTAssertEqual(next, [c]) } func testCycleHotModeRespectsMaxMountedLimit() { let a = UUID() let b = UUID() let c = UUID() let orderedTabIds: [UUID] = [a, b, c] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a, b, c], selected: b, pinnedIds: [], orderedTabIds: orderedTabIds, isCycleHot: true, maxMounted: 2 ) XCTAssertEqual(next, [b]) } func testPinnedIdsAreRetainedAcrossReconcile() { let a = UUID() let b = UUID() let c = UUID() let orderedTabIds: [UUID] = [a, b, c] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a], selected: c, pinnedIds: [a], orderedTabIds: orderedTabIds, isCycleHot: false, maxMounted: 2 ) XCTAssertEqual(next, [c, a]) } func testCycleHotModeKeepsRetiringWorkspaceWhenPinned() { let a = UUID() let b = UUID() let orderedTabIds: [UUID] = [a, b] let next = WorkspaceMountPolicy.nextMountedWorkspaceIds( current: [a], selected: b, pinnedIds: [a], orderedTabIds: orderedTabIds, isCycleHot: true, maxMounted: WorkspaceMountPolicy.maxMountedWorkspacesDuringCycle ) XCTAssertEqual(next, [b, a]) } } @MainActor final class SidebarWorkspaceShortcutHintMetricsTests: XCTestCase { override func setUp() { super.setUp() SidebarWorkspaceShortcutHintMetrics.resetCacheForTesting() } override func tearDown() { SidebarWorkspaceShortcutHintMetrics.resetCacheForTesting() super.tearDown() } func testHintWidthCachesRepeatedMeasurements() { XCTAssertEqual(SidebarWorkspaceShortcutHintMetrics.measurementCountForTesting(), 0) let first = SidebarWorkspaceShortcutHintMetrics.hintWidth(for: "⌘1") XCTAssertGreaterThan(first, 0) XCTAssertEqual(SidebarWorkspaceShortcutHintMetrics.measurementCountForTesting(), 1) let second = SidebarWorkspaceShortcutHintMetrics.hintWidth(for: "⌘1") XCTAssertEqual(second, first) XCTAssertEqual(SidebarWorkspaceShortcutHintMetrics.measurementCountForTesting(), 1) _ = SidebarWorkspaceShortcutHintMetrics.hintWidth(for: "⌘2") XCTAssertEqual(SidebarWorkspaceShortcutHintMetrics.measurementCountForTesting(), 2) } func testSlotWidthAppliesMinimumAndDebugInset() { let nilLabelWidth = SidebarWorkspaceShortcutHintMetrics.slotWidth(label: nil, debugXOffset: 999) XCTAssertEqual(nilLabelWidth, 28) let base = SidebarWorkspaceShortcutHintMetrics.slotWidth(label: "⌘1", debugXOffset: 0) let widened = SidebarWorkspaceShortcutHintMetrics.slotWidth(label: "⌘1", debugXOffset: 10) XCTAssertGreaterThan(widened, base) } }