import XCTest @testable import CommandNotch @MainActor final class WorkspaceRegistryTests: XCTestCase { func testRegistryCreatesDefaultWorkspaceWhenEmpty() { let registry = makeRegistry() XCTAssertEqual(registry.allWorkspaceSummaries().count, 1) XCTAssertEqual(registry.allWorkspaceSummaries().first?.name, "Main") XCTAssertNotNil(registry.controller(for: registry.defaultWorkspaceID)) } func testCreateRenameAndDeleteWorkspaceUpdatesSummaries() { let registry = makeRegistry() let createdID = registry.createWorkspace(named: "Docs") XCTAssertEqual(registry.allWorkspaceSummaries().map(\.name), ["Main", "Docs"]) registry.renameWorkspace(id: createdID, to: "Review") XCTAssertEqual(registry.allWorkspaceSummaries().map(\.name), ["Main", "Review"]) registry.deleteWorkspace(id: createdID) XCTAssertEqual(registry.allWorkspaceSummaries().map(\.name), ["Main"]) } func testDeletingLastWorkspaceIsIgnored() { let registry = makeRegistry() let onlyWorkspaceID = registry.defaultWorkspaceID registry.deleteWorkspace(id: onlyWorkspaceID) XCTAssertEqual(registry.allWorkspaceSummaries().count, 1) XCTAssertEqual(registry.allWorkspaceSummaries().first?.id, onlyWorkspaceID) } func testDeletionFallbackPrefersRequestedWorkspaceWhenAvailable() { let registry = makeRegistry() let docsID = registry.createWorkspace(named: "Docs") let reviewID = registry.createWorkspace(named: "Review") let fallback = registry.deletionFallbackWorkspaceID( forDeleting: docsID, preferredFallback: reviewID ) XCTAssertEqual(fallback, reviewID) } func testRegistryLoadsPersistedWorkspacesFromStore() { let store = InMemoryWorkspaceStore() let docsID = UUID(uuidString: "11111111-1111-1111-1111-111111111111")! store.savedSummaries = [WorkspaceSummary(id: docsID, name: "Docs")] let registry = makeRegistry(initialWorkspaces: nil, store: store) XCTAssertEqual(registry.allWorkspaceSummaries().count, 1) XCTAssertEqual(registry.allWorkspaceSummaries().first?.id, docsID) XCTAssertEqual(registry.allWorkspaceSummaries().first?.name, "Docs") XCTAssertEqual(registry.defaultWorkspaceID, docsID) } func testRegistryPersistsWorkspaceSummaryChanges() { let store = InMemoryWorkspaceStore() let registry = makeRegistry(store: store) let createdID = registry.createWorkspace(named: "Docs") XCTAssertEqual(store.savedSummaries.map(\.name), ["Main", "Docs"]) registry.renameWorkspace(id: createdID, to: "Review") XCTAssertEqual(store.savedSummaries.map(\.name), ["Main", "Review"]) registry.deleteWorkspace(id: createdID) XCTAssertEqual(store.savedSummaries.map(\.name), ["Main"]) } func testUpdateWorkspaceHotkeyPersistsAndUpdatesSummary() { let store = InMemoryWorkspaceStore() let registry = makeRegistry(store: store) let docsID = registry.createWorkspace(named: "Docs") let hotkey = HotkeyBinding.cmdShiftDigit(4) registry.updateWorkspaceHotkey(id: docsID, to: hotkey) XCTAssertEqual(registry.summary(for: docsID)?.hotkey, hotkey) XCTAssertEqual(store.savedSummaries.last?.hotkey, hotkey) } func testNextAndPreviousWorkspaceWrapAroundRegistryOrder() { let registry = makeRegistry() let mainID = registry.defaultWorkspaceID let docsID = registry.createWorkspace(named: "Docs") let reviewID = registry.createWorkspace(named: "Review") XCTAssertEqual(registry.nextWorkspaceID(after: mainID), docsID) XCTAssertEqual(registry.nextWorkspaceID(after: reviewID), mainID) XCTAssertEqual(registry.previousWorkspaceID(before: mainID), reviewID) XCTAssertEqual(registry.previousWorkspaceID(before: docsID), mainID) } private func makeRegistry( initialWorkspaces: [WorkspaceSummary]? = [], store: (any WorkspaceStoreType)? = nil ) -> WorkspaceRegistry { let settingsProvider = TestSettingsProvider() let sessionFactory = UnusedTerminalSessionFactory() return WorkspaceRegistry( initialWorkspaces: initialWorkspaces, store: store, controllerFactory: { summary in WorkspaceController( summary: summary, sessionFactory: sessionFactory, settingsProvider: settingsProvider, bootstrapDefaultTab: false ) } ) } } @MainActor final class WorkspaceControllerTests: XCTestCase { func testNewTabUsesActiveTabCurrentDirectory() { let factory = RecordingTerminalSessionFactory() let controller = WorkspaceController( summary: WorkspaceSummary(name: "Main"), sessionFactory: factory, settingsProvider: TestSettingsProvider(), bootstrapDefaultTab: false ) controller.newTab() controller.activeTab?.currentDirectory = "/tmp/Raycast" controller.newTab() XCTAssertEqual(factory.requestedDirectories, [nil, "/tmp/Raycast"]) XCTAssertEqual(controller.activeTab?.currentDirectory, "/tmp/Raycast") XCTAssertEqual(controller.tabs.count, 2) XCTAssertEqual(controller.activeTabIndex, 1) } func testNewTabNormalizesCurrentDirectoryFileURL() { let factory = RecordingTerminalSessionFactory() let controller = WorkspaceController( summary: WorkspaceSummary(name: "Main"), sessionFactory: factory, settingsProvider: TestSettingsProvider(), bootstrapDefaultTab: false ) let expectedPath = "/tmp/Raycast Folder" controller.newTab() controller.activeTab?.currentDirectory = URL(fileURLWithPath: expectedPath).absoluteString controller.newTab() XCTAssertEqual(controller.activeTab?.currentDirectory, expectedPath) } func testNewTabFallsBackToHomeDirectoryWhenWorkspaceHasNoTabs() { let factory = RecordingTerminalSessionFactory() let controller = WorkspaceController( summary: WorkspaceSummary(name: "Main"), sessionFactory: factory, settingsProvider: TestSettingsProvider(), bootstrapDefaultTab: false ) controller.newTab() XCTAssertEqual(factory.requestedDirectories, [nil]) XCTAssertEqual(controller.activeTab?.currentDirectory, NSHomeDirectory()) XCTAssertEqual(controller.tabs.count, 1) XCTAssertEqual(controller.activeTabIndex, 0) } } private final class InMemoryWorkspaceStore: WorkspaceStoreType { var savedSummaries: [WorkspaceSummary] = [] func loadWorkspaceSummaries() -> [WorkspaceSummary] { savedSummaries } func saveWorkspaceSummaries(_ summaries: [WorkspaceSummary]) { savedSummaries = summaries } } private final class TestSettingsProvider: TerminalSessionConfigurationProviding { let terminalSessionConfiguration = TerminalSessionConfiguration(fontSize: 13, theme: .terminalApp, shellPath: "") let hotkeySettings = AppSettings.default.hotkeys let terminalSizePresets = TerminalSizePresetStore.loadDefaults() } private final class RecordingTerminalSessionFactory: TerminalSessionFactoryType { private(set) var requestedDirectories: [String?] = [] @MainActor func makeSession( fontSize: CGFloat, theme: TerminalTheme, shellPath: String, initialDirectory: String? ) -> TerminalSession { requestedDirectories.append(initialDirectory) return TerminalSession( fontSize: fontSize, theme: theme, shellPath: shellPath, initialDirectory: initialDirectory, startImmediately: false ) } } private struct UnusedTerminalSessionFactory: TerminalSessionFactoryType { @MainActor func makeSession( fontSize: CGFloat, theme: TerminalTheme, shellPath: String, initialDirectory: String? ) -> TerminalSession { fatalError("WorkspaceRegistryTests should not create live terminal sessions.") } }