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 final class SidebarActiveForegroundColorTests: XCTestCase { func testLightAppearanceUsesBlackWithRequestedOpacity() { guard let lightAppearance = NSAppearance(named: .aqua), let color = sidebarActiveForegroundNSColor( opacity: 0.8, appAppearance: lightAppearance ).usingColorSpace(.sRGB) else { XCTFail("Expected sRGB-convertible color") return } XCTAssertEqual(color.redComponent, 0, accuracy: 0.001) XCTAssertEqual(color.greenComponent, 0, accuracy: 0.001) XCTAssertEqual(color.blueComponent, 0, accuracy: 0.001) XCTAssertEqual(color.alphaComponent, 0.8, accuracy: 0.001) } func testDarkAppearanceUsesWhiteWithRequestedOpacity() { guard let darkAppearance = NSAppearance(named: .darkAqua), let color = sidebarActiveForegroundNSColor( opacity: 0.65, appAppearance: darkAppearance ).usingColorSpace(.sRGB) else { XCTFail("Expected sRGB-convertible color") return } XCTAssertEqual(color.redComponent, 1, accuracy: 0.001) XCTAssertEqual(color.greenComponent, 1, accuracy: 0.001) XCTAssertEqual(color.blueComponent, 1, accuracy: 0.001) XCTAssertEqual(color.alphaComponent, 0.65, accuracy: 0.001) } } final class SidebarBranchLayoutSettingsTests: XCTestCase { func testDefaultUsesVerticalLayout() { let suiteName = "SidebarBranchLayoutSettingsTests.Default.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } XCTAssertTrue(SidebarBranchLayoutSettings.usesVerticalLayout(defaults: defaults)) } func testStoredPreferenceOverridesDefault() { let suiteName = "SidebarBranchLayoutSettingsTests.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(false, forKey: SidebarBranchLayoutSettings.key) XCTAssertFalse(SidebarBranchLayoutSettings.usesVerticalLayout(defaults: defaults)) defaults.set(true, forKey: SidebarBranchLayoutSettings.key) XCTAssertTrue(SidebarBranchLayoutSettings.usesVerticalLayout(defaults: defaults)) } } final class SidebarActiveTabIndicatorSettingsTests: XCTestCase { func testDefaultStyleWhenUnset() { let suiteName = "SidebarActiveTabIndicatorSettingsTests.Default.\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { XCTFail("Failed to create isolated UserDefaults suite") return } defer { defaults.removePersistentDomain(forName: suiteName) } defaults.removeObject(forKey: SidebarActiveTabIndicatorSettings.styleKey) XCTAssertEqual( SidebarActiveTabIndicatorSettings.current(defaults: defaults), SidebarActiveTabIndicatorSettings.defaultStyle ) } func testStoredStyleParsesAndInvalidFallsBack() { let suiteName = "SidebarActiveTabIndicatorSettingsTests.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(SidebarActiveTabIndicatorStyle.leftRail.rawValue, forKey: SidebarActiveTabIndicatorSettings.styleKey) XCTAssertEqual(SidebarActiveTabIndicatorSettings.current(defaults: defaults), .leftRail) defaults.set("rail", forKey: SidebarActiveTabIndicatorSettings.styleKey) XCTAssertEqual(SidebarActiveTabIndicatorSettings.current(defaults: defaults), .leftRail) defaults.set("not-a-style", forKey: SidebarActiveTabIndicatorSettings.styleKey) XCTAssertEqual( SidebarActiveTabIndicatorSettings.current(defaults: defaults), SidebarActiveTabIndicatorSettings.defaultStyle ) } } final class SidebarRemoteErrorCopySupportTests: XCTestCase { func testMenuLabelIsNilWhenThereAreNoErrors() { XCTAssertNil(SidebarRemoteErrorCopySupport.menuLabel(for: [])) XCTAssertNil(SidebarRemoteErrorCopySupport.clipboardText(for: [])) } func testSingleErrorUsesCopyErrorLabelAndSingleLinePayload() { let entries = [ SidebarRemoteErrorCopyEntry( workspaceTitle: "alpha", target: "devbox:22", detail: "failed to start reverse relay" ) ] XCTAssertEqual(SidebarRemoteErrorCopySupport.menuLabel(for: entries), "Copy Error") XCTAssertEqual( SidebarRemoteErrorCopySupport.clipboardText(for: entries), "SSH error (devbox:22): failed to start reverse relay" ) } func testMultipleErrorsUseCopyErrorsLabelAndEnumeratedPayload() { let entries = [ SidebarRemoteErrorCopyEntry( workspaceTitle: "alpha", target: "devbox-a:22", detail: "connection timed out" ), SidebarRemoteErrorCopyEntry( workspaceTitle: "beta", target: "devbox-b:22", detail: "permission denied" ), ] XCTAssertEqual(SidebarRemoteErrorCopySupport.menuLabel(for: entries), "Copy Errors") XCTAssertEqual( SidebarRemoteErrorCopySupport.clipboardText(for: entries), """ 1. alpha (devbox-a:22): connection timed out 2. beta (devbox-b:22): permission denied """ ) } func testClipboardTextSingleEntryUsesStructuredEntryFields() { let entry = SidebarRemoteErrorCopyEntry( workspaceTitle: "alpha", target: "devbox:22", detail: "failed to bootstrap daemon" ) XCTAssertEqual( SidebarRemoteErrorCopySupport.clipboardText(for: [entry]), "SSH error (devbox:22): failed to bootstrap daemon" ) } } final class SidebarBranchOrderingTests: XCTestCase { func testOrderedUniqueBranchesDedupesByNameAndMergesDirtyState() { let first = UUID() let second = UUID() let third = UUID() let branches = SidebarBranchOrdering.orderedUniqueBranches( orderedPanelIds: [first, second, third], panelBranches: [ first: SidebarGitBranchState(branch: "main", isDirty: false), second: SidebarGitBranchState(branch: "feature", isDirty: false), third: SidebarGitBranchState(branch: "main", isDirty: true) ], fallbackBranch: SidebarGitBranchState(branch: "fallback", isDirty: false) ) XCTAssertEqual( branches, [ SidebarBranchOrdering.BranchEntry(name: "main", isDirty: true), SidebarBranchOrdering.BranchEntry(name: "feature", isDirty: false) ] ) } func testOrderedUniqueBranchesUsesFallbackWhenNoPanelBranchesExist() { let branches = SidebarBranchOrdering.orderedUniqueBranches( orderedPanelIds: [], panelBranches: [:], fallbackBranch: SidebarGitBranchState(branch: "fallback", isDirty: true) ) XCTAssertEqual( branches, [SidebarBranchOrdering.BranchEntry(name: "fallback", isDirty: true)] ) } func testOrderedUniqueBranchDirectoryEntriesDedupesPairsAndMergesDirtyState() { let first = UUID() let second = UUID() let third = UUID() let fourth = UUID() let fifth = UUID() let rows = SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries( orderedPanelIds: [first, second, third, fourth, fifth], panelBranches: [ first: SidebarGitBranchState(branch: "main", isDirty: false), second: SidebarGitBranchState(branch: "feature", isDirty: false), third: SidebarGitBranchState(branch: "main", isDirty: true), fourth: SidebarGitBranchState(branch: "main", isDirty: false) ], panelDirectories: [ first: "/repo/a", second: "/repo/b", third: "/repo/a", fourth: "/repo/d", fifth: "/repo/e" ], defaultDirectory: "/repo/default", homeDirectoryForTildeExpansion: nil, fallbackBranch: SidebarGitBranchState(branch: "fallback", isDirty: false) ) XCTAssertEqual( rows, [ SidebarBranchOrdering.BranchDirectoryEntry(branch: "main", isDirty: true, directory: "/repo/a"), SidebarBranchOrdering.BranchDirectoryEntry(branch: "feature", isDirty: false, directory: "/repo/b"), SidebarBranchOrdering.BranchDirectoryEntry(branch: "main", isDirty: false, directory: "/repo/d"), SidebarBranchOrdering.BranchDirectoryEntry(branch: nil, isDirty: false, directory: "/repo/e") ] ) } func testOrderedUniqueBranchDirectoryEntriesUsesFallbackBranchWhenPanelBranchesMissing() { let first = UUID() let second = UUID() let rows = SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries( orderedPanelIds: [first, second], panelBranches: [:], panelDirectories: [ first: "/repo/one", second: "/repo/two" ], defaultDirectory: "/repo/default", homeDirectoryForTildeExpansion: nil, fallbackBranch: SidebarGitBranchState(branch: "main", isDirty: true) ) XCTAssertEqual( rows, [ SidebarBranchOrdering.BranchDirectoryEntry(branch: "main", isDirty: true, directory: "/repo/one"), SidebarBranchOrdering.BranchDirectoryEntry(branch: "main", isDirty: true, directory: "/repo/two") ] ) } func testOrderedUniqueBranchDirectoryEntriesFallsBackWhenNoPanelsExist() { let rows = SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries( orderedPanelIds: [], panelBranches: [:], panelDirectories: [:], defaultDirectory: "/repo/default", homeDirectoryForTildeExpansion: nil, fallbackBranch: SidebarGitBranchState(branch: "main", isDirty: false) ) XCTAssertEqual( rows, [SidebarBranchOrdering.BranchDirectoryEntry(branch: "main", isDirty: false, directory: "/repo/default")] ) } func testOrderedUniqueBranchDirectoryEntriesKeepsAbsoluteDirectoryWhenLaterEntryUsesTildeAlias() { let first = UUID() let second = UUID() let rows = SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries( orderedPanelIds: [first, second], panelBranches: [ first: SidebarGitBranchState(branch: "main", isDirty: false), second: SidebarGitBranchState(branch: "feature", isDirty: true) ], panelDirectories: [ first: "/home/remoteuser/project", second: "~/project" ], defaultDirectory: nil, homeDirectoryForTildeExpansion: "/home/remoteuser", fallbackBranch: nil ) XCTAssertEqual( rows, [ SidebarBranchOrdering.BranchDirectoryEntry( branch: "feature", isDirty: true, directory: "/home/remoteuser/project" ) ] ) } func testOrderedUniquePullRequestsFollowsPanelOrderAcrossSplitsAndTabs() { let first = UUID() let second = UUID() let third = UUID() let fourth = UUID() let pullRequests = SidebarBranchOrdering.orderedUniquePullRequests( orderedPanelIds: [first, second, third, fourth], panelPullRequests: [ first: pullRequestState( number: 337, label: "PR", url: "https://github.com/manaflow-ai/cmux/pull/337", status: .open ), second: pullRequestState( number: 18, label: "MR", url: "https://gitlab.com/manaflow/cmux/-/merge_requests/18", status: .open ), third: pullRequestState( number: 337, label: "PR", url: "https://github.com/manaflow-ai/cmux/pull/337", status: .merged ), fourth: pullRequestState( number: 92, label: "PR", url: "https://bitbucket.org/manaflow/cmux/pull-requests/92", status: .closed ) ], fallbackPullRequest: pullRequestState( number: 1, label: "PR", url: "https://example.invalid/fallback/1", status: .open ) ) XCTAssertEqual( pullRequests.map { "\($0.label)#\($0.number)" }, ["PR#337", "MR#18", "PR#92"] ) XCTAssertEqual( pullRequests.map(\.status), [.merged, .open, .closed] ) } func testOrderedUniquePullRequestsTreatsSameNumberDifferentLabelsAsDistinct() { let first = UUID() let second = UUID() let pullRequests = SidebarBranchOrdering.orderedUniquePullRequests( orderedPanelIds: [first, second], panelPullRequests: [ first: pullRequestState( number: 42, label: "PR", url: "https://github.com/manaflow-ai/cmux/pull/42", status: .open ), second: pullRequestState( number: 42, label: "MR", url: "https://gitlab.com/manaflow/cmux/-/merge_requests/42", status: .open ) ], fallbackPullRequest: nil ) XCTAssertEqual( pullRequests.map { "\($0.label)#\($0.number)" }, ["PR#42", "MR#42"] ) } func testOrderedUniquePullRequestsTreatsSameNumberAndLabelDifferentUrlsAsDistinct() { let first = UUID() let second = UUID() let pullRequests = SidebarBranchOrdering.orderedUniquePullRequests( orderedPanelIds: [first, second], panelPullRequests: [ first: pullRequestState( number: 42, label: "PR", url: "https://github.com/manaflow-ai/cmux/pull/42", status: .open ), second: pullRequestState( number: 42, label: "PR", url: "https://github.com/manaflow-ai/other-repo/pull/42", status: .open ) ], fallbackPullRequest: nil ) XCTAssertEqual( pullRequests.map(\.url.absoluteString), [ "https://github.com/manaflow-ai/cmux/pull/42", "https://github.com/manaflow-ai/other-repo/pull/42" ] ) } func testOrderedUniquePullRequestsPrefersEntryWithChecksWhenStatusesMatch() { let first = UUID() let second = UUID() let pullRequests = SidebarBranchOrdering.orderedUniquePullRequests( orderedPanelIds: [first, second], panelPullRequests: [ first: pullRequestState( number: 42, label: "PR", url: "https://github.com/manaflow-ai/cmux/pull/42", status: .open ), second: pullRequestState( number: 42, label: "PR", url: "https://github.com/manaflow-ai/cmux/pull/42", status: .open, checks: .pass ) ], fallbackPullRequest: nil ) XCTAssertEqual(pullRequests.count, 1) XCTAssertEqual(pullRequests.first?.checks, .pass) } @MainActor func testUpdatePanelPullRequestPreservesExistingChecksWhenUpdateOmitsThem() { let workspace = Workspace(title: "Tests", workingDirectory: FileManager.default.currentDirectoryPath, portOrdinal: 0) guard let panelId = workspace.focusedPanelId else { XCTFail("Expected focused panel for new workspace") return } workspace.updatePanelPullRequest( panelId: panelId, number: 42, label: "PR", url: URL(string: "https://github.com/manaflow-ai/cmux/pull/42")!, status: .open, checks: .pass ) workspace.updatePanelPullRequest( panelId: panelId, number: 42, label: "PR", url: URL(string: "https://github.com/manaflow-ai/cmux/pull/42")!, status: .open ) XCTAssertEqual(workspace.panelPullRequests[panelId]?.checks, .pass) XCTAssertEqual(workspace.pullRequest?.checks, .pass) } func testOrderedUniquePullRequestsUsesFallbackWhenNoPanelPullRequestsExist() { let fallback = pullRequestState( number: 11, label: "PR", url: "https://github.com/manaflow-ai/cmux/pull/11", status: .open ) let pullRequests = SidebarBranchOrdering.orderedUniquePullRequests( orderedPanelIds: [], panelPullRequests: [:], fallbackPullRequest: fallback ) XCTAssertEqual(pullRequests, [fallback]) } @MainActor func testUpdatePanelGitBranchClearsFocusedPullRequestWhenBranchChanges() { let workspace = Workspace(title: "Tests", workingDirectory: FileManager.default.currentDirectoryPath, portOrdinal: 0) guard let panelId = workspace.focusedPanelId else { XCTFail("Expected focused panel for new workspace") return } workspace.updatePanelGitBranch(panelId: panelId, branch: "feature/sidebar-pr", isDirty: false) workspace.updatePanelPullRequest( panelId: panelId, number: 1629, label: "PR", url: URL(string: "https://github.com/manaflow-ai/cmux/pull/1629")!, status: .open ) workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false) XCTAssertNil(workspace.pullRequest) XCTAssertNil(workspace.panelPullRequests[panelId]) XCTAssertTrue(workspace.sidebarPullRequestsInDisplayOrder().isEmpty) } @MainActor func testSidebarPullRequestsHideBranchMismatches() { let workspace = Workspace(title: "Tests", workingDirectory: FileManager.default.currentDirectoryPath, portOrdinal: 0) guard let panelId = workspace.focusedPanelId else { XCTFail("Expected focused panel for new workspace") return } workspace.updatePanelGitBranch(panelId: panelId, branch: "main", isDirty: false) workspace.updatePanelPullRequest( panelId: panelId, number: 1629, label: "PR", url: URL(string: "https://github.com/manaflow-ai/cmux/pull/1629")!, status: .open, branch: "feature/sidebar-pr" ) XCTAssertTrue(workspace.sidebarPullRequestsInDisplayOrder().isEmpty) } private func pullRequestState( number: Int, label: String, url: String, status: SidebarPullRequestStatus, branch: String? = nil, checks: SidebarPullRequestChecksStatus? = nil ) -> SidebarPullRequestState { SidebarPullRequestState( number: number, label: label, url: URL(string: url)!, status: status, branch: branch, checks: checks ) } } final class SidebarDropPlannerTests: XCTestCase { func testNoIndicatorForNoOpEdges() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: first, targetTabId: first, tabIds: tabIds, pinnedTabIds: [] ) ) XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: third, targetTabId: nil, tabIds: tabIds, pinnedTabIds: [] ) ) } func testNoIndicatorWhenOnlyOneTabExists() { let only = UUID() XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: only, targetTabId: nil, tabIds: [only], pinnedTabIds: [] ) ) XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: only, targetTabId: only, tabIds: [only], pinnedTabIds: [] ) ) } func testIndicatorAppearsForRealMoveToEnd() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] let indicator = SidebarDropPlanner.indicator( draggedTabId: second, targetTabId: nil, tabIds: tabIds, pinnedTabIds: [] ) XCTAssertEqual(indicator?.tabId, nil) XCTAssertEqual(indicator?.edge, .bottom) } func testTargetIndexForMoveToEndFromMiddle() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] let index = SidebarDropPlanner.targetIndex( draggedTabId: second, targetTabId: nil, indicator: SidebarDropIndicator(tabId: nil, edge: .bottom), tabIds: tabIds, pinnedTabIds: [] ) XCTAssertEqual(index, 2) } func testNoIndicatorForSelfDropInMiddle() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: second, targetTabId: second, tabIds: tabIds, pinnedTabIds: [] ) ) } func testPointerEdgeTopCanSuppressNoOpWhenDraggingFirstOverSecond() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: first, targetTabId: second, tabIds: tabIds, pinnedTabIds: [], pointerY: 2, targetHeight: 40 ) ) } func testPointerEdgeBottomAllowsMoveWhenDraggingFirstOverSecond() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] let indicator = SidebarDropPlanner.indicator( draggedTabId: first, targetTabId: second, tabIds: tabIds, pinnedTabIds: [], pointerY: 38, targetHeight: 40 ) XCTAssertEqual(indicator?.tabId, third) XCTAssertEqual(indicator?.edge, .top) XCTAssertEqual( SidebarDropPlanner.targetIndex( draggedTabId: first, targetTabId: second, indicator: indicator, tabIds: tabIds, pinnedTabIds: [] ), 1 ) } func testEquivalentBoundaryInputsResolveToSingleCanonicalIndicator() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] let fromBottomOfFirst = SidebarDropPlanner.indicator( draggedTabId: third, targetTabId: first, tabIds: tabIds, pinnedTabIds: [], pointerY: 38, targetHeight: 40 ) let fromTopOfSecond = SidebarDropPlanner.indicator( draggedTabId: third, targetTabId: second, tabIds: tabIds, pinnedTabIds: [], pointerY: 2, targetHeight: 40 ) XCTAssertEqual(fromBottomOfFirst?.tabId, second) XCTAssertEqual(fromBottomOfFirst?.edge, .top) XCTAssertEqual(fromTopOfSecond?.tabId, second) XCTAssertEqual(fromTopOfSecond?.edge, .top) } func testPointerEdgeBottomSuppressesNoOpWhenDraggingLastOverSecond() { let first = UUID() let second = UUID() let third = UUID() let tabIds = [first, second, third] XCTAssertNil( SidebarDropPlanner.indicator( draggedTabId: third, targetTabId: second, tabIds: tabIds, pinnedTabIds: [], pointerY: 38, targetHeight: 40 ) ) } func testIndicatorSnapsUnpinnedDropToFirstUnpinnedBoundaryWhenHoveringPinnedWorkspace() { let pinnedA = UUID() let pinnedB = UUID() let unpinnedA = UUID() let unpinnedB = UUID() let tabIds = [pinnedA, pinnedB, unpinnedA, unpinnedB] let pinnedIds: Set = [pinnedA, pinnedB] let indicator = SidebarDropPlanner.indicator( draggedTabId: unpinnedB, targetTabId: pinnedA, tabIds: tabIds, pinnedTabIds: pinnedIds, pointerY: 2, targetHeight: 40 ) XCTAssertEqual(indicator?.tabId, unpinnedA) XCTAssertEqual(indicator?.edge, .top) } func testTargetIndexSnapsUnpinnedDropToFirstUnpinnedBoundaryWhenHoveringPinnedWorkspace() { let pinnedA = UUID() let pinnedB = UUID() let unpinnedA = UUID() let unpinnedB = UUID() let tabIds = [pinnedA, pinnedB, unpinnedA, unpinnedB] let pinnedIds: Set = [pinnedA, pinnedB] let targetIndex = SidebarDropPlanner.targetIndex( draggedTabId: unpinnedB, targetTabId: pinnedA, indicator: SidebarDropIndicator(tabId: pinnedA, edge: .top), tabIds: tabIds, pinnedTabIds: pinnedIds ) XCTAssertEqual(targetIndex, 2) } } final class SidebarDragAutoScrollPlannerTests: XCTestCase { func testAutoScrollPlanTriggersNearTopAndBottomOnly() { let topPlan = SidebarDragAutoScrollPlanner.plan(distanceToTop: 4, distanceToBottom: 96, edgeInset: 44, minStep: 2, maxStep: 12) XCTAssertEqual(topPlan?.direction, .up) XCTAssertNotNil(topPlan) let bottomPlan = SidebarDragAutoScrollPlanner.plan(distanceToTop: 96, distanceToBottom: 4, edgeInset: 44, minStep: 2, maxStep: 12) XCTAssertEqual(bottomPlan?.direction, .down) XCTAssertNotNil(bottomPlan) XCTAssertNil( SidebarDragAutoScrollPlanner.plan(distanceToTop: 60, distanceToBottom: 60, edgeInset: 44, minStep: 2, maxStep: 12) ) } func testAutoScrollPlanSpeedsUpCloserToEdge() { let nearTop = SidebarDragAutoScrollPlanner.plan(distanceToTop: 1, distanceToBottom: 99, edgeInset: 44, minStep: 2, maxStep: 12) let midTop = SidebarDragAutoScrollPlanner.plan(distanceToTop: 22, distanceToBottom: 78, edgeInset: 44, minStep: 2, maxStep: 12) XCTAssertNotNil(nearTop) XCTAssertNotNil(midTop) XCTAssertGreaterThan(nearTop?.pointsPerTick ?? 0, midTop?.pointsPerTick ?? 0) } func testAutoScrollPlanStillTriggersWhenPointerIsPastEdge() { let aboveTop = SidebarDragAutoScrollPlanner.plan(distanceToTop: -500, distanceToBottom: 600, edgeInset: 44, minStep: 2, maxStep: 12) XCTAssertEqual(aboveTop?.direction, .up) XCTAssertEqual(aboveTop?.pointsPerTick, 12) let belowBottom = SidebarDragAutoScrollPlanner.plan(distanceToTop: 600, distanceToBottom: -500, edgeInset: 44, minStep: 2, maxStep: 12) XCTAssertEqual(belowBottom?.direction, .down) XCTAssertEqual(belowBottom?.pointsPerTick, 12) } } final class TerminalControllerSidebarDedupeTests: XCTestCase { func testShouldReplaceStatusEntryReturnsFalseForUnchangedPayload() { let current = SidebarStatusEntry( key: "agent", value: "idle", icon: "bolt", color: "#ffffff", timestamp: Date(timeIntervalSince1970: 123) ) XCTAssertFalse( TerminalController.shouldReplaceStatusEntry( current: current, key: "agent", value: "idle", icon: "bolt", color: "#ffffff", url: nil, priority: 0, format: .plain ) ) } func testShouldReplaceStatusEntryReturnsTrueWhenValueChanges() { let current = SidebarStatusEntry( key: "agent", value: "idle", icon: "bolt", color: "#ffffff", timestamp: Date(timeIntervalSince1970: 123) ) XCTAssertTrue( TerminalController.shouldReplaceStatusEntry( current: current, key: "agent", value: "running", icon: "bolt", color: "#ffffff", url: nil, priority: 0, format: .plain ) ) } func testShouldReplaceProgressReturnsFalseForUnchangedPayload() { XCTAssertFalse( TerminalController.shouldReplaceProgress( current: SidebarProgressState(value: 0.42, label: "indexing"), value: 0.42, label: "indexing" ) ) } func testShouldReplaceGitBranchReturnsFalseForUnchangedPayload() { XCTAssertFalse( TerminalController.shouldReplaceGitBranch( current: SidebarGitBranchState(branch: "main", isDirty: true), branch: "main", isDirty: true ) ) } func testShouldReplacePortsIgnoresOrderAndDuplicates() { XCTAssertFalse( TerminalController.shouldReplacePorts( current: [9229, 3000], next: [3000, 9229, 3000] ) ) XCTAssertTrue( TerminalController.shouldReplacePorts( current: [9229, 3000], next: [3000] ) ) } func testExplicitSocketScopeParsesValidUUIDTabAndPanel() { let workspaceId = UUID() let panelId = UUID() let scope = TerminalController.explicitSocketScope( options: [ "tab": workspaceId.uuidString, "panel": panelId.uuidString ] ) XCTAssertEqual(scope?.workspaceId, workspaceId) XCTAssertEqual(scope?.panelId, panelId) } func testExplicitSocketScopeAcceptsSurfaceAlias() { let workspaceId = UUID() let panelId = UUID() let scope = TerminalController.explicitSocketScope( options: [ "tab": workspaceId.uuidString, "surface": panelId.uuidString ] ) XCTAssertEqual(scope?.workspaceId, workspaceId) XCTAssertEqual(scope?.panelId, panelId) } func testExplicitSocketScopeRejectsMissingOrInvalidValues() { XCTAssertNil(TerminalController.explicitSocketScope(options: [:])) XCTAssertNil(TerminalController.explicitSocketScope(options: ["tab": "workspace:1", "panel": UUID().uuidString])) XCTAssertNil(TerminalController.explicitSocketScope(options: ["tab": UUID().uuidString, "panel": "surface:1"])) } func testNormalizeReportedDirectoryTrimsWhitespace() { XCTAssertEqual( TerminalController.normalizeReportedDirectory(" /Users/cmux/project "), "/Users/cmux/project" ) } func testNormalizeReportedDirectoryResolvesFileURL() { XCTAssertEqual( TerminalController.normalizeReportedDirectory("file:///Users/cmux/project"), "/Users/cmux/project" ) } func testNormalizeReportedDirectoryLeavesInvalidURLTrimmed() { XCTAssertEqual( TerminalController.normalizeReportedDirectory(" file://bad host "), "file://bad host" ) } }