native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087import ComposableArchitectureimport DependenciesTestSupportimport Foundationimport IdentifiedCollectionsimport ProwlCLISharedimport Sharingimport Testing
@testable import Prowl
@MainActorstruct ShelfFeatureTests { @Test(.dependencies) func toggleShelfFromWorktreeEntersShelfWithoutRedirecting() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let worktree = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [worktree]) ) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.toggleShelf) { $0.isShelfActive = true $0.openedWorktreeIDs = [worktree.id] $0.pendingTerminalFocusWorktreeIDs = [worktree.id] } await store.finish() }
@Test(.dependencies) func toggleShelfWhileActiveExitsShelf() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let worktree = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [worktree]) ) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) state.isShelfActive = true let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.toggleShelf) { $0.isShelfActive = false } await store.finish() }
@Test(.dependencies) func toggleShelfWithoutWorktreesIsNoOp() async { let store = TestStore(initialState: RepositoriesFeature.State()) { RepositoriesFeature() }
await store.send(.toggleShelf) await store.finish() }
@Test(.dependencies) func toggleShelfFromCanvasRedirectsToWorktreeAndEntersShelf() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let worktree = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [worktree]) ) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .canvas state.lastFocusedWorktreeID = worktree.id let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.toggleShelf) { $0.isShelfActive = true } await store.receive(\.selectWorktree) { $0.selection = .worktree(worktree.id) $0.sidebarSelectedWorktreeIDs = [worktree.id] $0.pendingTerminalFocusWorktreeIDs = [worktree.id] $0.openedWorktreeIDs = [worktree.id] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func toggleShelfFromCanvasPrefersCanvasFocusedCard() async { // Canvas can have a focused card distinct from `lastFocusedWorktreeID` // (which is only updated while `selection` is `.worktree`). A direct // Canvas → Shelf switch should open *that* card as the active book. let rootURL = URL(fileURLWithPath: "/tmp/repo") let worktreeA = Worktree( id: "/tmp/repo/wt-a", name: "wt-a", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt-a"), repositoryRootURL: rootURL ) let worktreeB = Worktree( id: "/tmp/repo/wt-b", name: "wt-b", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt-b"), repositoryRootURL: rootURL ) let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [worktreeA, worktreeB]) ) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .canvas state.lastFocusedWorktreeID = worktreeA.id let store = TestStore(initialState: state) { RepositoriesFeature() } withDependencies: { $0.terminalClient.canvasFocusedWorktreeID = { worktreeB.id } }
await store.send(.toggleShelf) { $0.isShelfActive = true } await store.receive(\.selectWorktree) { $0.selection = .worktree(worktreeB.id) $0.sidebarSelectedWorktreeIDs = [worktreeB.id] $0.pendingTerminalFocusWorktreeIDs = [worktreeB.id] $0.openedWorktreeIDs = [worktreeB.id] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func toggleShelfFromArchivedRedirectsToWorktreeAndEntersShelf() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let worktree = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [worktree]) ) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .archivedWorktrees let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.toggleShelf) { $0.isShelfActive = true } await store.receive(\.selectWorktree) { $0.selection = .worktree(worktree.id) $0.sidebarSelectedWorktreeIDs = [worktree.id] $0.pendingTerminalFocusWorktreeIDs = [worktree.id] $0.openedWorktreeIDs = [worktree.id] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func selectingADifferentWorktreeKeepsShelfActive() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let first = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let second = Worktree( id: "/tmp/repo/wt2", name: "wt2", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt2"), repositoryRootURL: rootURL ) let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [first, second]) ) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(first.id) state.isShelfActive = true let store = TestStore(initialState: state) { RepositoriesFeature() }
// Mirrors "user clicks second worktree in the left navigation // while in Shelf mode": Shelf must not exit; only the open book // changes via the new `selectedWorktreeID`. await store.send(.selectWorktree(second.id, focusTerminal: true)) { $0.selection = .worktree(second.id) $0.sidebarSelectedWorktreeIDs = [second.id] $0.pendingTerminalFocusWorktreeIDs = [second.id] $0.openedWorktreeIDs = [second.id] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func selectCanvasClearsShelfActiveFlag() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let worktree = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [worktree]) ) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) state.isShelfActive = true let store = TestStore(initialState: state) { RepositoriesFeature() } withDependencies: { $0.terminalClient.send = { _ in } }
await store.send(.selectCanvas) { $0.preCanvasWorktreeID = worktree.id $0.preCanvasTerminalTargetID = worktree.id $0.isShelfActive = false $0.selection = .canvas $0.sidebarSelectedWorktreeIDs = [] } await store.finish() }
@Test(.dependencies) func selectShelfBookByIndexDispatchesWorktreeSelection() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let wt1 = Worktree( id: "/tmp/repo", name: "main", detail: "", workingDirectory: rootURL, repositoryRootURL: rootURL ) let wt2 = Worktree( id: "/tmp/repo/feature", name: "feature", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/feature"), repositoryRootURL: rootURL ) let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [wt1, wt2]) ) var state = RepositoriesFeature.State(repositories: [repo]) state.repositoryRoots = [rootURL] state.repositoryOrderIDs = [repo.id] state.selection = .worktree(wt1.id) state.isShelfActive = true state.openedWorktreeIDs = [wt1.id, wt2.id] let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.selectShelfBook(2)) await store.receive(\.selectWorktree) { $0.selection = .worktree(wt2.id) $0.sidebarSelectedWorktreeIDs = [wt2.id] $0.pendingTerminalFocusWorktreeIDs = [wt2.id] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func selectShelfBookOutOfRangeIsNoOp() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let wt1 = Worktree( id: "/tmp/repo", name: "main", detail: "", workingDirectory: rootURL, repositoryRootURL: rootURL ) let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [wt1]) ) var state = RepositoriesFeature.State(repositories: [repo]) state.repositoryRoots = [rootURL] state.repositoryOrderIDs = [repo.id] state.selection = .worktree(wt1.id) let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.selectShelfBook(5)) await store.finish() }
@Test(.dependencies) func selectNextShelfBookWrapsAround() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let wt1 = Worktree( id: "/tmp/repo", name: "main", detail: "", workingDirectory: rootURL, repositoryRootURL: rootURL ) let wt2 = Worktree( id: "/tmp/repo/feature", name: "feature", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/feature"), repositoryRootURL: rootURL ) let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [wt1, wt2]) ) var state = RepositoriesFeature.State(repositories: [repo]) state.repositoryRoots = [rootURL] state.repositoryOrderIDs = [repo.id] state.selection = .worktree(wt2.id) // Currently on the last book. state.openedWorktreeIDs = [wt1.id, wt2.id] let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.selectNextShelfBook) // Wrapping: next-after-last lands back on the first book. await store.receive(\.selectWorktree) { $0.selection = .worktree(wt1.id) $0.sidebarSelectedWorktreeIDs = [wt1.id] $0.pendingTerminalFocusWorktreeIDs = [wt1.id] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func selectPreviousShelfBookWrapsAround() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let wt1 = Worktree( id: "/tmp/repo", name: "main", detail: "", workingDirectory: rootURL, repositoryRootURL: rootURL ) let wt2 = Worktree( id: "/tmp/repo/feature", name: "feature", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/feature"), repositoryRootURL: rootURL ) let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [wt1, wt2]) ) var state = RepositoriesFeature.State(repositories: [repo]) state.repositoryRoots = [rootURL] state.repositoryOrderIDs = [repo.id] state.selection = .worktree(wt1.id) // Currently on the first book. state.openedWorktreeIDs = [wt1.id, wt2.id] let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.selectPreviousShelfBook) // Wrapping: previous-before-first lands on the last book. await store.receive(\.selectWorktree) { $0.selection = .worktree(wt2.id) $0.sidebarSelectedWorktreeIDs = [wt2.id] $0.pendingTerminalFocusWorktreeIDs = [wt2.id] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func selectNextWorktreeRoutesToTabNavigationInShelf() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let wt1 = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [wt1]) ) var state = RepositoriesFeature.State(repositories: [repo]) state.selection = .worktree(wt1.id) state.isShelfActive = true let sentCommands = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore(initialState: state) { RepositoriesFeature() } withDependencies: { $0.terminalClient.send = { command in sentCommands.withValue { $0.append(command) } } }
await store.send(.selectNextWorktree) await store.finish() #expect(sentCommands.value == [.performBindingAction(wt1, action: "next_tab")]) }
@Test(.dependencies) func selectPreviousWorktreeRoutesToTabNavigationInShelf() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let wt1 = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [wt1]) ) var state = RepositoriesFeature.State(repositories: [repo]) state.selection = .worktree(wt1.id) state.isShelfActive = true let sentCommands = LockIsolated<[TerminalClient.Command]>([]) let store = TestStore(initialState: state) { RepositoriesFeature() } withDependencies: { $0.terminalClient.send = { command in sentCommands.withValue { $0.append(command) } } }
await store.send(.selectPreviousWorktree) await store.finish() #expect(sentCommands.value == [.performBindingAction(wt1, action: "previous_tab")]) }
@Test func shelfSwipeGestureClassifierMapsAxesToShelfNavigation() { #expect( ShelfSwipeGestureClassifier.action(accumulatedDeltaX: -90, accumulatedDeltaY: 10) == .nextBook ) #expect( ShelfSwipeGestureClassifier.action(accumulatedDeltaX: 90, accumulatedDeltaY: 10) == .previousBook ) #expect( ShelfSwipeGestureClassifier.action(accumulatedDeltaX: 10, accumulatedDeltaY: -90) == .nextTab ) #expect( ShelfSwipeGestureClassifier.action(accumulatedDeltaX: 10, accumulatedDeltaY: 90) == .previousTab ) }
@Test func shelfSwipeGestureClassifierIgnoresSmallOrAmbiguousScrolls() { #expect(ShelfSwipeGestureClassifier.action(accumulatedDeltaX: -79, accumulatedDeltaY: 0) == nil) #expect(ShelfSwipeGestureClassifier.action(accumulatedDeltaX: 60, accumulatedDeltaY: 60) == nil) #expect(ShelfSwipeGestureClassifier.action(accumulatedDeltaX: 90, accumulatedDeltaY: 70) == nil) }
@Test(.dependencies) func worktreeHistoryIsUnavailableWhileShelfIsActive() async { let fixture = threeWorktreeFixture() var state = RepositoriesFeature.State(repositories: [fixture.repo]) state.selection = .worktree(fixture.worktrees[1].id) state.isShelfActive = true state.worktreeHistoryBackStack = [fixture.worktrees[0].id] state.worktreeHistoryForwardStack = [fixture.worktrees[2].id] #expect(!state.canNavigateWorktreeHistoryBackward) #expect(!state.canNavigateWorktreeHistoryForward) let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.worktreeHistoryBack) await store.send(.worktreeHistoryForward) await store.finish() }
@Test(.dependencies) func worktreeHistoryIsUnavailableWhileCanvasIsActive() async { let fixture = threeWorktreeFixture() var state = RepositoriesFeature.State(repositories: [fixture.repo]) state.selection = .canvas state.worktreeHistoryBackStack = [fixture.worktrees[0].id] state.worktreeHistoryForwardStack = [fixture.worktrees[2].id] #expect(!state.canNavigateWorktreeHistoryBackward) #expect(!state.canNavigateWorktreeHistoryForward) let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.worktreeHistoryBack) await store.send(.worktreeHistoryForward) await store.finish() }
@Test(.dependencies) func selectNextWorktreeOutsideShelfStillCyclesWorktrees() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let wt1 = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let wt2 = Worktree( id: "/tmp/repo/wt2", name: "wt2", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt2"), repositoryRootURL: rootURL ) let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [wt1, wt2]) ) var state = RepositoriesFeature.State(repositories: [repo]) state.selection = .worktree(wt1.id) // Shelf NOT active — existing worktree-cycling behavior must survive. state.isShelfActive = false let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.selectNextWorktree) await store.receive(\.selectWorktree) { $0.selection = .worktree(wt2.id) $0.sidebarSelectedWorktreeIDs = [wt2.id] $0.openedWorktreeIDs = [wt2.id] $0.worktreeHistoryBackStack = [wt1.id] $0.pendingTerminalFocusWorktreeIDs = [wt2.id] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func markWorktreeClosedRemovesFromOpenedSet() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let wt1 = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [wt1]) ) var state = RepositoriesFeature.State(repositories: [repo]) state.openedWorktreeIDs = [wt1.id] state.selection = nil // Not currently selected, no auto-next needed. let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.markWorktreeClosed(wt1.id)) { $0.openedWorktreeIDs = [] } await store.finish() }
@Test(.dependencies) func markWorktreeClosedAdvancesToNextBookWhenAvailable() async { let fixture = threeWorktreeFixture() var state = RepositoriesFeature.State(repositories: [fixture.repo]) state.repositoryRoots = [fixture.repo.rootURL] state.repositoryOrderIDs = [fixture.repo.id] state.selection = .worktree(fixture.worktrees[1].id) // Middle book open. state.isShelfActive = true state.openedWorktreeIDs = Set(fixture.worktrees.map(\.id)) let store = TestStore(initialState: state) { RepositoriesFeature() }
// Closing the middle book → replacement is the one AFTER it (wt3), // not the first book (wt1). The user stays close to where they // were on the shelf. let closingID = fixture.worktrees[1].id let nextID = fixture.worktrees[2].id await store.send(.markWorktreeClosed(closingID)) { $0.openedWorktreeIDs = [fixture.worktrees[0].id, nextID] } await store.receive(\.selectWorktree) { $0.selection = .worktree(nextID) $0.sidebarSelectedWorktreeIDs = [nextID] $0.pendingTerminalFocusWorktreeIDs = [nextID] $0.openedWorktreeIDs = [fixture.worktrees[0].id, nextID] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func markWorktreeClosedFallsBackToPreviousBookWhenClosingLast() async { let fixture = threeWorktreeFixture() var state = RepositoriesFeature.State(repositories: [fixture.repo]) state.repositoryRoots = [fixture.repo.rootURL] state.repositoryOrderIDs = [fixture.repo.id] state.selection = .worktree(fixture.worktrees[2].id) // Last book open. state.isShelfActive = true state.openedWorktreeIDs = Set(fixture.worktrees.map(\.id)) let store = TestStore(initialState: state) { RepositoriesFeature() }
// Closing the last book → no book after it, so the replacement is // the book BEFORE it (wt2). let closingID = fixture.worktrees[2].id let prevID = fixture.worktrees[1].id await store.send(.markWorktreeClosed(closingID)) { $0.openedWorktreeIDs = [fixture.worktrees[0].id, prevID] } await store.receive(\.selectWorktree) { $0.selection = .worktree(prevID) $0.sidebarSelectedWorktreeIDs = [prevID] $0.pendingTerminalFocusWorktreeIDs = [prevID] $0.openedWorktreeIDs = [fixture.worktrees[0].id, prevID] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func markWorktreeClosedRemovesPlainFolderBookFromOpenedSet() async { // Plain-folder books live in `openedWorktreeIDs` under their // `Repository.ID`. The Shelf "Close Folder" menu dispatches // `.markWorktreeClosed(book.id)` for this path, so the reducer must // handle a plain-folder ID the same way it handles a worktree ID. let rootURL = URL(fileURLWithPath: "/tmp/folder") let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "folder", kind: .plain, worktrees: [] ) var state = RepositoriesFeature.State(repositories: [repo]) state.repositoryRoots = [rootURL] state.repositoryOrderIDs = [repo.id] state.selection = .repository(repo.id) state.isShelfActive = true state.openedWorktreeIDs = [repo.id] let store = TestStore(initialState: state) { RepositoriesFeature() }
// Only book on the Shelf — closing it leaves the opened set empty // and produces no replacement selection, so the Shelf falls back to // the empty state. await store.send(.markWorktreeClosed(repo.id)) { $0.openedWorktreeIDs = [] } await store.finish() }
@Test(.dependencies) func markWorktreeClosedAdvancesToPlainFolderNeighbor() async { // Closing a worktree book whose neighbor is a plain folder must // route through the `.selectRepository` branch of // `shelfBookSelectionEffect`, not `.selectWorktree`. Covers the // plain-folder path of the replacement dispatch that the new // Shelf "Close Worktree/Folder" menu relies on. let gitRootURL = URL(fileURLWithPath: "/tmp/git") let worktree = Worktree( id: "/tmp/git", name: "main", detail: "", workingDirectory: gitRootURL, repositoryRootURL: gitRootURL ) let gitRepo = Repository( id: gitRootURL.path(percentEncoded: false), rootURL: gitRootURL, name: "git", worktrees: IdentifiedArray(uniqueElements: [worktree]) ) let plainRootURL = URL(fileURLWithPath: "/tmp/plain") let plainRepo = Repository( id: plainRootURL.path(percentEncoded: false), rootURL: plainRootURL, name: "plain", kind: .plain, worktrees: [] ) var state = RepositoriesFeature.State(repositories: [gitRepo, plainRepo]) state.repositoryRoots = [gitRootURL, plainRootURL] state.repositoryOrderIDs = [gitRepo.id, plainRepo.id] state.selection = .worktree(worktree.id) state.isShelfActive = true state.openedWorktreeIDs = [worktree.id, plainRepo.id] let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.markWorktreeClosed(worktree.id)) { $0.openedWorktreeIDs = [plainRepo.id] } await store.receive(\.selectRepository) { $0.selection = .repository(plainRepo.id) $0.sidebarSelectedWorktreeIDs = [] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
private struct ThreeWorktreeFixture { let repo: Repository let worktrees: [Worktree] }
private func threeWorktreeFixture() -> ThreeWorktreeFixture { let rootURL = URL(fileURLWithPath: "/tmp/repo") let worktrees = (1...3).map { index in Worktree( id: "/tmp/repo/wt\(index)", name: "wt\(index)", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt\(index)"), repositoryRootURL: rootURL ) } let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: worktrees) ) return ThreeWorktreeFixture(repo: repo, worktrees: worktrees) }
@Test(.dependencies) func markWorktreeClosedLeavesSelectionAloneInNormalView() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let wt1 = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let wt2 = Worktree( id: "/tmp/repo/wt2", name: "wt2", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt2"), repositoryRootURL: rootURL ) let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [wt1, wt2]) ) var state = RepositoriesFeature.State(repositories: [repo]) state.selection = .worktree(wt1.id) state.isShelfActive = false // Normal view. state.openedWorktreeIDs = [wt1.id, wt2.id] let store = TestStore(initialState: state) { RepositoriesFeature() }
// In normal view, removing from the opened set must not also steal // selection away from the user — they are actively on wt1. await store.send(.markWorktreeClosed(wt1.id)) { $0.openedWorktreeIDs = [wt2.id] } await store.finish() }
@Test(.dependencies) func markWorktreeOpenedAddsToOpenedSet() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let wt1 = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repo = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [wt1]) ) let store = TestStore(initialState: RepositoriesFeature.State(repositories: [repo])) { RepositoriesFeature() }
// Mirrors the AppFeature forwarding `terminalEvent(.tabCreated)` → // `.markWorktreeOpened`. Any tab creation (including restored // layouts) adds its worktree to the Shelf's visible book set. await store.send(.markWorktreeOpened(wt1.id)) { $0.openedWorktreeIDs = [wt1.id] } await store.finish() }
@Test(.dependencies) func selectArchivedWorktreesClearsShelfActiveFlag() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let worktree = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [worktree]) ) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) state.isShelfActive = true let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.selectArchivedWorktrees) { $0.isShelfActive = false $0.worktreeHistoryBackStack = [worktree.id] $0.selection = .archivedWorktrees $0.sidebarSelectedWorktreeIDs = [] $0.preArchivedWorktreeID = worktree.id } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func selectArchivedWorktreesTogglesBackToPreviousWorktree() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let worktree = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [worktree]) ) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .archivedWorktrees state.sidebarSelectedWorktreeIDs = [] state.preArchivedWorktreeID = worktree.id let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.selectArchivedWorktrees) { $0.selection = .worktree(worktree.id) $0.sidebarSelectedWorktreeIDs = [worktree.id] $0.openedWorktreeIDs = [worktree.id] $0.pendingTerminalFocusWorktreeIDs = [worktree.id] $0.preArchivedWorktreeID = nil } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func selectArchivedWorktreesStalePreviousIDClearsSelection() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let worktree = Worktree( id: "/tmp/repo/wt1", name: "wt1", detail: "", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt1"), repositoryRootURL: rootURL ) let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [worktree]) ) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .archivedWorktrees state.sidebarSelectedWorktreeIDs = [] // Point to a worktree ID that no longer exists in any repository. state.preArchivedWorktreeID = "/tmp/repo/wt-deleted" let store = TestStore(initialState: state) { RepositoriesFeature() }
await store.send(.selectArchivedWorktrees) { $0.selection = nil $0.sidebarSelectedWorktreeIDs = [] $0.preArchivedWorktreeID = nil } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish() }
@Test(.dependencies) func defaultViewShelfDefersDuringLayoutRestore() async { // When Layout Restore is active the snapshot-load hook must stay // quiet: Layout Restore clears selection and replays tabs, so // entering Shelf here would flash an empty open area and leave a // stray spine if the restored active book differs from // `lastFocusedWorktreeID`. The AppFeature hook on `.layoutRestored` // takes over once Layout Restore has settled. let repoRoot = "/tmp/default-shelf-restore-repo" let rootURL = URL(fileURLWithPath: repoRoot) let worktree = Worktree( id: "\(repoRoot)/main", name: "main", detail: "", workingDirectory: URL(fileURLWithPath: "\(repoRoot)/main"), repositoryRootURL: rootURL ) let repository = Repository( id: repoRoot, rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [worktree]) )
@Shared(.settingsFile) var settingsFile $settingsFile.withLock { var updated = $0.global updated.defaultViewMode = .shelf $0.global = updated } defer { $settingsFile.withLock { var updated = $0.global updated.defaultViewMode = .normal $0.global = updated } }
var initialState = RepositoriesFeature.State() initialState.lastFocusedWorktreeID = worktree.id initialState.shouldRestoreLastFocusedWorktree = true initialState.snapshotPersistencePhase = .restoring initialState.launchRestoreMode = .restoreLayout let store = TestStore(initialState: initialState) { RepositoriesFeature() }
await store.send(.repositorySnapshotLoaded([repository])) { $0.repositories = [repository] $0.repositoryRoots = [rootURL] $0.selection = .worktree(worktree.id) $0.shouldRestoreLastFocusedWorktree = false $0.isInitialLoadComplete = true } await store.receive(\.delegate.repositoriesChanged) await store.receive(\.delegate.selectedWorktreeChanged) // No `.toggleShelf` here — the Layout Restore path is responsible. await store.finish() }
@Test(.dependencies) func defaultViewCanvasDefersDuringLayoutRestore() async { // Mirrors the Shelf deferral: during Layout Restore the snapshot-load // hook stays quiet and the AppFeature `.layoutRestored` path takes over. let repoRoot = "/tmp/default-canvas-restore-repo" let rootURL = URL(fileURLWithPath: repoRoot) let worktree = Worktree( id: "\(repoRoot)/main", name: "main", detail: "", workingDirectory: URL(fileURLWithPath: "\(repoRoot)/main"), repositoryRootURL: rootURL ) let repository = Repository( id: repoRoot, rootURL: rootURL, name: "repo", worktrees: IdentifiedArray(uniqueElements: [worktree]) )
@Shared(.settingsFile) var settingsFile $settingsFile.withLock { var updated = $0.global updated.defaultViewMode = .canvas $0.global = updated } defer { $settingsFile.withLock { var updated = $0.global updated.defaultViewMode = .normal $0.global = updated } }
var initialState = RepositoriesFeature.State() initialState.lastFocusedWorktreeID = worktree.id initialState.shouldRestoreLastFocusedWorktree = true initialState.snapshotPersistencePhase = .restoring initialState.launchRestoreMode = .restoreLayout let store = TestStore(initialState: initialState) { RepositoriesFeature() }
await store.send(.repositorySnapshotLoaded([repository])) { $0.repositories = [repository] $0.repositoryRoots = [rootURL] $0.selection = .worktree(worktree.id) $0.shouldRestoreLastFocusedWorktree = false $0.isInitialLoadComplete = true } await store.receive(\.delegate.repositoriesChanged) await store.receive(\.delegate.selectedWorktreeChanged) // No `.toggleCanvas` here — the Layout Restore path is responsible. await store.finish() }
@Test func isShowingShelfRequiresAtLeastOneRepository() { let rootURL = URL(fileURLWithPath: "/tmp/repo") let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "repo", worktrees: [] )
// Active shelf with zero repositories falls back to Normal — covers the // zero-repo launch race where `isShelfActive` is flipped on (from the // repository snapshot) before the empty entries file reconciles repos // back to empty. var empty = RepositoriesFeature.State() empty.isShelfActive = true #expect(empty.repositories.isEmpty) #expect(empty.isShowingShelf == false)
// With a repository present, the active shelf renders. var withRepo = RepositoriesFeature.State(repositories: [repository]) withRepo.isShelfActive = true #expect(withRepo.isShowingShelf == true)
// Inactive shelf is never showing. withRepo.isShelfActive = false #expect(withRepo.isShowingShelf == false) }}