import XCTest import AppKit import SwiftUI import UniformTypeIdentifiers import WebKit import ObjectiveC.runtime import Bonsplit import UserNotifications #if canImport(cmux_DEV) @testable import cmux_DEV #elseif canImport(cmux) @testable import cmux #endif let lastSurfaceCloseShortcutDefaultsKey = "closeWorkspaceOnLastSurfaceShortcut" func drainMainQueue() { let expectation = XCTestExpectation(description: "drain main queue") DispatchQueue.main.async { expectation.fulfill() } XCTWaiter().wait(for: [expectation], timeout: 1.0) } @discardableResult private func waitForCondition( timeout: TimeInterval = 3.0, pollInterval: TimeInterval = 0.05, file: StaticString = #filePath, line: UInt = #line, _ condition: @escaping () -> Bool ) -> Bool { if condition() { return true } let expectation = XCTestExpectation(description: "wait for condition") let deadline = Date().addingTimeInterval(timeout) func poll() { if condition() { expectation.fulfill() return } guard Date() < deadline else { return } DispatchQueue.main.asyncAfter(deadline: .now() + pollInterval) { poll() } } DispatchQueue.main.async { poll() } let result = XCTWaiter().wait(for: [expectation], timeout: timeout + pollInterval + 0.1) if result != .completed { XCTFail("Timed out waiting for condition", file: file, line: line) return false } return true } private struct ProcessRunResult { let status: Int32 let stdout: String let stderr: String } private func splitNodes(in node: ExternalTreeNode) -> [ExternalSplitNode] { switch node { case .pane: return [] case .split(let split): return [split] + splitNodes(in: split.first) + splitNodes(in: split.second) } } private func runProcess( executablePath: String, arguments: [String], environment: [String: String]? = nil, currentDirectoryURL: URL? = nil ) throws -> ProcessRunResult { let process = Process() let stdoutPipe = Pipe() let stderrPipe = Pipe() process.executableURL = URL(fileURLWithPath: executablePath) process.arguments = arguments process.environment = environment process.currentDirectoryURL = currentDirectoryURL process.standardInput = FileHandle.nullDevice process.standardOutput = stdoutPipe process.standardError = stderrPipe try process.run() process.waitUntilExit() return ProcessRunResult( status: process.terminationStatus, stdout: String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "", stderr: String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" ) } private func runGit( _ arguments: [String], in directoryURL: URL, file: StaticString = #filePath, line: UInt = #line ) throws -> String { let result = try runProcess( executablePath: "/usr/bin/env", arguments: ["git"] + arguments, currentDirectoryURL: directoryURL ) XCTAssertEqual( result.status, 0, "git \(arguments.joined(separator: " ")) failed: \(result.stderr)", file: file, line: line ) return result.stdout } @MainActor final class TabManagerChildExitCloseTests: XCTestCase { func testChildExitOnLastPanelClosesSelectedWorkspaceAndKeepsIndexStable() { let manager = TabManager() let first = manager.tabs[0] let second = manager.addWorkspace() let third = manager.addWorkspace() manager.selectWorkspace(second) XCTAssertEqual(manager.selectedTabId, second.id) guard let secondPanelId = second.focusedPanelId else { XCTFail("Expected focused panel in selected workspace") return } manager.closePanelAfterChildExited(tabId: second.id, surfaceId: secondPanelId) XCTAssertEqual(manager.tabs.map(\.id), [first.id, third.id]) XCTAssertEqual( manager.selectedTabId, third.id, "Expected selection to stay at the same index after deleting the selected workspace" ) } func testChildExitOnLastPanelInLastWorkspaceSelectsPreviousWorkspace() { let manager = TabManager() let first = manager.tabs[0] let second = manager.addWorkspace() manager.selectWorkspace(second) XCTAssertEqual(manager.selectedTabId, second.id) guard let secondPanelId = second.focusedPanelId else { XCTFail("Expected focused panel in selected workspace") return } manager.closePanelAfterChildExited(tabId: second.id, surfaceId: secondPanelId) XCTAssertEqual(manager.tabs.map(\.id), [first.id]) XCTAssertEqual( manager.selectedTabId, first.id, "Expected previous workspace to be selected after closing the last-index workspace" ) } func testChildExitOnLastRemotePanelKeepsWorkspaceAndDemotesToLocal() throws { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let remotePanelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace with focused panel") return } workspace.configureRemoteConnection( WorkspaceRemoteConfiguration( destination: "cmux-macmini", port: nil, identityFile: nil, sshOptions: [], localProxyPort: nil, relayPort: 64015, relayID: String(repeating: "a", count: 16), relayToken: String(repeating: "b", count: 64), localSocketPath: "/tmp/cmux-debug-test.sock", terminalStartupCommand: "ssh cmux-macmini" ), autoConnect: false ) XCTAssertTrue(workspace.isRemoteWorkspace) XCTAssertTrue(workspace.isRemoteTerminalSurface(remotePanelId)) manager.closePanelAfterChildExited(tabId: workspace.id, surfaceId: remotePanelId) drainMainQueue() drainMainQueue() XCTAssertEqual(manager.tabs.count, 1) XCTAssertEqual(manager.selectedTabId, workspace.id) XCTAssertEqual(manager.tabs.first?.id, workspace.id) XCTAssertFalse(workspace.isRemoteWorkspace) XCTAssertNil(workspace.panels[remotePanelId]) XCTAssertEqual(workspace.panels.count, 1) XCTAssertNotEqual(workspace.focusedPanelId, remotePanelId) XCTAssertEqual(workspace.activeRemoteTerminalSessionCount, 0) } func testChildExitAfterRemoteSessionEndKeepsWorkspaceAndDemotesToLocal() throws { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let remotePanelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace with focused panel") return } workspace.configureRemoteConnection( WorkspaceRemoteConfiguration( destination: "cmux-macmini", port: nil, identityFile: nil, sshOptions: [], localProxyPort: nil, relayPort: 64016, relayID: String(repeating: "a", count: 16), relayToken: String(repeating: "b", count: 64), localSocketPath: "/tmp/cmux-debug-test.sock", terminalStartupCommand: "ssh cmux-macmini" ), autoConnect: false ) workspace.markRemoteTerminalSessionEnded(surfaceId: remotePanelId, relayPort: 64016) XCTAssertFalse(workspace.isRemoteWorkspace) manager.closePanelAfterChildExited(tabId: workspace.id, surfaceId: remotePanelId) drainMainQueue() drainMainQueue() XCTAssertEqual(manager.tabs.count, 1) XCTAssertEqual(manager.selectedTabId, workspace.id) XCTAssertEqual(manager.tabs.first?.id, workspace.id) XCTAssertFalse(workspace.isRemoteWorkspace) XCTAssertNil(workspace.panels[remotePanelId]) XCTAssertEqual(workspace.panels.count, 1) XCTAssertNotEqual(workspace.focusedPanelId, remotePanelId) XCTAssertEqual(workspace.activeRemoteTerminalSessionCount, 0) } func testChildExitOnNonLastPanelClosesOnlyPanel() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let initialPanelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace with focused panel") return } guard let splitPanel = workspace.newTerminalSplit(from: initialPanelId, orientation: .horizontal) else { XCTFail("Expected split terminal panel to be created") return } let panelCountBefore = workspace.panels.count manager.closePanelAfterChildExited(tabId: workspace.id, surfaceId: splitPanel.id) XCTAssertEqual(manager.tabs.count, 1) XCTAssertEqual(manager.tabs.first?.id, workspace.id) XCTAssertEqual(workspace.panels.count, panelCountBefore - 1) XCTAssertNotNil(workspace.panels[initialPanelId], "Expected sibling panel to remain") } } @MainActor final class TabManagerWorkspaceOwnershipTests: XCTestCase { func testCloseWorkspaceIgnoresWorkspaceNotOwnedByManager() { let manager = TabManager() _ = manager.addWorkspace() let initialTabIds = manager.tabs.map(\.id) let initialSelectedTabId = manager.selectedTabId let externalWorkspace = Workspace(title: "External workspace") let externalPanelCountBefore = externalWorkspace.panels.count let externalPanelTitlesBefore = externalWorkspace.panelTitles manager.closeWorkspace(externalWorkspace) XCTAssertEqual(manager.tabs.map(\.id), initialTabIds) XCTAssertEqual(manager.selectedTabId, initialSelectedTabId) XCTAssertEqual(externalWorkspace.panels.count, externalPanelCountBefore) XCTAssertEqual(externalWorkspace.panelTitles, externalPanelTitlesBefore) } } @MainActor final class TabManagerPullRequestProbeTests: XCTestCase { func testGitHubRepositorySlugsPrioritizeUpstreamThenOriginAndDeduplicate() { let output = """ origin https://github.com/austinwang/cmux.git (fetch) origin https://github.com/austinwang/cmux.git (push) upstream git@github.com:manaflow-ai/cmux.git (fetch) upstream git@github.com:manaflow-ai/cmux.git (push) backup ssh://git@github.com/manaflow-ai/cmux.git (fetch) mirror https://gitlab.com/manaflow-ai/cmux.git (fetch) """ XCTAssertEqual( TabManager.githubRepositorySlugs(fromGitRemoteVOutput: output), ["manaflow-ai/cmux", "austinwang/cmux"] ) } func testPreferredPullRequestPrefersOpenOverMergedAndClosed() { let candidates = [ TabManager.GitHubPullRequestProbeItem( number: 1889, state: "MERGED", url: "https://github.com/manaflow-ai/cmux/pull/1889", updatedAt: "2026-03-20T18:00:00Z" ), TabManager.GitHubPullRequestProbeItem( number: 1891, state: "OPEN", url: "https://github.com/manaflow-ai/cmux/pull/1891", updatedAt: "2026-03-19T18:00:00Z" ), TabManager.GitHubPullRequestProbeItem( number: 1800, state: "CLOSED", url: "https://github.com/manaflow-ai/cmux/pull/1800", updatedAt: "2026-03-21T18:00:00Z" ), ] XCTAssertEqual( TabManager.preferredPullRequest(from: candidates), candidates[1] ) } func testPreferredPullRequestPrefersMostRecentlyUpdatedWithinSameStatus() { let olderOpen = TabManager.GitHubPullRequestProbeItem( number: 1880, state: "OPEN", url: "https://github.com/manaflow-ai/cmux/pull/1880", updatedAt: "2026-03-18T18:00:00Z" ) let newerOpen = TabManager.GitHubPullRequestProbeItem( number: 1890, state: "OPEN", url: "https://github.com/manaflow-ai/cmux/pull/1890", updatedAt: "2026-03-20T18:00:00Z" ) XCTAssertEqual( TabManager.preferredPullRequest(from: [olderOpen, newerOpen]), newerOpen ) } func testPreferredPullRequestIgnoresMalformedCandidates() { let valid = TabManager.GitHubPullRequestProbeItem( number: 1888, state: "OPEN", url: "https://github.com/manaflow-ai/cmux/pull/1888", updatedAt: "2026-03-20T18:00:00Z" ) XCTAssertEqual( TabManager.preferredPullRequest(from: [ TabManager.GitHubPullRequestProbeItem( number: 9999, state: "WHATEVER", url: "https://github.com/manaflow-ai/cmux/pull/9999", updatedAt: "2026-03-21T18:00:00Z" ), TabManager.GitHubPullRequestProbeItem( number: 10000, state: "OPEN", url: "not a url", updatedAt: "2026-03-21T18:00:00Z" ), valid, ]), valid ) } func testShouldSkipWorkspacePullRequestLookupOnlyForExactMainAndMaster() { XCTAssertTrue(TabManager.shouldSkipWorkspacePullRequestLookup(branch: "main")) XCTAssertTrue(TabManager.shouldSkipWorkspacePullRequestLookup(branch: "master")) XCTAssertTrue(TabManager.shouldSkipWorkspacePullRequestLookup(branch: " master \n")) XCTAssertFalse(TabManager.shouldSkipWorkspacePullRequestLookup(branch: "Main")) XCTAssertFalse(TabManager.shouldSkipWorkspacePullRequestLookup(branch: "mainline")) XCTAssertFalse(TabManager.shouldSkipWorkspacePullRequestLookup(branch: "feature/main")) XCTAssertFalse(TabManager.shouldSkipWorkspacePullRequestLookup(branch: "release/master-fix")) } func testTrackedWorkspaceGitMetadataPollCandidatesIncludeMainAndMasterPanels() throws { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let mainPanelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace with focused panel") return } guard let masterPanel = workspace.newTerminalSplit(from: mainPanelId, orientation: .horizontal), let featurePanel = workspace.newTerminalSplit(from: mainPanelId, orientation: .vertical), let mainlinePanel = workspace.newTerminalSplit(from: mainPanelId, orientation: .horizontal) else { XCTFail("Expected split panels to be created") return } let staleURL = try XCTUnwrap(URL(string: "https://github.com/manaflow-ai/cmux/pull/371")) workspace.updatePanelGitBranch(panelId: mainPanelId, branch: "main", isDirty: false) workspace.updatePanelPullRequest( panelId: mainPanelId, number: 371, label: "PR", url: staleURL, status: .open, branch: "main" ) workspace.updatePanelGitBranch(panelId: masterPanel.id, branch: "master", isDirty: false) workspace.updatePanelGitBranch(panelId: featurePanel.id, branch: "feature/sidebar-pr", isDirty: false) workspace.updatePanelGitBranch(panelId: mainlinePanel.id, branch: "mainline", isDirty: false) XCTAssertEqual( manager.trackedWorkspaceGitMetadataPollCandidatePanelIdsForTesting(workspaceId: workspace.id), Set([mainPanelId, masterPanel.id, featurePanel.id, mainlinePanel.id]) ) } func testTrackedWorkspaceGitMetadataPollCandidatesIncludeFocusedFallbackOnMain() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let panelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace with focused panel") return } workspace.gitBranch = SidebarGitBranchState(branch: "main", isDirty: false) XCTAssertEqual( manager.trackedWorkspaceGitMetadataPollCandidatePanelIdsForTesting(workspaceId: workspace.id), Set([panelId]) ) workspace.gitBranch = SidebarGitBranchState(branch: "feature/sidebar-pr", isDirty: false) XCTAssertEqual( manager.trackedWorkspaceGitMetadataPollCandidatePanelIdsForTesting(workspaceId: workspace.id), Set([panelId]) ) } func testPeriodicWorkspaceGitMetadataRefreshUpdatesMainWorkspaceAfterCheckoutToFeatureBranch() throws { let fileManager = FileManager.default let repoURL = fileManager.temporaryDirectory.appendingPathComponent("cmux-git-main-refresh-\(UUID().uuidString)") try fileManager.createDirectory(at: repoURL, withIntermediateDirectories: true) defer { try? fileManager.removeItem(at: repoURL) } try runGit(["init", "-b", "main"], in: repoURL) try runGit(["config", "user.name", "cmux tests"], in: repoURL) try runGit(["config", "user.email", "cmux@example.invalid"], in: repoURL) try "seed\n".write( to: repoURL.appendingPathComponent("README.md"), atomically: true, encoding: .utf8 ) try runGit(["add", "README.md"], in: repoURL) try runGit(["commit", "-m", "Initial commit"], in: repoURL) let manager = TabManager() guard let workspace = manager.selectedWorkspace, let panelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace with focused panel") return } workspace.updatePanelDirectory(panelId: panelId, directory: repoURL.path) workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false) XCTAssertEqual( manager.trackedWorkspaceGitMetadataPollCandidatePanelIdsForTesting(workspaceId: workspace.id), Set([panelId]) ) try runGit(["checkout", "-b", "feature/sidebar-live-refresh"], in: repoURL) manager.refreshTrackedWorkspaceGitMetadataForTesting() XCTAssertTrue( waitForCondition { workspace.panelGitBranches[panelId]?.branch == "feature/sidebar-live-refresh" } ) XCTAssertEqual(workspace.gitBranch?.branch, "feature/sidebar-live-refresh") } func testResolvedCommandPathFallsBackOutsideAppPATH() throws { let fileManager = FileManager.default let tempDir = fileManager.temporaryDirectory.appendingPathComponent( "cmux-command-path-\(UUID().uuidString)", isDirectory: true ) try fileManager.createDirectory(at: tempDir, withIntermediateDirectories: true) defer { try? fileManager.removeItem(at: tempDir) } let executableName = "cmux-gh-test-\(UUID().uuidString)" let executableURL = tempDir.appendingPathComponent(executableName) try """ #!/bin/sh exit 0 """.write(to: executableURL, atomically: true, encoding: .utf8) try fileManager.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executableURL.path) XCTAssertEqual( TabManager.resolvedCommandPathForTesting( executable: executableName, environment: ["PATH": "/usr/bin:/bin"], fallbackDirectories: [tempDir.path] ), executableURL.path ) } func testPeriodicWorkspaceGitMetadataRefreshClearsStalePullRequestAfterBranchReset() throws { let fileManager = FileManager.default let repoURL = fileManager.temporaryDirectory.appendingPathComponent("cmux-git-refresh-\(UUID().uuidString)") try fileManager.createDirectory(at: repoURL, withIntermediateDirectories: true) defer { try? fileManager.removeItem(at: repoURL) } try runGit(["init", "-b", "main"], in: repoURL) try runGit(["config", "user.name", "cmux tests"], in: repoURL) try runGit(["config", "user.email", "cmux@example.invalid"], in: repoURL) try "seed\n".write( to: repoURL.appendingPathComponent("README.md"), atomically: true, encoding: .utf8 ) try runGit(["add", "README.md"], in: repoURL) try runGit(["commit", "-m", "Initial commit"], in: repoURL) try runGit(["checkout", "-b", "feature/sidebar-pr"], in: repoURL) let manager = TabManager() guard let workspace = manager.selectedWorkspace, let panelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace with focused panel") return } workspace.updatePanelDirectory(panelId: panelId, directory: repoURL.path) workspace.updatePanelGitBranch(panelId: panelId, branch: "feature/sidebar-pr", isDirty: false) workspace.updatePanelPullRequest( panelId: panelId, number: 1052, label: "PR", url: try XCTUnwrap(URL(string: "https://github.com/manaflow-ai/cmux/pull/1052")), status: .open, branch: "feature/sidebar-pr" ) XCTAssertEqual(workspace.panelGitBranches[panelId]?.branch, "feature/sidebar-pr") XCTAssertEqual(workspace.panelPullRequests[panelId]?.number, 1052) XCTAssertEqual(workspace.sidebarPullRequestsInDisplayOrder().map(\.number), [1052]) try runGit(["checkout", "main"], in: repoURL) manager.refreshTrackedWorkspaceGitMetadataForTesting() XCTAssertTrue( waitForCondition { workspace.panelGitBranches[panelId]?.branch == "main" && workspace.panelPullRequests[panelId] == nil } ) XCTAssertEqual(workspace.gitBranch?.branch, "main") XCTAssertNil(workspace.pullRequest) XCTAssertTrue(workspace.sidebarPullRequestsInDisplayOrder().isEmpty) } } @MainActor final class TabManagerCloseWorkspacesWithConfirmationTests: XCTestCase { func testCloseWorkspacesWithConfirmationPromptsOnceAndClosesAcceptedWorkspaces() { let manager = TabManager() let second = manager.addWorkspace() let third = manager.addWorkspace() manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha") manager.setCustomTitle(tabId: second.id, title: "Beta") manager.setCustomTitle(tabId: third.id, title: "Gamma") var prompts: [(title: String, message: String, acceptCmdD: Bool)] = [] manager.confirmCloseHandler = { title, message, acceptCmdD in prompts.append((title, message, acceptCmdD)) return true } manager.closeWorkspacesWithConfirmation([manager.tabs[0].id, second.id], allowPinned: true) let expectedMessage = String( format: String( localized: "dialog.closeWorkspaces.message", defaultValue: "This will close %1$lld workspaces and all of their panels:\n%2$@" ), locale: .current, Int64(2), "• Alpha\n• Beta" ) XCTAssertEqual(prompts.count, 1, "Expected a single confirmation prompt for multi-close") XCTAssertEqual( prompts.first?.title, String(localized: "dialog.closeWorkspaces.title", defaultValue: "Close workspaces?") ) XCTAssertEqual(prompts.first?.message, expectedMessage) XCTAssertEqual(prompts.first?.acceptCmdD, false) XCTAssertEqual(manager.tabs.map(\.title), ["Gamma"]) } func testCloseWorkspacesWithConfirmationKeepsWorkspacesWhenCancelled() { let manager = TabManager() let second = manager.addWorkspace() manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha") manager.setCustomTitle(tabId: second.id, title: "Beta") var prompts: [(title: String, message: String, acceptCmdD: Bool)] = [] manager.confirmCloseHandler = { title, message, acceptCmdD in prompts.append((title, message, acceptCmdD)) return false } manager.closeWorkspacesWithConfirmation([manager.tabs[0].id, second.id], allowPinned: true) let expectedMessage = String( format: String( localized: "dialog.closeWorkspacesWindow.message", defaultValue: "This will close the current window, its %1$lld workspaces, and all of their panels:\n%2$@" ), locale: .current, Int64(2), "• Alpha\n• Beta" ) XCTAssertEqual(prompts.count, 1) XCTAssertEqual( prompts.first?.title, String(localized: "dialog.closeWindow.title", defaultValue: "Close window?") ) XCTAssertEqual(prompts.first?.message, expectedMessage) XCTAssertEqual(prompts.first?.acceptCmdD, true) XCTAssertEqual(manager.tabs.map(\.title), ["Alpha", "Beta"]) } func testCloseCurrentWorkspaceWithConfirmationUsesSidebarMultiSelection() { let manager = TabManager() let second = manager.addWorkspace() let third = manager.addWorkspace() manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha") manager.setCustomTitle(tabId: second.id, title: "Beta") manager.setCustomTitle(tabId: third.id, title: "Gamma") manager.selectWorkspace(second) manager.setSidebarSelectedWorkspaceIds([manager.tabs[0].id, second.id]) var prompts: [(title: String, message: String, acceptCmdD: Bool)] = [] manager.confirmCloseHandler = { title, message, acceptCmdD in prompts.append((title, message, acceptCmdD)) return false } manager.closeCurrentWorkspaceWithConfirmation() let expectedMessage = String( format: String( localized: "dialog.closeWorkspaces.message", defaultValue: "This will close %1$lld workspaces and all of their panels:\n%2$@" ), locale: .current, Int64(2), "• Alpha\n• Beta" ) XCTAssertEqual(prompts.count, 1, "Expected Cmd+Shift+W path to reuse the multi-close summary dialog") XCTAssertEqual( prompts.first?.title, String(localized: "dialog.closeWorkspaces.title", defaultValue: "Close workspaces?") ) XCTAssertEqual(prompts.first?.message, expectedMessage) XCTAssertEqual(prompts.first?.acceptCmdD, false) XCTAssertEqual(manager.tabs.map(\.title), ["Alpha", "Beta", "Gamma"]) } } @MainActor final class TabManagerCloseCurrentPanelTests: XCTestCase { func testRuntimeCloseSkipsConfirmationWhenShellReportsPromptIdle() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let panelId = workspace.focusedPanelId, let terminalPanel = workspace.terminalPanel(for: panelId) else { XCTFail("Expected selected workspace and focused terminal panel") return } terminalPanel.surface.setNeedsConfirmCloseOverrideForTesting(true) workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) var promptCount = 0 manager.confirmCloseHandler = { _, _, _ in promptCount += 1 return false } manager.closeRuntimeSurfaceWithConfirmation(tabId: workspace.id, surfaceId: panelId) drainMainQueue() drainMainQueue() XCTAssertEqual(promptCount, 0, "Runtime closes should honor prompt-idle shell state") XCTAssertNil(workspace.panels[panelId], "Expected the original panel to close") XCTAssertEqual(workspace.panels.count, 1, "Expected a replacement surface after closing the last panel") } func testRuntimeClosePromptsWhenShellReportsRunningCommand() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let panelId = workspace.focusedPanelId, let terminalPanel = workspace.terminalPanel(for: panelId) else { XCTFail("Expected selected workspace and focused terminal panel") return } terminalPanel.surface.setNeedsConfirmCloseOverrideForTesting(false) workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) var promptCount = 0 manager.confirmCloseHandler = { _, _, _ in promptCount += 1 return false } manager.closeRuntimeSurfaceWithConfirmation(tabId: workspace.id, surfaceId: panelId) XCTAssertEqual(promptCount, 1, "Running commands should still require confirmation") XCTAssertNotNil(workspace.panels[panelId], "Prompt rejection should keep the original panel open") } func testCloseCurrentPanelClosesWorkspaceWhenItOwnsTheLastSurface() { let manager = TabManager() let firstWorkspace = manager.tabs[0] let secondWorkspace = manager.addWorkspace() manager.selectWorkspace(secondWorkspace) guard let secondPanelId = secondWorkspace.focusedPanelId else { XCTFail("Expected focused panel in selected workspace") return } XCTAssertEqual(manager.selectedTabId, secondWorkspace.id) XCTAssertEqual(secondWorkspace.panels.count, 1) manager.closeCurrentPanelWithConfirmation() drainMainQueue() drainMainQueue() XCTAssertEqual(manager.tabs.map(\.id), [firstWorkspace.id]) XCTAssertEqual(manager.selectedTabId, firstWorkspace.id) XCTAssertNil(secondWorkspace.panels[secondPanelId]) XCTAssertTrue(secondWorkspace.panels.isEmpty) } func testCloseCurrentPanelPromptsBeforeClosingPinnedWorkspaceLastSurface() { let manager = TabManager() _ = manager.tabs[0] let pinnedWorkspace = manager.addWorkspace() manager.setPinned(pinnedWorkspace, pinned: true) manager.selectWorkspace(pinnedWorkspace) guard let pinnedPanelId = pinnedWorkspace.focusedPanelId else { XCTFail("Expected focused panel in pinned workspace") return } XCTAssertEqual(manager.selectedTabId, pinnedWorkspace.id) XCTAssertEqual(pinnedWorkspace.panels.count, 1) var prompts: [(title: String, message: String, acceptCmdD: Bool)] = [] manager.confirmCloseHandler = { title, message, acceptCmdD in prompts.append((title, message, acceptCmdD)) return false } manager.closeCurrentPanelWithConfirmation() drainMainQueue() drainMainQueue() XCTAssertEqual(prompts.count, 1) XCTAssertEqual( prompts.first?.title, String(localized: "dialog.closePinnedWorkspace.title", defaultValue: "Close pinned workspace?") ) XCTAssertEqual( prompts.first?.message, String( localized: "dialog.closePinnedWorkspace.message", defaultValue: "This workspace is pinned. Closing it will close the workspace and all of its panels." ) ) XCTAssertEqual(prompts.first?.acceptCmdD, false) XCTAssertEqual(manager.tabs.count, 2) XCTAssertTrue(manager.tabs.contains(where: { $0.id == pinnedWorkspace.id })) XCTAssertEqual(manager.selectedTabId, pinnedWorkspace.id) XCTAssertNotNil(pinnedWorkspace.panels[pinnedPanelId]) XCTAssertEqual(pinnedWorkspace.panels.count, 1) } func testCloseCurrentPanelClosesPinnedWorkspaceAfterConfirmation() { let manager = TabManager() let firstWorkspace = manager.tabs[0] let pinnedWorkspace = manager.addWorkspace() manager.setPinned(pinnedWorkspace, pinned: true) manager.selectWorkspace(pinnedWorkspace) guard let pinnedPanelId = pinnedWorkspace.focusedPanelId else { XCTFail("Expected focused panel in pinned workspace") return } manager.confirmCloseHandler = { _, _, _ in true } manager.closeCurrentPanelWithConfirmation() drainMainQueue() drainMainQueue() XCTAssertEqual(manager.tabs.map(\.id), [firstWorkspace.id]) XCTAssertEqual(manager.selectedTabId, firstWorkspace.id) XCTAssertNil(pinnedWorkspace.panels[pinnedPanelId]) XCTAssertTrue(pinnedWorkspace.panels.isEmpty) } func testCloseCurrentPanelKeepsWorkspaceOpenWhenKeepWorkspaceOpenPreferenceIsEnabled() { let defaults = UserDefaults.standard let originalSetting = defaults.object(forKey: lastSurfaceCloseShortcutDefaultsKey) defaults.set(false, forKey: lastSurfaceCloseShortcutDefaultsKey) defer { if let originalSetting { defaults.set(originalSetting, forKey: lastSurfaceCloseShortcutDefaultsKey) } else { defaults.removeObject(forKey: lastSurfaceCloseShortcutDefaultsKey) } } let manager = TabManager() guard let workspace = manager.selectedWorkspace, let initialPanelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace and focused panel") return } let initialWorkspaceId = workspace.id manager.closeCurrentPanelWithConfirmation() drainMainQueue() drainMainQueue() XCTAssertEqual(manager.tabs.count, 1) XCTAssertEqual(manager.selectedTabId, initialWorkspaceId) XCTAssertEqual(manager.tabs.first?.id, initialWorkspaceId) XCTAssertNil(workspace.panels[initialPanelId]) XCTAssertEqual(workspace.panels.count, 1) XCTAssertNotEqual(workspace.focusedPanelId, initialPanelId) } func testClosePanelButtonClosesWorkspaceWhenItOwnsTheLastSurface() { let manager = TabManager() let firstWorkspace = manager.tabs[0] let secondWorkspace = manager.addWorkspace() manager.selectWorkspace(secondWorkspace) guard let secondPanelId = secondWorkspace.focusedPanelId else { XCTFail("Expected focused panel in selected workspace") return } XCTAssertEqual(manager.selectedTabId, secondWorkspace.id) XCTAssertEqual(secondWorkspace.panels.count, 1) guard let secondSurfaceId = secondWorkspace.surfaceIdFromPanelId(secondPanelId) else { XCTFail("Expected bonsplit surface ID for focused panel") return } secondWorkspace.markExplicitClose(surfaceId: secondSurfaceId) XCTAssertFalse(secondWorkspace.closePanel(secondPanelId)) drainMainQueue() drainMainQueue() XCTAssertEqual(manager.tabs.map(\.id), [firstWorkspace.id]) XCTAssertEqual(manager.selectedTabId, firstWorkspace.id) XCTAssertNil(secondWorkspace.panels[secondPanelId]) XCTAssertTrue(secondWorkspace.panels.isEmpty) } func testClosePanelButtonStillClosesWorkspaceWhenKeepWorkspaceOpenPreferenceIsEnabled() { let defaults = UserDefaults.standard let originalSetting = defaults.object(forKey: lastSurfaceCloseShortcutDefaultsKey) defaults.set(false, forKey: lastSurfaceCloseShortcutDefaultsKey) defer { if let originalSetting { defaults.set(originalSetting, forKey: lastSurfaceCloseShortcutDefaultsKey) } else { defaults.removeObject(forKey: lastSurfaceCloseShortcutDefaultsKey) } } let manager = TabManager() let firstWorkspace = manager.tabs[0] let secondWorkspace = manager.addWorkspace() manager.selectWorkspace(secondWorkspace) guard let secondPanelId = secondWorkspace.focusedPanelId else { XCTFail("Expected focused panel in selected workspace") return } guard let secondSurfaceId = secondWorkspace.surfaceIdFromPanelId(secondPanelId) else { XCTFail("Expected bonsplit surface ID for focused panel") return } secondWorkspace.markExplicitClose(surfaceId: secondSurfaceId) XCTAssertFalse(secondWorkspace.closePanel(secondPanelId)) drainMainQueue() drainMainQueue() XCTAssertEqual(manager.tabs.map(\.id), [firstWorkspace.id]) XCTAssertEqual(manager.selectedTabId, firstWorkspace.id) XCTAssertNil(secondWorkspace.panels[secondPanelId]) XCTAssertTrue(secondWorkspace.panels.isEmpty) } func testGenericClosePanelKeepsWorkspaceOpenWithoutExplicitCloseMarker() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let initialPanelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace and focused panel") return } let initialWorkspaceId = workspace.id XCTAssertEqual(manager.tabs.count, 1) XCTAssertEqual(workspace.panels.count, 1) XCTAssertTrue(workspace.closePanel(initialPanelId)) drainMainQueue() drainMainQueue() XCTAssertEqual(manager.tabs.count, 1) XCTAssertEqual(manager.selectedTabId, initialWorkspaceId) XCTAssertEqual(manager.tabs.first?.id, initialWorkspaceId) XCTAssertNil(workspace.panels[initialPanelId]) XCTAssertEqual(workspace.panels.count, 1) XCTAssertNotEqual(workspace.focusedPanelId, initialPanelId) } func testCloseCurrentPanelIgnoresStaleSurfaceId() { let manager = TabManager() let firstWorkspace = manager.tabs[0] let secondWorkspace = manager.addWorkspace() manager.closePanelWithConfirmation(tabId: secondWorkspace.id, surfaceId: UUID()) XCTAssertEqual(manager.tabs.map(\.id), [firstWorkspace.id, secondWorkspace.id]) } func testCloseCurrentPanelClearsNotificationsForClosedSurface() { let appDelegate = AppDelegate.shared ?? AppDelegate() let manager = TabManager() let store = TerminalNotificationStore.shared let originalTabManager = appDelegate.tabManager let originalNotificationStore = appDelegate.notificationStore store.replaceNotificationsForTesting([]) store.configureNotificationDeliveryHandlerForTesting { _, _ in } appDelegate.tabManager = manager appDelegate.notificationStore = store defer { store.replaceNotificationsForTesting([]) store.resetNotificationDeliveryHandlerForTesting() appDelegate.tabManager = originalTabManager appDelegate.notificationStore = originalNotificationStore } guard let workspace = manager.selectedWorkspace, let initialPanelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace and focused panel") return } store.addNotification( tabId: workspace.id, surfaceId: initialPanelId, title: "Unread", subtitle: "", body: "" ) XCTAssertTrue(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: initialPanelId)) manager.closeCurrentPanelWithConfirmation() drainMainQueue() drainMainQueue() XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: initialPanelId)) } } @MainActor final class TabManagerNotificationFocusTests: XCTestCase { func testFocusTabFromNotificationClearsSplitZoomBeforeFocusingTargetPanel() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let leftPanelId = workspace.focusedPanelId, let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else { XCTFail("Expected split setup to succeed") return } workspace.focusPanel(leftPanelId) XCTAssertTrue(workspace.toggleSplitZoom(panelId: leftPanelId), "Expected split zoom to enable") XCTAssertTrue(workspace.bonsplitController.isSplitZoomed, "Expected workspace to start zoomed") XCTAssertTrue(manager.focusTabFromNotification(workspace.id, surfaceId: rightPanel.id)) drainMainQueue() drainMainQueue() XCTAssertFalse( workspace.bonsplitController.isSplitZoomed, "Expected notification focus to exit split zoom so the target pane becomes visible" ) XCTAssertEqual(workspace.focusedPanelId, rightPanel.id, "Expected notification target panel to be focused") } func testFocusTabFromNotificationReturnsFalseForMissingPanel() { let manager = TabManager() guard let workspace = manager.selectedWorkspace else { XCTFail("Expected selected workspace") return } XCTAssertFalse(manager.focusTabFromNotification(workspace.id, surfaceId: UUID())) } func testFocusTabFromNotificationDismissesUnreadWithDismissFlash() { let appDelegate = AppDelegate.shared ?? AppDelegate() let manager = TabManager() let store = TerminalNotificationStore.shared let defaults = UserDefaults.standard let originalTabManager = appDelegate.tabManager let originalNotificationStore = appDelegate.notificationStore let originalAppFocusOverride = AppFocusState.overrideIsFocused let originalExperimentEnabled = defaults.object(forKey: TmuxOverlayExperimentSettings.enabledKey) let originalExperimentTarget = defaults.object(forKey: TmuxOverlayExperimentSettings.targetKey) store.replaceNotificationsForTesting([]) store.configureNotificationDeliveryHandlerForTesting { _, _ in } appDelegate.tabManager = manager appDelegate.notificationStore = store AppFocusState.overrideIsFocused = true defaults.set(true, forKey: TmuxOverlayExperimentSettings.enabledKey) defaults.set(TmuxOverlayExperimentTarget.bonsplitPane.rawValue, forKey: TmuxOverlayExperimentSettings.targetKey) defer { store.replaceNotificationsForTesting([]) store.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) } } 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.focusPanel(leftPanelId) store.addNotification( tabId: workspace.id, surfaceId: rightPanel.id, title: "Unread", subtitle: "", body: "Right pane should dismiss attention when focused from a notification" ) XCTAssertTrue(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: rightPanel.id)) XCTAssertTrue(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: rightPanel.id)) XCTAssertEqual(workspace.tmuxWorkspaceFlashToken, 0) XCTAssertTrue(manager.focusTabFromNotification(workspace.id, surfaceId: rightPanel.id)) let expectation = XCTestExpectation(description: "notification focus flash") DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } wait(for: [expectation], timeout: 1) XCTAssertEqual(workspace.focusedPanelId, rightPanel.id) XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: rightPanel.id)) XCTAssertFalse(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: rightPanel.id)) XCTAssertEqual(workspace.tmuxWorkspaceFlashToken, 1) XCTAssertEqual(workspace.tmuxWorkspaceFlashPanelId, rightPanel.id) XCTAssertEqual(workspace.tmuxWorkspaceFlashReason, .notificationDismiss) } } @MainActor final class TabManagerPendingUnfocusPolicyTests: XCTestCase { func testDoesNotUnfocusWhenPendingTabIsCurrentlySelected() { let tabId = UUID() XCTAssertFalse( TabManager.shouldUnfocusPendingWorkspace( pendingTabId: tabId, selectedTabId: tabId ) ) } func testUnfocusesWhenPendingTabIsNotSelected() { XCTAssertTrue( TabManager.shouldUnfocusPendingWorkspace( pendingTabId: UUID(), selectedTabId: UUID() ) ) XCTAssertTrue( TabManager.shouldUnfocusPendingWorkspace( pendingTabId: UUID(), selectedTabId: nil ) ) } } @MainActor final class TabManagerSurfaceCreationTests: XCTestCase { func testNewSurfaceFocusesCreatedSurface() { let manager = TabManager() guard let workspace = manager.selectedWorkspace else { XCTFail("Expected a selected workspace") return } let beforePanels = Set(workspace.panels.keys) manager.newSurface() let afterPanels = Set(workspace.panels.keys) let createdPanels = afterPanels.subtracting(beforePanels) XCTAssertEqual(createdPanels.count, 1, "Expected one new surface for Cmd+T path") guard let createdPanelId = createdPanels.first else { return } XCTAssertEqual( workspace.focusedPanelId, createdPanelId, "Expected newly created surface to be focused" ) } func testOpenBrowserInsertAtEndPlacesNewBrowserAtPaneEnd() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let paneId = workspace.bonsplitController.focusedPaneId else { XCTFail("Expected focused workspace and pane") return } // Add one extra surface so we verify append-to-end rather than first insert behavior. _ = workspace.newTerminalSurface(inPane: paneId, focus: false) guard let browserPanelId = manager.openBrowser(insertAtEnd: true) else { XCTFail("Expected browser panel to be created") return } let tabs = workspace.bonsplitController.tabs(inPane: paneId) guard let lastSurfaceId = tabs.last?.id else { XCTFail("Expected at least one surface in pane") return } XCTAssertEqual( workspace.panelIdFromSurfaceId(lastSurfaceId), browserPanelId, "Expected Cmd+Shift+B/Cmd+L open path to append browser surface at end" ) XCTAssertEqual(workspace.focusedPanelId, browserPanelId, "Expected opened browser surface to be focused") } func testOpenBrowserInWorkspaceSplitRightSelectsTargetWorkspaceAndCreatesSplit() { let manager = TabManager() guard let initialWorkspace = manager.selectedWorkspace else { XCTFail("Expected initial selected workspace") return } guard let url = URL(string: "https://example.com/pull/123") else { XCTFail("Expected test URL to be valid") return } let targetWorkspace = manager.addWorkspace(select: false) manager.selectWorkspace(initialWorkspace) let initialPaneCount = targetWorkspace.bonsplitController.allPaneIds.count let initialPanelCount = targetWorkspace.panels.count guard let browserPanelId = manager.openBrowser( inWorkspace: targetWorkspace.id, url: url, preferSplitRight: true, insertAtEnd: true ) else { XCTFail("Expected browser panel to be created in target workspace") return } XCTAssertEqual(manager.selectedTabId, targetWorkspace.id, "Expected target workspace to become selected") XCTAssertEqual( targetWorkspace.bonsplitController.allPaneIds.count, initialPaneCount + 1, "Expected split-right browser open to create a new pane" ) XCTAssertEqual( targetWorkspace.panels.count, initialPanelCount + 1, "Expected browser panel count to increase by one" ) XCTAssertEqual( targetWorkspace.focusedPanelId, browserPanelId, "Expected created browser panel to be focused in target workspace" ) XCTAssertTrue( targetWorkspace.panels[browserPanelId] is BrowserPanel, "Expected created panel to be a browser panel" ) } func testOpenBrowserInWorkspaceSplitRightReusesTopRightPaneWhenAlreadySplit() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let leftPanelId = workspace.focusedPanelId, let topRightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal), workspace.newTerminalSplit(from: topRightPanel.id, orientation: .vertical) != nil, let topRightPaneId = workspace.paneId(forPanelId: topRightPanel.id), let url = URL(string: "https://example.com/pull/456") else { XCTFail("Expected split setup to succeed") return } let initialPaneCount = workspace.bonsplitController.allPaneIds.count guard let browserPanelId = manager.openBrowser( inWorkspace: workspace.id, url: url, preferSplitRight: true, insertAtEnd: true ) else { XCTFail("Expected browser panel to be created") return } XCTAssertEqual( workspace.bonsplitController.allPaneIds.count, initialPaneCount, "Expected split-right browser open to reuse existing panes" ) XCTAssertEqual( workspace.paneId(forPanelId: browserPanelId), topRightPaneId, "Expected browser to open in the top-right pane when multiple splits already exist" ) let targetPaneTabs = workspace.bonsplitController.tabs(inPane: topRightPaneId) guard let lastSurfaceId = targetPaneTabs.last?.id else { XCTFail("Expected top-right pane to contain tabs") return } XCTAssertEqual( workspace.panelIdFromSurfaceId(lastSurfaceId), browserPanelId, "Expected browser surface to be appended at end in the reused top-right pane" ) } } @MainActor final class TabManagerEqualizeSplitsTests: XCTestCase { func testEqualizeSplitsSetsEverySplitDividerToHalf() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let leftPanelId = workspace.focusedPanelId, let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal), workspace.newTerminalSplit(from: rightPanel.id, orientation: .vertical) != nil else { XCTFail("Expected nested split setup to succeed") return } let initialSplits = splitNodes(in: workspace.bonsplitController.treeSnapshot()) XCTAssertGreaterThanOrEqual(initialSplits.count, 2, "Expected at least two split nodes in nested layout") for (index, split) in initialSplits.enumerated() { guard let splitId = UUID(uuidString: split.id) else { XCTFail("Expected split ID to be a UUID") return } let targetPosition: CGFloat = index.isMultiple(of: 2) ? 0.2 : 0.8 XCTAssertTrue( workspace.bonsplitController.setDividerPosition(targetPosition, forSplit: splitId), "Expected to seed divider position for split \(splitId)" ) } XCTAssertTrue(manager.equalizeSplits(tabId: workspace.id), "Expected equalize splits command to succeed") let equalizedSplits = splitNodes(in: workspace.bonsplitController.treeSnapshot()) XCTAssertEqual(equalizedSplits.count, initialSplits.count) for split in equalizedSplits { XCTAssertEqual(split.dividerPosition, 0.5, accuracy: 0.000_1) } } } @MainActor final class TabManagerResizeSplitsTests: XCTestCase { func testResizeSplitMovesHorizontalDividerRightForFirstChildPane() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let leftPanelId = workspace.focusedPanelId, workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) != nil else { XCTFail("Expected split setup to succeed") return } guard let split = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first, let splitId = UUID(uuidString: split.id) else { XCTFail("Expected a split node in tree snapshot") return } XCTAssertTrue( workspace.bonsplitController.setDividerPosition(0.5, forSplit: splitId), "Expected to seed divider position" ) XCTAssertTrue( manager.resizeSplit(tabId: workspace.id, surfaceId: leftPanelId, direction: .right, amount: 120), "Expected resizeSplit to succeed for the right edge of the left pane" ) guard let updatedSplit = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first else { XCTFail("Expected updated split node in tree snapshot") return } XCTAssertGreaterThan( updatedSplit.dividerPosition, 0.5, "Expected resizing the left pane to the right to move the divider toward the second child" ) } func testResizeSplitMovesHorizontalDividerLeftForSecondChildPane() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let leftPanelId = workspace.focusedPanelId, let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else { XCTFail("Expected split setup to succeed") return } guard let split = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first, let splitId = UUID(uuidString: split.id) else { XCTFail("Expected a split node in tree snapshot") return } XCTAssertTrue( workspace.bonsplitController.setDividerPosition(0.5, forSplit: splitId), "Expected to seed divider position" ) XCTAssertTrue( manager.resizeSplit(tabId: workspace.id, surfaceId: rightPanel.id, direction: .left, amount: 120), "Expected resizeSplit to succeed for the left edge of the right pane" ) guard let updatedSplit = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first else { XCTFail("Expected updated split node in tree snapshot") return } XCTAssertLessThan( updatedSplit.dividerPosition, 0.5, "Expected resizing the right pane to the left to move the divider toward the first child" ) } func testResizeSplitMovesVerticalDividerDownForFirstChildPane() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let topPanelId = workspace.focusedPanelId, workspace.newTerminalSplit(from: topPanelId, orientation: .vertical) != nil else { XCTFail("Expected split setup to succeed") return } guard let split = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first, let splitId = UUID(uuidString: split.id) else { XCTFail("Expected a split node in tree snapshot") return } XCTAssertTrue( workspace.bonsplitController.setDividerPosition(0.5, forSplit: splitId), "Expected to seed divider position" ) XCTAssertTrue( manager.resizeSplit(tabId: workspace.id, surfaceId: topPanelId, direction: .down, amount: 120), "Expected resizeSplit to succeed for the bottom edge of the top pane" ) guard let updatedSplit = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first else { XCTFail("Expected updated split node in tree snapshot") return } XCTAssertGreaterThan( updatedSplit.dividerPosition, 0.5, "Expected resizing the top pane downward to move the divider toward the second child" ) } func testResizeSplitMovesVerticalDividerUpForSecondChildPane() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let topPanelId = workspace.focusedPanelId, let bottomPanel = workspace.newTerminalSplit(from: topPanelId, orientation: .vertical) else { XCTFail("Expected split setup to succeed") return } guard let split = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first, let splitId = UUID(uuidString: split.id) else { XCTFail("Expected a split node in tree snapshot") return } XCTAssertTrue( workspace.bonsplitController.setDividerPosition(0.5, forSplit: splitId), "Expected to seed divider position" ) XCTAssertTrue( manager.resizeSplit(tabId: workspace.id, surfaceId: bottomPanel.id, direction: .up, amount: 120), "Expected resizeSplit to succeed for the top edge of the bottom pane" ) guard let updatedSplit = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first else { XCTFail("Expected updated split node in tree snapshot") return } XCTAssertLessThan( updatedSplit.dividerPosition, 0.5, "Expected resizing the bottom pane upward to move the divider toward the first child" ) } func testResizeSplitReturnsFalseWhenPaneHasNoBorderInDirection() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let leftPanelId = workspace.focusedPanelId, workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) != nil else { XCTFail("Expected split setup to succeed") return } guard let split = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first else { XCTFail("Expected a split node in tree snapshot") return } XCTAssertFalse( manager.resizeSplit(tabId: workspace.id, surfaceId: leftPanelId, direction: .left, amount: 120), "Expected resizeSplit to fail when the pane has no adjacent border in that direction" ) guard let updatedSplit = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first else { XCTFail("Expected updated split node in tree snapshot") return } XCTAssertEqual(updatedSplit.dividerPosition, split.dividerPosition, accuracy: 0.000_1) } func testResizeSplitClampsDividerPositionAtUpperBound() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let leftPanelId = workspace.focusedPanelId, workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) != nil else { XCTFail("Expected split setup to succeed") return } guard let split = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first, let splitId = UUID(uuidString: split.id) else { XCTFail("Expected a split node in tree snapshot") return } XCTAssertTrue( workspace.bonsplitController.setDividerPosition(0.89, forSplit: splitId), "Expected to seed divider position near upper bound" ) XCTAssertTrue( manager.resizeSplit(tabId: workspace.id, surfaceId: leftPanelId, direction: .right, amount: 10_000), "Expected resizeSplit to clamp instead of failing" ) guard let updatedSplit = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first else { XCTFail("Expected updated split node in tree snapshot") return } XCTAssertEqual(updatedSplit.dividerPosition, 0.9, accuracy: 0.000_1) } func testResizeSplitClampsDividerPositionAtLowerBound() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let topPanelId = workspace.focusedPanelId, let bottomPanel = workspace.newTerminalSplit(from: topPanelId, orientation: .vertical) else { XCTFail("Expected split setup to succeed") return } guard let split = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first, let splitId = UUID(uuidString: split.id) else { XCTFail("Expected a split node in tree snapshot") return } XCTAssertTrue( workspace.bonsplitController.setDividerPosition(0.11, forSplit: splitId), "Expected to seed divider position near lower bound" ) XCTAssertTrue( manager.resizeSplit(tabId: workspace.id, surfaceId: bottomPanel.id, direction: .up, amount: 10_000), "Expected resizeSplit to clamp instead of failing" ) guard let updatedSplit = splitNodes(in: workspace.bonsplitController.treeSnapshot()).first else { XCTFail("Expected updated split node in tree snapshot") return } XCTAssertEqual(updatedSplit.dividerPosition, 0.1, accuracy: 0.000_1) } } @MainActor final class TabManagerWorkspaceConfigInheritanceSourceTests: XCTestCase { func testUsesFocusedTerminalWhenTerminalIsFocused() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let terminalPanelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace with focused terminal") return } let sourcePanel = manager.terminalPanelForWorkspaceConfigInheritanceSource() XCTAssertEqual(sourcePanel?.id, terminalPanelId) } func testFallsBackToTerminalWhenBrowserIsFocused() { 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 selected workspace setup to succeed") return } XCTAssertEqual(workspace.focusedPanelId, browserPanel.id) let sourcePanel = manager.terminalPanelForWorkspaceConfigInheritanceSource() XCTAssertEqual( sourcePanel?.id, terminalPanelId, "Expected new workspace inheritance source to resolve to the pane terminal when browser is focused" ) } func testPrefersLastFocusedTerminalAcrossPanesWhenBrowserIsFocused() { 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 = manager.terminalPanelForWorkspaceConfigInheritanceSource() XCTAssertEqual( sourcePanel?.id, leftTerminalPanelId, "Expected workspace inheritance source to use last focused terminal across panes" ) } } @MainActor final class TabManagerFocusedNotificationIndicatorTests: XCTestCase { func testFocusPanelDismissesUnreadNotificationWithDismissFlash() { let appDelegate = AppDelegate.shared ?? AppDelegate() let manager = TabManager() let store = TerminalNotificationStore.shared let defaults = UserDefaults.standard let originalTabManager = appDelegate.tabManager let originalNotificationStore = appDelegate.notificationStore let originalAppFocusOverride = AppFocusState.overrideIsFocused let originalExperimentEnabled = defaults.object(forKey: TmuxOverlayExperimentSettings.enabledKey) let originalExperimentTarget = defaults.object(forKey: TmuxOverlayExperimentSettings.targetKey) store.replaceNotificationsForTesting([]) store.configureNotificationDeliveryHandlerForTesting { _, _ in } appDelegate.tabManager = manager appDelegate.notificationStore = store AppFocusState.overrideIsFocused = true defaults.set(true, forKey: TmuxOverlayExperimentSettings.enabledKey) defaults.set(TmuxOverlayExperimentTarget.bonsplitPane.rawValue, forKey: TmuxOverlayExperimentSettings.targetKey) defer { store.replaceNotificationsForTesting([]) store.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) } } guard let workspace = manager.selectedWorkspace, let leftPanelId = workspace.focusedPanelId, let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else { XCTFail("Expected split terminal panels") return } store.addNotification( tabId: workspace.id, surfaceId: leftPanelId, title: "Unread", subtitle: "", body: "Left pane should dismiss attention when focused" ) XCTAssertTrue(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: leftPanelId)) XCTAssertTrue(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: leftPanelId)) XCTAssertEqual(workspace.focusedPanelId, rightPanel.id) XCTAssertEqual(workspace.tmuxWorkspaceFlashToken, 0) workspace.focusPanel(leftPanelId) XCTAssertEqual(workspace.focusedPanelId, leftPanelId) XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: leftPanelId)) XCTAssertFalse(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: leftPanelId)) XCTAssertEqual(workspace.tmuxWorkspaceFlashToken, 1) XCTAssertEqual(workspace.tmuxWorkspaceFlashPanelId, leftPanelId) XCTAssertEqual(workspace.tmuxWorkspaceFlashReason, .notificationDismiss) } func testDismissNotificationOnDirectInteractionClearsFocusedNotificationIndicator() { let appDelegate = AppDelegate.shared ?? AppDelegate() let manager = TabManager() let store = TerminalNotificationStore.shared let originalTabManager = appDelegate.tabManager let originalNotificationStore = appDelegate.notificationStore let originalAppFocusOverride = AppFocusState.overrideIsFocused store.replaceNotificationsForTesting([]) store.configureNotificationDeliveryHandlerForTesting { _, _ in } appDelegate.tabManager = manager appDelegate.notificationStore = store AppFocusState.overrideIsFocused = true defer { store.replaceNotificationsForTesting([]) store.resetNotificationDeliveryHandlerForTesting() appDelegate.tabManager = originalTabManager appDelegate.notificationStore = originalNotificationStore AppFocusState.overrideIsFocused = originalAppFocusOverride } guard let workspace = manager.selectedWorkspace, let panelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace with focused panel") return } store.setFocusedReadIndicator(forTabId: workspace.id, surfaceId: panelId) XCTAssertTrue(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: panelId)) XCTAssertTrue( manager.dismissNotificationOnDirectInteraction(tabId: workspace.id, surfaceId: panelId) ) XCTAssertFalse(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: panelId)) } func testDismissNotificationOnDirectInteractionTriggersDismissFlashForFocusedIndicatorOnly() { let appDelegate = AppDelegate.shared ?? AppDelegate() let manager = TabManager() let store = TerminalNotificationStore.shared let defaults = UserDefaults.standard let originalTabManager = appDelegate.tabManager let originalNotificationStore = appDelegate.notificationStore let originalAppFocusOverride = AppFocusState.overrideIsFocused let originalExperimentEnabled = defaults.object(forKey: TmuxOverlayExperimentSettings.enabledKey) let originalExperimentTarget = defaults.object(forKey: TmuxOverlayExperimentSettings.targetKey) store.replaceNotificationsForTesting([]) store.configureNotificationDeliveryHandlerForTesting { _, _ in } appDelegate.tabManager = manager appDelegate.notificationStore = store AppFocusState.overrideIsFocused = true defaults.set(true, forKey: TmuxOverlayExperimentSettings.enabledKey) defaults.set(TmuxOverlayExperimentTarget.bonsplitPane.rawValue, forKey: TmuxOverlayExperimentSettings.targetKey) defer { store.replaceNotificationsForTesting([]) store.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) } } guard let workspace = manager.selectedWorkspace, let panelId = workspace.focusedPanelId else { XCTFail("Expected selected workspace with focused panel") return } store.setFocusedReadIndicator(forTabId: workspace.id, surfaceId: panelId) XCTAssertTrue(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: panelId)) XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: panelId)) XCTAssertEqual(workspace.tmuxWorkspaceFlashToken, 0) XCTAssertTrue( manager.dismissNotificationOnDirectInteraction(tabId: workspace.id, surfaceId: panelId) ) XCTAssertFalse(store.hasVisibleNotificationIndicator(forTabId: workspace.id, surfaceId: panelId)) XCTAssertEqual( workspace.tmuxWorkspaceFlashToken, 1, "Expected dismissing a focused-read indicator to emit a dismiss flash even when unread is already cleared" ) XCTAssertEqual(workspace.tmuxWorkspaceFlashPanelId, panelId) XCTAssertEqual(workspace.tmuxWorkspaceFlashReason, .notificationDismiss) } } @MainActor final class TabManagerReopenClosedBrowserFocusTests: XCTestCase { func testReopenFromDifferentWorkspaceFocusesReopenedBrowser() { let manager = TabManager() guard let workspace1 = manager.selectedWorkspace, let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/ws-switch")) else { XCTFail("Expected initial workspace and browser panel") return } drainMainQueue() XCTAssertTrue(workspace1.closePanel(closedBrowserId, force: true)) drainMainQueue() let workspace2 = manager.addWorkspace() XCTAssertEqual(manager.selectedTabId, workspace2.id) XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel()) drainMainQueue() XCTAssertEqual(manager.selectedTabId, workspace1.id) XCTAssertTrue(isFocusedPanelBrowser(in: workspace1)) } func testReopenFallsBackToCurrentWorkspaceAndFocusesBrowserWhenOriginalWorkspaceDeleted() { let manager = TabManager() guard let originalWorkspace = manager.selectedWorkspace, let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/deleted-ws")) else { XCTFail("Expected initial workspace and browser panel") return } drainMainQueue() XCTAssertTrue(originalWorkspace.closePanel(closedBrowserId, force: true)) drainMainQueue() let currentWorkspace = manager.addWorkspace() manager.closeWorkspace(originalWorkspace) XCTAssertEqual(manager.selectedTabId, currentWorkspace.id) XCTAssertFalse(manager.tabs.contains(where: { $0.id == originalWorkspace.id })) XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel()) drainMainQueue() XCTAssertEqual(manager.selectedTabId, currentWorkspace.id) XCTAssertTrue(isFocusedPanelBrowser(in: currentWorkspace)) } func testReopenCollapsedSplitFromDifferentWorkspaceFocusesBrowser() { let manager = TabManager() guard let workspace1 = manager.selectedWorkspace, let sourcePanelId = workspace1.focusedPanelId, let splitBrowserId = manager.newBrowserSplit( tabId: workspace1.id, fromPanelId: sourcePanelId, orientation: .horizontal, insertFirst: false, url: URL(string: "https://example.com/collapsed-split") ) else { XCTFail("Expected to create browser split") return } drainMainQueue() XCTAssertTrue(workspace1.closePanel(splitBrowserId, force: true)) drainMainQueue() let workspace2 = manager.addWorkspace() XCTAssertEqual(manager.selectedTabId, workspace2.id) XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel()) drainMainQueue() XCTAssertEqual(manager.selectedTabId, workspace1.id) XCTAssertTrue(isFocusedPanelBrowser(in: workspace1)) } func testReopenFromDifferentWorkspaceWinsAgainstSingleDeferredStaleFocus() { let manager = TabManager() guard let workspace1 = manager.selectedWorkspace, let preReopenPanelId = workspace1.focusedPanelId, let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/stale-focus-cross-ws")) else { XCTFail("Expected initial workspace state and browser panel") return } drainMainQueue() XCTAssertTrue(workspace1.closePanel(closedBrowserId, force: true)) drainMainQueue() let panelIdsBeforeReopen = Set(workspace1.panels.keys) let workspace2 = manager.addWorkspace() XCTAssertEqual(manager.selectedTabId, workspace2.id) XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel()) guard let reopenedPanelId = singleNewPanelId(in: workspace1, comparedTo: panelIdsBeforeReopen) else { XCTFail("Expected reopened browser panel ID") return } // Simulate one delayed stale focus callback from the panel that was focused before reopen. DispatchQueue.main.async { workspace1.focusPanel(preReopenPanelId) } drainMainQueue() drainMainQueue() drainMainQueue() XCTAssertEqual(manager.selectedTabId, workspace1.id) XCTAssertEqual(workspace1.focusedPanelId, reopenedPanelId) XCTAssertTrue(workspace1.panels[reopenedPanelId] is BrowserPanel) } func testReopenInSameWorkspaceWinsAgainstSingleDeferredStaleFocus() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, let preReopenPanelId = workspace.focusedPanelId, let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/stale-focus-same-ws")) else { XCTFail("Expected initial workspace state and browser panel") return } drainMainQueue() XCTAssertTrue(workspace.closePanel(closedBrowserId, force: true)) drainMainQueue() let panelIdsBeforeReopen = Set(workspace.panels.keys) XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel()) guard let reopenedPanelId = singleNewPanelId(in: workspace, comparedTo: panelIdsBeforeReopen) else { XCTFail("Expected reopened browser panel ID") return } // Simulate one delayed stale focus callback from the panel that was focused before reopen. DispatchQueue.main.async { workspace.focusPanel(preReopenPanelId) } drainMainQueue() drainMainQueue() drainMainQueue() XCTAssertEqual(manager.selectedTabId, workspace.id) XCTAssertEqual(workspace.focusedPanelId, reopenedPanelId) XCTAssertTrue(workspace.panels[reopenedPanelId] is BrowserPanel) } private func isFocusedPanelBrowser(in workspace: Workspace) -> Bool { guard let focusedPanelId = workspace.focusedPanelId else { return false } return workspace.panels[focusedPanelId] is BrowserPanel } private func singleNewPanelId(in workspace: Workspace, comparedTo previousPanelIds: Set) -> UUID? { let newPanelIds = Set(workspace.panels.keys).subtracting(previousPanelIds) guard newPanelIds.count == 1 else { return nil } return newPanelIds.first } private func drainMainQueue() { let expectation = expectation(description: "drain main queue") DispatchQueue.main.async { expectation.fulfill() } wait(for: [expectation], timeout: 1.0) } }