native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370import AppKitimport ComposableArchitectureimport DependenciesTestSupportimport Foundationimport IdentifiedCollectionsimport ProwlCLISharedimport SwiftUIimport Testing
@testable import Prowl
@MainActorstruct AppFeatureCommandPaletteTests { @Test(.dependencies) func closingCommandPaletteRestoresSelectedTerminalFocus() async { let worktree = makeWorktree( id: "/tmp/repo-focus/wt-1", name: "wt-1", repoRoot: "/tmp/repo-focus" ) let repository = makeRepository(id: "/tmp/repo-focus", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) var state = AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) state.commandPalette.isPresented = true let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.commandPalette(.setPresented(false))) { $0.commandPalette.isPresented = false } await store.finish()
#expect(sent.value == [.focusSelectedTab(worktree)]) }
@Test(.dependencies) func togglingPresentedCommandPaletteClosedRestoresSelectedTerminalFocus() async { let worktree = makeWorktree( id: "/tmp/repo-toggle-focus/wt-1", name: "wt-1", repoRoot: "/tmp/repo-toggle-focus" ) let repository = makeRepository(id: "/tmp/repo-toggle-focus", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) var state = AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) state.commandPalette.isPresented = true let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.commandPalette(.togglePresented)) { $0.commandPalette.isPresented = false } await store.finish()
#expect(sent.value == [.focusSelectedTab(worktree)]) }
@Test(.dependencies) func closingCommandPaletteDoesNotRestoreFocusWithoutSelectedTerminal() async { var state = AppFeature.State() state.commandPalette.isPresented = true let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.commandPalette(.setPresented(false))) { $0.commandPalette.isPresented = false } await store.finish()
#expect(sent.value.isEmpty) }
@Test(.dependencies) func closingCommandPaletteInCanvasRestoresCanvasFocusedTerminalFocus() async { let worktree = makeWorktree( id: "/tmp/repo-canvas-focus/wt-1", name: "wt-1", repoRoot: "/tmp/repo-canvas-focus" ) let repository = makeRepository(id: "/tmp/repo-canvas-focus", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .canvas var state = AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) state.commandPalette.isPresented = true let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.canvasFocusedWorktreeID = { worktree.id } $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.commandPalette(.setPresented(false))) { $0.commandPalette.isPresented = false } await store.finish()
#expect(sent.value == [.focusSelectedTab(worktree)]) }
@Test(.dependencies) func passiveCommandPaletteCommandInCanvasRestoresCanvasFocusedTerminalFocus() async { let worktree = makeWorktree( id: "/tmp/repo-canvas-passive/wt-1", name: "wt-1", repoRoot: "/tmp/repo-canvas-passive" ) let repository = makeRepository(id: "/tmp/repo-canvas-passive", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .canvas let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.canvasFocusedWorktreeID = { worktree.id } $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.commandPalette(.delegate(.checkForUpdates))) await store.receive(\.updates.checkForUpdates) await store.finish()
#expect(sent.value == [.focusSelectedTab(worktree)]) }
@Test(.dependencies) func ghosttyCommandDelegateInCanvasUsesCanvasFocusedWorktree() async { let worktree = makeWorktree( id: "/tmp/repo-canvas-ghostty/wt-1", name: "wt-1", repoRoot: "/tmp/repo-canvas-ghostty" ) let repository = makeRepository(id: "/tmp/repo-canvas-ghostty", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .canvas let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.canvasFocusedWorktreeID = { worktree.id } $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.commandPalette(.delegate(.ghosttyCommand("new_tab")))) await store.finish()
#expect( sent.value == [ .performBindingAction(worktree, action: "new_tab"), .focusSelectedTab(worktree), ] ) }
@Test(.dependencies) func passiveCommandPaletteCommandRestoresSelectedTerminalFocus() async { let worktree = makeWorktree( id: "/tmp/repo-passive/wt-1", name: "wt-1", repoRoot: "/tmp/repo-passive" ) let repository = makeRepository(id: "/tmp/repo-passive", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.commandPalette(.delegate(.checkForUpdates))) await store.receive(\.updates.checkForUpdates) await store.finish()
#expect(sent.value == [.focusSelectedTab(worktree)]) }
@Test(.dependencies) func selectingWorktreeDoesNotRestorePreviousTerminalFocus() async { let worktree = makeWorktree( id: "/tmp/repo-select/wt-1", name: "wt-1", repoRoot: "/tmp/repo-select" ) let repository = makeRepository(id: "/tmp/repo-select", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.selectWorktree(worktree.id)))) await store.finish()
#expect(!sent.value.contains(.focusSelectedTab(worktree))) }
@Test(.dependencies) func selectingWorktreeInCanvasFocusesCanvasCard() async { let worktree = makeWorktree( id: "/tmp/repo-select-canvas/wt-1", name: "wt-1", repoRoot: "/tmp/repo-select-canvas" ) let repository = makeRepository(id: "/tmp/repo-select-canvas", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .canvas let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() }
await store.send(.commandPalette(.delegate(.selectWorktree(worktree.id)))) await store.receive(\.repositories.focusCanvasWorktree) { $0.repositories.nextCanvasFocusRequestID = 1 $0.repositories.pendingCanvasFocusRequest = CanvasFocusRequest( id: 1, target: .worktree(worktree.id) ) $0.repositories.openedWorktreeIDs = [worktree.id] } }
@Test(.dependencies) func selectingPlainFolderInCanvasFocusesCanvasCard() async { let repository = Repository( id: "/tmp/folder-select-canvas", rootURL: URL(fileURLWithPath: "/tmp/folder-select-canvas"), name: "folder-select-canvas", kind: .plain, worktrees: [] ) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .canvas let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() }
await store.send(.commandPalette(.delegate(.selectWorktree(repository.id)))) await store.receive(\.repositories.focusCanvasRepository) { $0.repositories.nextCanvasFocusRequestID = 1 $0.repositories.pendingCanvasFocusRequest = CanvasFocusRequest( id: 1, target: .worktree(repository.id) ) $0.repositories.openedWorktreeIDs = [repository.id] } }
@Test(.dependencies) func openSettingsShowsWindow() async { let shown = LockIsolated(false) var state = AppFeature.State() state.settings.selection = .updates let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.settingsWindowClient.show = { shown.withValue { $0 = true } } }
await store.send(.commandPalette(.delegate(.openSettings))) await store.receive(\.settings.setSelection) { $0.settings.selection = .general } await store.finish() #expect(shown.value) }
@Test(.dependencies) func newWorktreeDispatchesCreateRandomWorktree() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() }
let expectedAlert = AlertState<RepositoriesFeature.Alert> { TextState("Unable to create worktree") } actions: { ButtonState(role: .cancel) { TextState("OK") } } message: { TextState("Open a repository to create a worktree.") }
await store.send(.commandPalette(.delegate(.newWorktree))) await store.receive(\.repositories.worktreeCreation.createRandomWorktree) { $0.repositories.alert = expectedAlert } }
@Test(.dependencies) func openRepositoryShowsOpenPanel() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() }
await store.send(.commandPalette(.delegate(.openRepository))) await store.receive(\.repositories.setOpenPanelPresented) { $0.repositories.isOpenPanelPresented = true } }
@Test(.dependencies) func newWorkspaceDispatchesPromptRequest() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() }
let title = "Workspace" let requestedRootPath = defaultWorkspaceBaseRootPath(for: title) let resolvedRootPath = expectedDefaultWorkspaceRootPath(for: title) await store.send(.commandPalette(.delegate(.newWorkspace))) await store.receive(\.repositories.workspaceCreation.promptRequested) { $0.repositories.workspaceEditor = WorkspaceEditorFeature.State( repositories: [], title: title, rootPath: requestedRootPath ) } if resolvedRootPath == requestedRootPath { await store.receive(\.repositories.workspaceCreation.defaultRootPathResolved) } else { await store.receive(\.repositories.workspaceCreation.defaultRootPathResolved) { $0.repositories.workspaceEditor?.rootPath = resolvedRootPath } } }
@Test(.dependencies) func refreshWorktreesDispatchesRefresh() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.refreshWorktrees))) await store.receive(\.repositories.refreshWorktrees) }
@Test(.dependencies) func checkForUpdatesDispatchesUpdateAction() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() }
await store.send(.commandPalette(.delegate(.checkForUpdates))) await store.receive(\.updates.checkForUpdates) }
@Test(.dependencies) func jumpToLatestUnreadDispatchesAppAction() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() }
await store.send(.commandPalette(.delegate(.jumpToLatestUnread))) await store.receive(\.jumpToLatestUnread) }
@Test(.dependencies) func ghosttyCommandDispatchesBindingActionToTerminalClient() async { let worktree = makeWorktree( id: "/tmp/repo-ghostty/wt-1", name: "wt-1", repoRoot: "/tmp/repo-ghostty" ) let repository = makeRepository(id: "/tmp/repo-ghostty", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.commandPalette(.delegate(.ghosttyCommand("goto_split:right")))) await store.finish()
// Two effects run in parallel (.merge) — assert both fire without // depending on dispatch order. With no selected surface available the // dispatch falls back to the focused-surface command. #expect(sent.value.count == 2) #expect(sent.value.contains(.performBindingAction(worktree, action: "goto_split:right"))) #expect(sent.value.contains(.focusSelectedTab(worktree))) }
@Test(.dependencies) func ghosttyCommandTargetsSelectedSurfaceWhenAvailable() async { let worktree = makeWorktree( id: "/tmp/repo-ghostty/wt-1", name: "wt-1", repoRoot: "/tmp/repo-ghostty" ) let repository = makeRepository(id: "/tmp/repo-ghostty", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) let surfaceID = UUID() let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } $0.terminalClient.selectedSurfaceID = { _ in surfaceID } }
await store.send(.commandPalette(.delegate(.ghosttyCommand("goto_split:right")))) await store.finish()
#expect(sent.value.count == 2) #expect( sent.value.contains( .performBindingActionOnSurface(worktree, surfaceID: surfaceID, action: "goto_split:right") ) ) #expect(sent.value.contains(.focusSelectedTab(worktree))) }
@Test(.dependencies) func ghosttyCommandCapturesSelectedSurfaceBeforeAsyncDispatch() async { let worktree = makeWorktree( id: "/tmp/repo-ghostty/wt-1", name: "wt-1", repoRoot: "/tmp/repo-ghostty" ) let repository = makeRepository(id: "/tmp/repo-ghostty", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) let firstSurface = UUID() let secondSurface = UUID() let currentSurface = LockIsolated(firstSurface) let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } $0.terminalClient.selectedSurfaceID = { _ in currentSurface.value } }
let task = await store.send(.commandPalette(.delegate(.ghosttyCommand("toggle_split_zoom")))) // Simulates the palette-dismiss focus drift: by the time the async dispatch // resolves, `selectedSurfaceID` would already point at the leftmost surface. currentSurface.setValue(secondSurface) await task.finish() await store.finish()
#expect( sent.value.contains( .performBindingActionOnSurface(worktree, surfaceID: firstSurface, action: "toggle_split_zoom") ) ) #expect( !sent.value.contains( .performBindingActionOnSurface(worktree, surfaceID: secondSurface, action: "toggle_split_zoom") ) ) }
@Test(.dependencies) func viewToggleDelegateRestoresTerminalFocusByDefault() async { let worktree = makeWorktree( id: "/tmp/repo-view-toggle/wt-1", name: "wt-1", repoRoot: "/tmp/repo-view-toggle" ) let repository = makeRepository(id: "/tmp/repo-view-toggle", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.toggleLeftSidebar))) await store.finish()
#expect(sent.value.contains(.focusSelectedTab(worktree))) }
@Test(.dependencies) func toggleCanvasDelegateDoesNotRestoreTerminalFocus() async { let worktree = makeWorktree( id: "/tmp/repo-canvas-toggle/wt-1", name: "wt-1", repoRoot: "/tmp/repo-canvas-toggle" ) let repository = makeRepository(id: "/tmp/repo-canvas-toggle", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) let sent = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.toggleCanvas))) await store.finish()
#expect(!sent.value.contains(.focusSelectedTab(worktree))) }
@Test(.dependencies) func revealInFinderDispatchesOpenWorktreeFinder() async { let worktree = makeWorktree( id: "/tmp/repo-finder/wt-1", name: "wt-1", repoRoot: "/tmp/repo-finder" ) let repository = makeRepository(id: "/tmp/repo-finder", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.revealInFinder))) await store.receive(\.openWorktree) }
@Test(.dependencies) func copyPathWritesWorktreePathToPasteboard() async { let worktree = makeWorktree( id: "/tmp/repo-copy/wt-1", name: "wt-1", repoRoot: "/tmp/repo-copy" ) let repository = makeRepository(id: "/tmp/repo-copy", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() }
NSPasteboard.general.clearContents() NSPasteboard.general.setString("__sentinel__", forType: .string)
await store.send(.commandPalette(.delegate(.copyPath))) await store.finish()
#expect(NSPasteboard.general.string(forType: .string) == worktree.workingDirectory.path) }
@Test(.dependencies) func copyPathWithoutSelectedWorktreeIsNoop() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() }
await store.send(.commandPalette(.delegate(.copyPath))) await store.finish() }
@Test(.dependencies) func revealInSidebarShowsSidebarAndReveals() async { let worktree = makeWorktree( id: "/tmp/repo-reveal/wt-1", name: "wt-1", repoRoot: "/tmp/repo-reveal" ) let repository = makeRepository(id: "/tmp/repo-reveal", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) var appState = AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) appState.leftSidebarVisibility = .detailOnly let store = TestStore(initialState: appState) { AppFeature() } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.revealInSidebar))) await store.receive(\.showLeftSidebar) { $0.leftSidebarVisibility = .all } await store.receive(\.repositories.revealSelectedWorktreeInSidebar) }
@Test(.dependencies) func revealInSidebarWithoutSelectedWorktreeIsNoop() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() }
await store.send(.commandPalette(.delegate(.revealInSidebar))) await store.finish() }
@Test(.dependencies) func runScriptDelegateDispatchesAppAction() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.runScript))) await store.receive(\.runScript) }
@Test(.dependencies) func stopRunScriptDelegateDispatchesAppAction() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.stopRunScript))) await store.receive(\.stopRunScript) }
@Test(.dependencies) func togglePinWorktreeWhenNotPinnedDispatchesPin() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() } store.exhaustivity = .off
await store.send( .commandPalette(.delegate(.togglePinWorktree("/tmp/repo/wt-1", isCurrentlyPinned: false))) ) await store.receive(\.repositories.worktreeOrdering.pinWorktree) }
@Test(.dependencies) func togglePinWorktreeWhenPinnedDispatchesUnpin() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() } store.exhaustivity = .off
await store.send( .commandPalette(.delegate(.togglePinWorktree("/tmp/repo/wt-1", isCurrentlyPinned: true))) ) await store.receive(\.repositories.worktreeOrdering.unpinWorktree) }
@Test(.dependencies) func renameBranchDelegateDispatchesRequestPrompt() async { let worktree = makeWorktree( id: "/tmp/repo-rename/wt-1", name: "wt-1", repoRoot: "/tmp/repo-rename" ) let repository = makeRepository(id: "/tmp/repo-rename", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.renameBranch))) await store.receive(\.repositories.requestRenameBranchPrompt) { $0.repositories.nextPendingRenameBranchRequestID = 1 $0.repositories.pendingRenameBranchRequest = PendingRenameBranchRequest( id: 1, worktreeID: worktree.id, currentName: worktree.name ) } }
@Test(.dependencies) func renameBranchDelegateUsesCanvasFocusedWorktree() async { let worktree = makeWorktree( id: "/tmp/repo-canvas-rename/wt-1", name: "wt-1", repoRoot: "/tmp/repo-canvas-rename" ) let repository = makeRepository(id: "/tmp/repo-canvas-rename", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .canvas let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.canvasFocusedWorktreeID = { worktree.id } } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.renameBranch))) await store.receive(\.repositories.requestRenameBranchPrompt) { $0.repositories.nextPendingRenameBranchRequestID = 1 $0.repositories.pendingRenameBranchRequest = PendingRenameBranchRequest( id: 1, worktreeID: worktree.id, currentName: worktree.name ) } }
@Test(.dependencies) func renameBranchDelegateNoopsDuringSidebarMultiSelection() async { let worktreeA = makeWorktree( id: "/tmp/repo-multi-rename/wt-a", name: "wt-a", repoRoot: "/tmp/repo-multi-rename" ) let worktreeB = makeWorktree( id: "/tmp/repo-multi-rename/wt-b", name: "wt-b", repoRoot: "/tmp/repo-multi-rename" ) let repository = makeRepository(id: "/tmp/repo-multi-rename", worktrees: [worktreeA, worktreeB]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktreeA.id) repositoriesState.sidebarSelectedWorktreeIDs = [worktreeA.id, worktreeB.id] let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() }
await store.send(.commandPalette(.delegate(.renameBranch))) await store.finish() }
@Test(.dependencies) func renameBranchDelegateNoopsWithoutSelectedWorktree() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() }
await store.send(.commandPalette(.delegate(.renameBranch))) await store.finish() }
@Test(.dependencies) func openRepositorySettingsDelegateNavigatesAndShowsWindow() async { // Palette handler funnels through the existing // repositories.repositoryManagement.openRepositorySettings flow, which // guards on the repo actually existing. let repository = makeRepository(id: "/tmp/repo-x", worktrees: []) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] let shown = LockIsolated(false) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.settingsWindowClient.show = { shown.withValue { $0 = true } } } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.openRepositorySettings("/tmp/repo-x")))) await store.receive(\.settings.setSelection) { $0.settings.selection = .repository("/tmp/repo-x") } await store.finish() #expect(shown.value) }
@Test(.dependencies) func runCustomCommandDelegateDispatchesAppAction() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() } store.exhaustivity = .off
let commandID = EffectiveCustomCommand.Identifier(source: .global, commandID: "global-command") await store.send(.commandPalette(.delegate(.runCustomCommand(commandID)))) await store.receive(\.runCustomCommand) }
@Test(.dependencies) func showDiffDelegateUsesConfiguredExternalDiffTool() async { let worktree = makeWorktree( id: "/tmp/repo-diff/wt-1", name: "wt-1", repoRoot: "/tmp/repo-diff" ) let repository = makeRepository(id: "/tmp/repo-diff", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) var settings = GlobalSettings.default settings.externalDiffToolID = ExternalDiffTool.custom.settingsID settings.externalDiffCustomCommand = "my-diff {leftPath} {rightPath}" let storage = SettingsTestStorage() let settingsFileURL = URL( fileURLWithPath: "/tmp/prowl-settings-\(UUID().uuidString).json" ) let launched = LockIsolated<[(ExternalDiffSettings, DiffTarget)]>([]) let store = withDependencies { $0.settingsFileStorage = storage.storage $0.settingsFileURL = settingsFileURL $0.terminalClient.send = { _ in } $0.externalDiffToolClient.open = { settings, target, _, _ in launched.withValue { $0.append((settings, target)) } } } operation: { @Shared(.settingsFile) var settingsFile $settingsFile.withLock { $0.global = settings } return TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State(settings: settings) ) ) { AppFeature() } }
await store.send(.commandPalette(.delegate(.showDiff))) await store.finish()
#expect( launched.value.map(\.0) == [ ExternalDiffSettings( toolID: ExternalDiffTool.custom.settingsID, customCommand: "my-diff {leftPath} {rightPath}" ) ] ) #expect(launched.value.map(\.1) == [DiffTarget(worktree: worktree)]) }
@Test(.dependencies) func showSelectedWorktreeDiffUsesConfiguredExternalDiffTool() async { let worktree = makeWorktree( id: "/tmp/repo-shortcut-diff/wt-1", name: "wt-1", repoRoot: "/tmp/repo-shortcut-diff" ) let repository = makeRepository(id: "/tmp/repo-shortcut-diff", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) var settings = GlobalSettings.default settings.externalDiffToolID = ExternalDiffTool.custom.settingsID settings.externalDiffCustomCommand = "my-diff {leftPath} {rightPath}" let storage = SettingsTestStorage() let settingsFileURL = URL( fileURLWithPath: "/tmp/prowl-settings-\(UUID().uuidString).json" ) let launched = LockIsolated<[(ExternalDiffSettings, DiffTarget)]>([]) let store = withDependencies { $0.settingsFileStorage = storage.storage $0.settingsFileURL = settingsFileURL $0.terminalClient.send = { _ in } $0.externalDiffToolClient.open = { settings, target, _, _ in launched.withValue { $0.append((settings, target)) } } } operation: { @Shared(.settingsFile) var settingsFile $settingsFile.withLock { $0.global = settings } return TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State(settings: settings) ) ) { AppFeature() } }
await store.send(.showSelectedWorktreeDiff) await store.finish()
#expect( launched.value.map(\.0) == [ ExternalDiffSettings( toolID: ExternalDiffTool.custom.settingsID, customCommand: "my-diff {leftPath} {rightPath}" ) ] ) #expect(launched.value.map(\.1) == [DiffTarget(worktree: worktree)]) }
@Test(.dependencies) func showDiffForWorkspaceChildTargetsChildRepository() async { let entry = ProjectWorkspace.RepositoryEntry( id: "app", name: "App", path: "app", sourceKind: .existingPath ) let workspace = Repository( id: "/tmp/ws-child-diff", rootURL: URL(fileURLWithPath: "/tmp/ws-child-diff"), name: "Workspace", kind: .plain, worktrees: [], workspace: ProjectWorkspace(title: "Workspace", repositories: [entry]) ) let childID = entry.resolvedURL(relativeTo: workspace.rootURL).path(percentEncoded: false) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [workspace] repositoriesState.selection = .repository(workspace.id) repositoriesState.selectedWorkspaceChildID = childID repositoriesState.workspaceChildBranchByID[childID] = "feature/child"
let launched = LockIsolated<[DiffTarget]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.externalDiffToolClient.open = { _, target, _, _ in launched.withValue { $0.append(target) } } // The effect canonicalizes the child's repository root at invocation // time; the resolved root must reach the diff client. $0.gitClient.repoRoot = { _ in URL(fileURLWithPath: "/tmp/child-source-root") } } store.exhaustivity = .off
// Badge/context-menu route and selection-following shortcut route must // resolve to the same child target. await store.send( .repositories(.delegate(.showDiff(.workspaceChild(workspaceID: workspace.id, path: childID)))) ) await store.send(.showSelectedWorktreeDiff) await store.finish()
let childURL = URL(fileURLWithPath: childID) #expect(launched.value.count == 2) for target in launched.value { #expect(target.id == .workspaceChild(workspaceID: workspace.id, path: childID)) #expect(target.workingDirectory == childURL) #expect(target.branchName == "feature/child") #expect(target.repositoryRootURL == URL(fileURLWithPath: "/tmp/child-source-root")) #expect(target.terminalHost.id == workspace.id) #expect(target.terminalWorkingDirectory == childURL) } }
@Test(.dependencies) func outgoingChangesForWorkspaceChildTargetsChildRepository() async { let entry = ProjectWorkspace.RepositoryEntry( id: "app", name: "App", path: "app", sourceKind: .existingPath ) let workspace = Repository( id: "/tmp/ws-child-outgoing", rootURL: URL(fileURLWithPath: "/tmp/ws-child-outgoing"), name: "Workspace", kind: .plain, worktrees: [], workspace: ProjectWorkspace(title: "Workspace", repositories: [entry]) ) let childID = entry.resolvedURL(relativeTo: workspace.rootURL).path(percentEncoded: false) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [workspace] repositoriesState.selection = .repository(workspace.id) repositoriesState.selectedWorkspaceChildID = childID
let outgoingRequests = LockIsolated<[DiffTarget]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.outgoingChangesClient.open = { target, _, _ in outgoingRequests.withValue { $0.append(target) } } // A failed root canonicalization keeps the metadata fallback instead of // blocking the action. $0.gitClient.repoRoot = { _ in throw NSError(domain: "test", code: 1) } } store.exhaustivity = .off
// Row context-menu delegate and selection-following shortcut/palette // route must both reach the outgoing client with the child target. await store.send( .repositories( .delegate(.showOutgoingChanges(.workspaceChild(workspaceID: workspace.id, path: childID))) ) ) await store.send(.showSelectedWorktreeOutgoingChanges) await store.finish()
let childURL = URL(fileURLWithPath: childID) #expect(outgoingRequests.value.count == 2) for target in outgoingRequests.value { #expect(target.id == .workspaceChild(workspaceID: workspace.id, path: childID)) #expect(target.workingDirectory == childURL) #expect(target.repositoryRootURL == childURL) } }
@Test func commandPaletteOffersDiffItemsForSelectedWorkspaceChild() { let entry = ProjectWorkspace.RepositoryEntry( id: "app", name: "App", path: "app", sourceKind: .existingPath ) let workspace = Repository( id: "/tmp/ws-palette-diff", rootURL: URL(fileURLWithPath: "/tmp/ws-palette-diff"), name: "Workspace", kind: .plain, worktrees: [], workspace: ProjectWorkspace(title: "Workspace", repositories: [entry]) ) let childID = entry.resolvedURL(relativeTo: workspace.rootURL).path(percentEncoded: false) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [workspace] repositoriesState.selection = .repository(workspace.id)
// Workspace selected without a child: no diff target, no diff items. let itemsWithoutChild = CommandPaletteFeature.commandPaletteItems(from: repositoriesState) #expect(!itemsWithoutChild.contains { $0.kind == .showDiff }) #expect(!itemsWithoutChild.contains { $0.kind == .outgoingChanges })
repositoriesState.selectedWorkspaceChildID = childID let items = CommandPaletteFeature.commandPaletteItems(from: repositoriesState) #expect(items.contains { $0.kind == .showDiff }) #expect(items.contains { $0.kind == .outgoingChanges }) }
@Test(.dependencies) func outgoingChangesAlwaysUsesBuiltInClient() async { let worktree = makeWorktree( id: "/tmp/repo-outgoing/wt-1", name: "wt-1", repoRoot: "/tmp/repo-outgoing" ) let repository = makeRepository(id: "/tmp/repo-outgoing", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) repositoriesState.worktreeInfoByID[worktree.id] = WorktreeInfoEntry( addedLines: nil, removedLines: nil, pullRequest: GithubPullRequest( number: 42, title: "Outgoing Changes", state: "OPEN", additions: 1, deletions: 0, isDraft: false, reviewDecision: nil, mergeable: nil, mergeStateStatus: nil, updatedAt: nil, url: "https://github.com/upstream/project/pull/42", headRefName: worktree.name, baseRefName: "main", commitsCount: 1, authorLogin: "onevcat", statusCheckRollup: nil ) ) #expect( CommandPaletteFeature.commandPaletteItems(from: repositoriesState).contains { $0.kind == .outgoingChanges } ) let outgoingRequests = LockIsolated<[DiffTarget]>([]) let externalRequests = LockIsolated(0) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.outgoingChangesClient.open = { target, _, _ in outgoingRequests.withValue { $0.append(target) } } $0.externalDiffToolClient.open = { _, _, _, _ in externalRequests.withValue { $0 += 1 } } }
await store.send(.commandPalette(.delegate(.showOutgoingChanges))) await store.send(.showSelectedWorktreeOutgoingChanges) await store.finish()
let requests = outgoingRequests.value #expect(requests == [DiffTarget(worktree: worktree), DiffTarget(worktree: worktree)]) #expect(externalRequests.value == 0) }
@Test(.dependencies) func outgoingChangesWithoutPullRequestStillOpensViaClient() async { let worktree = makeWorktree( id: "/tmp/repo-outgoing-no-pr/wt-1", name: "wt-1", repoRoot: "/tmp/repo-outgoing-no-pr" ) let repository = makeRepository(id: "/tmp/repo-outgoing-no-pr", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) let outgoingRequests = LockIsolated<[DiffTarget]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.outgoingChangesClient.open = { target, _, _ in outgoingRequests.withValue { $0.append(target) } } }
await store.send(.showSelectedWorktreeOutgoingChanges) await store.finish()
#expect(outgoingRequests.value == [DiffTarget(worktree: worktree)]) #expect(store.state.alert == nil) }
@Test(.dependencies) func outgoingChangesResolutionFailureShowsAnAlert() async { let worktree = makeWorktree( id: "/tmp/repo-outgoing-fail/wt-1", name: "wt-1", repoRoot: "/tmp/repo-outgoing-fail" ) let repository = makeRepository(id: "/tmp/repo-outgoing-fail", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(worktree.id) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.outgoingChangesClient.open = { _, _, onError in onError( OpenActionError( title: "Unable to show outgoing changes", message: OutgoingBaseResolutionError.noResolvableBase.localizedDescription ) ) } } store.exhaustivity = .off
await store.send(.showSelectedWorktreeOutgoingChanges) await store.receive(\.openWorktreeFailed)
#expect(store.state.alert != nil) }
@Test(.dependencies) func sidebarOutgoingChangesDelegateTargetsTheRequestedWorktree() async { let selected = makeWorktree( id: "/tmp/repo-outgoing-target/wt-selected", name: "wt-selected", repoRoot: "/tmp/repo-outgoing-target" ) let targeted = makeWorktree( id: "/tmp/repo-outgoing-target/wt-targeted", name: "wt-targeted", repoRoot: "/tmp/repo-outgoing-target" ) let repository = makeRepository(id: "/tmp/repo-outgoing-target", worktrees: [selected, targeted]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] repositoriesState.selection = .worktree(selected.id) let outgoingRequests = LockIsolated<[DiffTarget]>([]) let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.outgoingChangesClient.open = { target, _, _ in outgoingRequests.withValue { $0.append(target) } } } store.exhaustivity = .off
await store.send(.repositories(.delegate(.showOutgoingChanges(.worktree(targeted.id))))) await store.finish()
#expect(outgoingRequests.value == [DiffTarget(worktree: targeted)]) }
@Test(.dependencies) func closePullRequestDispatchesAction() async { let store = TestStore(initialState: AppFeature.State()) { AppFeature() } store.exhaustivity = .off
await store.send(.commandPalette(.delegate(.closePullRequest("/tmp/repo/wt-close")))) await store.receive(\.repositories.githubIntegration.pullRequestAction) }
@Test(.dependencies) func deleteWorktreeDispatchesRequest() async { let worktree = makeWorktree( id: "/tmp/repo-run/wt-1", name: "wt-1", repoRoot: "/tmp/repo-run" ) let repository = makeRepository(id: "/tmp/repo-run", worktrees: [worktree]) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = [repository] let store = TestStore( initialState: AppFeature.State( repositories: repositoriesState, settings: SettingsFeature.State() ) ) { AppFeature() }
await store.send(.commandPalette(.delegate(.deleteWorktree(worktree.id, repository.id)))) await store.receive(\.repositories.worktreeLifecycle.requestDeleteWorktree) { $0.repositories.deleteWorktreeConfirmation = DeleteWorktreeConfirmation( id: 0, title: "Delete worktree?", message: "Delete \(worktree.name)? The worktree directory will be removed.", targets: [RepositoriesFeature.DeleteWorktreeTarget(worktreeID: worktree.id, repositoryID: repository.id)], deleteBranch: false ) $0.repositories.nextDeleteWorktreeConfirmationID = 1 } }
}
private func makeWorktree(id: String, name: String, repoRoot: String = "/tmp/repo") -> Worktree { Worktree( id: id, name: name, detail: "detail", workingDirectory: URL(fileURLWithPath: id), repositoryRootURL: URL(fileURLWithPath: repoRoot) )}
private func makeRepository(id: String, worktrees: [Worktree]) -> Repository { Repository( id: id, rootURL: URL(fileURLWithPath: id), name: "repo", worktrees: IdentifiedArray(uniqueElements: worktrees) )}