native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923import ComposableArchitectureimport CustomDumpimport Foundationimport IdentifiedCollectionsimport Testing
@testable import Prowl
@MainActorstruct CommandPaletteFeatureTests { @Test func commandPaletteItems_onlyGlobalWhenEmpty() { let items = CommandPaletteFeature.commandPaletteItems(from: RepositoriesFeature.State()) var expectedIDs = [ "global.check-for-updates", "global.open-settings", "global.open-repository", "global.new-workspace", "global.new-worktree", "global.refresh-worktrees", "global.jump-to-latest-unread", "global.view-archived-worktrees", "global.install-cli", "global.toggle-left-sidebar", "global.toggle-active-agents-panel", "global.toggle-canvas", "global.toggle-shelf", ] #if DEBUG expectedIDs.append(contentsOf: [ "debug.toast.inProgress", "debug.toast.success", "debug.update.simulate-found", "debug.dock.notification-dot", ]) #endif expectNoDifference(items.map(\.id), expectedIDs) }
@Test func installCommandLineToolSearchKeepsStableIDAcrossEnglishAndChineseTerms() { let items = CommandPaletteFeature.commandPaletteItems(from: RepositoriesFeature.State()) let expectedID = CommandPaletteItemID.globalInstallCLI
let englishResults = CommandPaletteFeature.filterItems( items: items, query: "Install Command Line Tool" ) let chineseResults = CommandPaletteFeature.filterItems(items: items, query: "命令行")
#expect(englishResults.contains { $0.id == expectedID }) #expect(chineseResults.contains { $0.id == expectedID }) }
@Test func revealInFinderSearchKeepsStableIDAcrossEnglishAndChineseTerms() { let rootPath = "/tmp/repo-reveal-search" let worktree = makeWorktree(id: rootPath, name: "repo", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) let items = CommandPaletteFeature.commandPaletteItems(from: state) let expectedID = CommandPaletteItemID.globalRevealInFinder
let englishResults = CommandPaletteFeature.filterItems(items: items, query: "Reveal in Finder") let chineseResults = CommandPaletteFeature.filterItems(items: items, query: "访达")
#expect(englishResults.contains { $0.id == expectedID }) #expect(chineseResults.contains { $0.id == expectedID }) }
@Test func commandPaletteItems_includesShowDiffWhenWorktreeSelected() { let rootPath = "/tmp/repo-diff" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state) #expect(items.contains { $0.id == "global.show-diff" }) }
@Test func commandPaletteItems_includesCanvasCommandsInCanvasMode() { var state = RepositoriesFeature.State() state.selection = .canvas
let ids = CommandPaletteFeature.commandPaletteItems(from: state).map(\.id) #expect(ids.contains("global.expand-canvas-card")) #expect(ids.contains("global.arrange-canvas-cards")) #expect(ids.contains("global.organize-canvas-cards")) #expect(ids.contains("global.select-all-canvas-cards")) }
@Test func commandPaletteItems_omitsCanvasCommandsOutsideCanvas() { let ids = CommandPaletteFeature.commandPaletteItems(from: RepositoriesFeature.State()).map(\.id) #expect(!ids.contains("global.expand-canvas-card")) #expect(!ids.contains("global.arrange-canvas-cards")) #expect(!ids.contains("global.organize-canvas-cards")) #expect(!ids.contains("global.select-all-canvas-cards")) }
@Test func commandPaletteItems_omitsShowDiffWithoutSelectedWorktree() { let items = CommandPaletteFeature.commandPaletteItems(from: RepositoriesFeature.State()) #expect(!items.contains { $0.id == "global.show-diff" }) }
@Test func commandPaletteItems_includesWorktreeNavigationWhenWorktreeSelected() { let rootPath = "/tmp/repo-nav" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state) let ids = Set(items.map(\.id)) #expect(ids.contains("global.reveal-in-finder")) #expect(ids.contains("global.copy-path")) #expect(ids.contains("global.reveal-in-sidebar")) }
@Test func commandPaletteItems_omitsWorktreeNavigationWithoutSelectedWorktree() { let items = CommandPaletteFeature.commandPaletteItems(from: RepositoriesFeature.State()) let ids = Set(items.map(\.id)) #expect(!ids.contains("global.reveal-in-finder")) #expect(!ids.contains("global.copy-path")) #expect(!ids.contains("global.reveal-in-sidebar")) }
@Test func commandPaletteItems_includesRunScriptWhenIdle() { let rootPath = "/tmp/repo-run" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state) let ids = Set(items.map(\.id)) #expect(ids.contains("global.run-script")) #expect(!ids.contains("global.stop-run-script")) }
@Test func commandPaletteItems_includesRunScriptForCanvasActionTarget() { let rootPath = "/tmp/repo-canvas-run" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .canvas
let items = CommandPaletteFeature.commandPaletteItems( from: state, actionTargetWorktreeID: worktree.id ) let ids = Set(items.map(\.id)) #expect(ids.contains("global.run-script")) #expect(!ids.contains("global.rename-branch")) #expect(!ids.contains("global.toggle-pin-worktree")) }
@Test func commandPaletteItems_includesStopScriptWhenRunning() { let rootPath = "/tmp/repo-run" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems( from: state, runScriptStatusByWorktreeID: [worktree.id: true] ) let ids = Set(items.map(\.id)) #expect(ids.contains("global.stop-run-script")) #expect(!ids.contains("global.run-script")) }
@Test func commandPaletteItems_includesPinForNonPinnedWorktree() { let rootPath = "/tmp/repo-pin" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state) let pinItem = items.first { $0.id == "global.toggle-pin-worktree" } #expect(pinItem?.kind == .togglePinWorktree(worktree.id, isCurrentlyPinned: false)) }
@Test func commandPaletteItems_includesUnpinForPinnedWorktree() { let rootPath = "/tmp/repo-pin" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) state.pinnedWorktreeIDs = [worktree.id]
let items = CommandPaletteFeature.commandPaletteItems(from: state) let pinItem = items.first { $0.id == "global.toggle-pin-worktree" } #expect(pinItem?.kind == .togglePinWorktree(worktree.id, isCurrentlyPinned: true)) }
@Test func commandPaletteItems_includesRenameBranchForNonMainWorktree() { let rootPath = "/tmp/repo-rename" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state) let renameItem = items.first { $0.id == "global.rename-branch" } #expect(renameItem?.kind == .renameBranch) #expect(renameItem?.appShortcutCommandID == AppShortcuts.CommandID.renameBranch) }
@Test func commandPaletteItems_omitsPinAndDeleteForMainWorktree() { let rootPath = "/tmp/repo-main" // Main worktree: workingDirectory == repositoryRootURL → Worktree.isMain == true let main = makeWorktree( id: rootPath, name: "main", repoRoot: rootPath, workingDirectory: rootPath ) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [main]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(main.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state) let ids = Set(items.map(\.id)) #expect(!ids.contains("global.toggle-pin-worktree")) #expect(!ids.contains("global.delete-worktree")) // Rename Branch is still available on the main worktree — `git branch -m` // works on the main branch the same as any other. #expect(ids.contains("global.rename-branch")) }
@Test func commandPaletteItems_includesDeleteWorktreeForNonMain() { let rootPath = "/tmp/repo-delete" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state) let deleteItem = items.first { $0.id == "global.delete-worktree" } #expect(deleteItem?.defaultSuggestion == false) }
@Test func commandPaletteItems_includesCustomCommands() { let buildCmd = UserCustomCommand( id: "cmd-build", title: "Build", systemImage: "hammer", command: "make build", execution: .shellScript, shortcut: nil ) let emptyCmd = UserCustomCommand( id: "cmd-empty", title: "Empty", systemImage: "terminal", command: " ", execution: .shellScript, shortcut: nil )
let items = CommandPaletteFeature.commandPaletteItems( from: RepositoriesFeature.State(), customCommands: [ EffectiveCustomCommand(source: .repository, command: buildCmd), EffectiveCustomCommand(source: .repository, command: emptyCmd), ] ) let ids = Set(items.map(\.id)) #expect(ids.contains("custom-command.cmd-build")) // Commands with empty body are filtered out. #expect(!ids.contains("custom-command.cmd-empty"))
let buildItem = items.first { $0.id == "custom-command.cmd-build" } #expect( matchesSupportedLocalization( buildItem?.subtitle, english: "Local custom command · Opens in a new tab", simplifiedChinese: "本地 自定义命令 · 在新标签页中打开" ) ) #expect(buildItem?.defaultSuggestion == false) #expect( buildItem?.kind == .runCustomCommand( .init(source: .repository, commandID: "cmd-build"), systemImage: "hammer" ) ) }
@Test func commandPaletteItems_includesRepoSettingsForSelectedWorktree() { let rootPath = "/tmp/repo-settings" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state) let item = items.first { $0.id == "repo.\(repository.id).open-settings" } #expect(item?.subtitle == "Repo") #expect(item?.kind == .openRepositorySettings(repository.id)) #expect(item?.category == .app) }
@Test func commandPaletteItems_includesRepoSettingsForSelectedRepository() { let rootPath = "/tmp/repo-settings-direct" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "RepoDirect", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .repository(repository.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state) #expect(items.contains { $0.id == "repo.\(repository.id).open-settings" }) }
@Test func commandPaletteItems_omitsRepoSettingsWithoutSelection() { let items = CommandPaletteFeature.commandPaletteItems(from: RepositoriesFeature.State()) #expect(!items.contains { $0.id.hasPrefix("repo.") }) }
@Test func commandPaletteItems_customCommandSubtitleVariants() { let shellCmd = UserCustomCommand( id: "cmd-shell", title: "Shell", systemImage: "terminal", command: "make", execution: .shellScript, shortcut: nil ) let inlineCmd = UserCustomCommand( id: "cmd-inline", title: "Inline", systemImage: "terminal", command: "ls", execution: .terminalInput, shortcut: nil ) let splitCmd = UserCustomCommand( id: "cmd-split", title: "Split", systemImage: "terminal", command: "watch", execution: .split, splitDirection: .down, closeOnSuccess: false, shortcut: nil )
let items = CommandPaletteFeature.commandPaletteItems( from: RepositoriesFeature.State(), customCommands: [shellCmd, inlineCmd, splitCmd].map { EffectiveCustomCommand(source: .repository, command: $0) } )
#expect( matchesSupportedLocalization( items.first { $0.id == "custom-command.cmd-shell" }?.subtitle, english: "Local custom command · Opens in a new tab", simplifiedChinese: "本地 自定义命令 · 在新标签页中打开" ) ) #expect( matchesSupportedLocalization( items.first { $0.id == "custom-command.cmd-inline" }?.subtitle, english: "Local custom command · Runs in the focused terminal", simplifiedChinese: "本地 自定义命令 · 在聚焦的终端中运行" ) ) #expect( matchesSupportedLocalization( items.first { $0.id == "custom-command.cmd-split" }?.subtitle, english: "Local custom command · Opens in a new split (down)", simplifiedChinese: "本地 自定义命令 · 在新的分屏中打开(下方)" ) ) }
@Test func commandPaletteItems_includeJumpToLatestUnreadAction() { let items = CommandPaletteFeature.commandPaletteItems(from: RepositoriesFeature.State()) let item = items.first { $0.id == "global.jump-to-latest-unread" }
#expect(item?.kind == .jumpToLatestUnread) #expect(item?.appShortcutCommandID == AppShortcuts.CommandID.jumpToLatestUnread)
let filtered = CommandPaletteFeature.filterItems(items: items, query: "jump unread") #expect(filtered.first?.id == "global.jump-to-latest-unread") }
@Test func commandPaletteItems_skipsPendingAndDeletingWorktrees() { let rootPath = "/tmp/repo" let keep = makeWorktree(id: "\(rootPath)/wt-keep", name: "keep", repoRoot: rootPath) let deleting = makeWorktree( id: "\(rootPath)/wt-delete", name: "delete", repoRoot: rootPath ) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [keep, deleting]) var state = RepositoriesFeature.State(repositories: [repository]) state.deletingWorktreeIDs = [deleting.id] state.pendingWorktrees = [ PendingWorktree( id: "\(rootPath)/wt-pending", repositoryID: repository.id, progress: WorktreeCreationProgress( stage: .creatingWorktree, worktreeName: "pending", baseRef: "origin/main", copyIgnored: false, copyUntracked: false ) ) ]
let items = CommandPaletteFeature.commandPaletteItems(from: state) let ids = items.map(\.id) #expect(ids.contains("worktree.\(keep.id).select")) #expect(ids.contains { $0.contains(deleting.id) } == false) #expect(ids.contains { $0.contains("wt-pending") } == false) }
@Test func commandPaletteItems_includeGhosttyCommandsWhenWorktreeSelected() { let rootPath = "/tmp/repo" let worktree = makeWorktree(id: rootPath, name: "repo", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems( from: state, ghosttyCommands: [ GhosttyCommand( title: "Focus Split Right", description: "Focus the split to the right.", action: "goto_split:right", actionKey: "goto_split" ) ] )
let ghosttyItem = items.first { if case .ghosttyCommand(let action) = $0.kind { return action == "goto_split:right" } return false }
#expect(ghosttyItem?.subtitle == "Focus the split to the right.") #expect(ghosttyItem?.id == "ghostty.goto_split:right|Focus Split Right") }
@Test func commandPaletteItems_includeGhosttyCommandsForCanvasActionTarget() { let rootPath = "/tmp/repo-canvas-new-tab" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "wt-1", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .canvas
let items = CommandPaletteFeature.commandPaletteItems( from: state, actionTargetWorktreeID: worktree.id, ghosttyCommands: [ GhosttyCommand( title: "New Tab", description: "Open a new tab.", action: "new_tab", actionKey: "new_tab" ) ] )
let ghosttyItem = items.first { if case .ghosttyCommand(let action) = $0.kind { return action == "new_tab" } return false }
#expect(ghosttyItem?.subtitle == "Open a new tab.") }
@Test func commandPaletteItems_filtersUnsupportedGhosttyCommands() { let rootPath = "/tmp/repo" let worktree = makeWorktree(id: rootPath, name: "repo", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems( from: state, ghosttyCommands: [ GhosttyCommand( title: "New Window", description: "Create a new window", action: "new_window", actionKey: "new_window" ), GhosttyCommand( title: "Next Window", description: "Go to next window", action: "goto_window:next", actionKey: "goto_window" ), GhosttyCommand( title: "Inspector", description: "Open inspector", action: "inspector", actionKey: "inspector" ), GhosttyCommand( title: "Focus Split Right", description: "Focus the split to the right.", action: "goto_split:right", actionKey: "goto_split" ), ] )
let ghosttyActions = items.compactMap { item -> String? in if case .ghosttyCommand(let action) = item.kind { return action } return nil }
#expect(ghosttyActions.contains("new_window") == false) #expect(ghosttyActions.contains("goto_window:next") == false) #expect(ghosttyActions.contains("inspector") == false) #expect(ghosttyActions.contains("goto_split:right")) }
@Test func commandPaletteItems_omitGhosttyCommandsWithoutSelectedWorktree() { let items = CommandPaletteFeature.commandPaletteItems( from: RepositoriesFeature.State(), ghosttyCommands: [ GhosttyCommand( title: "Focus Split Right", description: "", action: "goto_split:right", actionKey: "goto_split" ) ] )
#expect( items.contains { if case .ghosttyCommand = $0.kind { return true } return false } == false ) }
@Test func commandPaletteItems_omitsNewWorktreeForPlainFoldersOnly() { let repository = makeRepository( rootPath: "/tmp/folder", name: "Folder", kind: .plain, worktrees: [] ) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .repository(repository.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state)
#expect(items.contains(where: { $0.id == "global.new-worktree" }) == false) }
@Test func commandPaletteItems_showsCodeHostActionWithoutPullRequest() { let rootPath = "/tmp/repo" let worktree = makeWorktree(id: rootPath, name: "repo", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state) let openItem = items.first { if case .openRepositoryOnCodeHost(let worktreeID) = $0.kind { return worktreeID == worktree.id } return false }
#expect(openItem?.subtitle == repository.name)
let emptyQueryItems = CommandPaletteFeature.filterItems(items: items, query: "") #expect(emptyQueryItems.contains(where: { $0.id == openItem?.id }) == false)
let searchedItems = CommandPaletteFeature.filterItems(items: items, query: "code host") #expect(searchedItems.contains(where: { $0.id == openItem?.id }))
var githubState = state githubState.codeHostByRepositoryID[repository.id] = .github let githubItems = CommandPaletteFeature.commandPaletteItems(from: githubState) let githubOpenItem = githubItems.first { if case .openRepositoryOnCodeHost = $0.kind { return true } return false } }
@Test func commandPaletteItems_showsCodeHostActionForCanvasActionTarget() { let rootPath = "/tmp/repo-canvas-code-host" let worktree = makeWorktree(id: "\(rootPath)/wt-1", name: "feature/canvas", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .canvas state.codeHostByRepositoryID[repository.id] = .github state.worktreeInfoByID[worktree.id] = WorktreeInfoEntry( pullRequest: makePullRequest() )
let items = CommandPaletteFeature.commandPaletteItems( from: state, actionTargetWorktreeID: worktree.id ) let openItem = items.first { if case .openPullRequest(let worktreeID) = $0.kind { return worktreeID == worktree.id } return false }
#expect(openItem?.subtitle == "PR") }
@Test func emptyQueryHidesChangeFocusedTabIcon() { let rootPath = "/tmp/repo" let worktree = makeWorktree(id: rootPath, name: "repo", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id)
let items = CommandPaletteFeature.commandPaletteItems(from: state) let changeIconItem = items.first { if case .changeFocusedTabIcon = $0.kind { return true } return false } #expect(changeIconItem != nil)
let emptyQueryItems = CommandPaletteFeature.filterItems(items: items, query: "") #expect(emptyQueryItems.contains(where: { $0.id == changeIconItem?.id }) == false)
let searchedItems = CommandPaletteFeature.filterItems(items: items, query: "tab icon") #expect(searchedItems.contains(where: { $0.id == changeIconItem?.id })) }
@Test func keywordOnlyMatchSurfacesItem() { // "preferences" cannot fuzzy-match "Open Settings" (no p/r/f) so a match // only succeeds if keywords participate. let openSettings = makeItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings, keywords: ["preferences"] )
let result = CommandPaletteFeature.filterItems(items: [openSettings], query: "preferences") expectNoDifference(result.map(\.id), [openSettings.id]) }
@Test func keywordMatchRanksBelowDirectTitleMatch() { let openSettings = makeItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings, keywords: ["preferences"] ) let prefBranch = makeItem( id: "worktree.preferences.select", title: "preferences", subtitle: nil, kind: .worktreeSelect("wt-pref") )
let result = CommandPaletteFeature.filterItems( items: [openSettings, prefBranch], query: "preferences") #expect(result.first?.id == prefBranch.id) }
@Test func keywordMatchSurvivesMultiPieceQuery() { // "preferences" has to match via the keyword; "settings" matches the title. let openSettings = makeItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings, keywords: ["preferences"] )
let result = CommandPaletteFeature.filterItems( items: [openSettings], query: "preferences settings") expectNoDifference(result.map(\.id), [openSettings.id]) }
@Test func keywordMatchDoesNotIntroduceLabelHighlightsOutsideTitle() { let openSettings = makeItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings, keywords: ["preferences"] )
let result = CommandPaletteFeature.filterItems(items: [openSettings], query: "preferences") #expect(result.count == 1) // Sanity: the returned item is unchanged — no synthetic match positions leaked into the item. #expect(result.first?.title == openSettings.title) }
// MARK: - suggestions()
@Test func suggestionsEmptyInputReturnsEmptySections() { let result = CommandPaletteFeature.suggestions(items: [], recencyByID: [:], now: .now) #expect(result.recent.isEmpty) #expect(result.suggested.isEmpty) }
@Test func suggestionsOnlyDefaultSuggestionItemsAppearWhenNoRecency() { let suggested = makeItem( id: "s", title: "Suggested", subtitle: nil, kind: .openSettings ) let hidden = makeItem( id: "h", title: "Hidden", subtitle: nil, kind: .ghosttyCommand("focus_split") ) let suggestedWithFlag = CommandPaletteItem( id: suggested.id, title: suggested.title, subtitle: suggested.subtitle, kind: suggested.kind, category: suggested.category, defaultSuggestion: true )
let result = CommandPaletteFeature.suggestions( items: [suggestedWithFlag, hidden], recencyByID: [:], now: .now ) #expect(result.recent.isEmpty) expectNoDifference(result.suggested.map(\.id), [suggestedWithFlag.id]) }
@Test func suggestionsRecentIncludesNonSuggestionItemsWithRecency() { let now = Date(timeIntervalSince1970: 1_000_000) let worktree = makeItem( id: "worktree.fox.select", title: "Repo / fox", subtitle: nil, kind: .worktreeSelect("wt-fox") )
let result = CommandPaletteFeature.suggestions( items: [worktree], recencyByID: [worktree.id: now.timeIntervalSince1970 - 86_400], now: now ) expectNoDifference(result.recent.map(\.id), [worktree.id]) #expect(result.suggested.isEmpty) }
@Test func suggestionsRecentSortedByRecencyDesc() { let now = Date(timeIntervalSince1970: 1_000_000) let recent = makeItem(id: "recent", title: "Recent", subtitle: nil, kind: .openSettings) let older = makeItem(id: "older", title: "Older", subtitle: nil, kind: .openRepository)
let result = CommandPaletteFeature.suggestions( items: [older, recent], recencyByID: [ recent.id: now.timeIntervalSince1970 - 86_400, older.id: now.timeIntervalSince1970 - 10 * 86_400, ], now: now ) expectNoDifference(result.recent.map(\.id), [recent.id, older.id]) }
@Test func suggestionsDedupesRecentFromSuggested() { let now = Date(timeIntervalSince1970: 1_000_000) let appLevel = CommandPaletteItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings, category: .app, defaultSuggestion: true )
let result = CommandPaletteFeature.suggestions( items: [appLevel], recencyByID: [appLevel.id: now.timeIntervalSince1970 - 86_400], now: now ) expectNoDifference(result.recent.map(\.id), [appLevel.id]) #expect(result.suggested.isEmpty) }
@Test func suggestionsCapsTotalAt8() { let now = Date(timeIntervalSince1970: 1_000_000) let items = (0..<12).map { index in makeItem( id: "item-\(index)", title: "Item \(index)", subtitle: nil, kind: .worktreeSelect("wt-\(index)") ) } let recency = Dictionary( items.enumerated().map { idx, item in (item.id, now.timeIntervalSince1970 - TimeInterval(idx + 1) * 3600) }, uniquingKeysWith: { first, _ in first } )
let result = CommandPaletteFeature.suggestions(items: items, recencyByID: recency, now: now) #expect(result.recent.count + result.suggested.count == CommandPaletteSuggestions.maxItems) }
@Test func suggestionsSuggestedSortsByPriorityTierThenDeclarationOrder() { let lowPriority = CommandPaletteItem( id: "low", title: "Low", subtitle: nil, kind: .openSettings, category: .app, defaultSuggestion: true, priorityTier: 0 ) let defaultPriorityFirst = CommandPaletteItem( id: "default-first", title: "Default First", subtitle: nil, kind: .openSettings, category: .app, defaultSuggestion: true, priorityTier: CommandPaletteItem.defaultPriorityTier ) let defaultPrioritySecond = CommandPaletteItem( id: "default-second", title: "Default Second", subtitle: nil, kind: .openSettings, category: .app, defaultSuggestion: true, priorityTier: CommandPaletteItem.defaultPriorityTier )
let result = CommandPaletteFeature.suggestions( items: [defaultPriorityFirst, defaultPrioritySecond, lowPriority], recencyByID: [:], now: .now ) expectNoDifference( result.suggested.map(\.id), [lowPriority.id, defaultPriorityFirst.id, defaultPrioritySecond.id] ) }
@Test func filterItemsEmptyQueryHonorsDefaultSuggestionFlagOnly() { let suggested = CommandPaletteItem( id: "suggested", title: "Suggested", subtitle: nil, kind: .openSettings, category: .app, defaultSuggestion: true ) let hidden = CommandPaletteItem( id: "hidden", title: "Hidden", subtitle: nil, kind: .openSettings, category: .app, defaultSuggestion: false )
let result = CommandPaletteFeature.filterItems(items: [suggested, hidden], query: "") expectNoDifference(result.map(\.id), [suggested.id]) }
@Test func emptyQueryHidesGhosttyCommands() { let ghosttyItem = makeItem( id: "ghostty.goto_split:right|Focus Split Right", title: "Focus Split Right", subtitle: nil, kind: .ghosttyCommand("goto_split:right") ) let prAction = makeItem( id: "pr.open", title: "Open PR on GitHub", subtitle: "PR title", kind: .openPullRequest("wt-1"), priorityTier: 2 )
let result = CommandPaletteFeature.filterItems( items: [ghosttyItem, prAction], query: "" )
#expect(!result.contains { $0.id == ghosttyItem.id }) #expect(result.contains { $0.id == prAction.id }) }
@Test func commandPaletteItems_omitsSubActionsForMainWorktree() { let rootPath = "/tmp/repo" let main = makeWorktree( id: rootPath, name: "repo", detail: "main", repoRoot: rootPath, workingDirectory: rootPath ) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [main]) let items = CommandPaletteFeature.commandPaletteItems( from: RepositoriesFeature.State(repositories: [repository]) )
#expect( items.filter { if case .worktreeSelect = $0.kind { return true } return false }.count == 1 ) }
@Test func commandPaletteItems_trimsDetailToNil() { let rootPath = "/tmp/repo" let worktree = makeWorktree( id: "\(rootPath)/wt-detail", name: "detail", detail: " ", repoRoot: rootPath ) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) let items = CommandPaletteFeature.commandPaletteItems( from: RepositoriesFeature.State(repositories: [repository]) ) let selectItem = items.first { if case .worktreeSelect(let id) = $0.kind { return id == worktree.id } return false } #expect(selectItem?.subtitle == nil) }
@Test func commandPaletteItems_keepsFullWorktreeName() { let rootPath = "/tmp/repo" let worktree = makeWorktree( id: "\(rootPath)/wt-path", name: "khoi/cache", detail: "main", repoRoot: rootPath ) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) let items = CommandPaletteFeature.commandPaletteItems( from: RepositoriesFeature.State(repositories: [repository]) ) let selectItem = items.first { if case .worktreeSelect(let id) = $0.kind { return id == worktree.id } return false } }
@Test func commandPaletteItems_respectsRowOrderWithinRepository() { let rootPath = "/tmp/repo" let main = makeWorktree( id: rootPath, name: "repo", detail: "main", repoRoot: rootPath, workingDirectory: rootPath ) let pinned = makeWorktree( id: "\(rootPath)/wt-pinned", name: "pinned", detail: "pinned", repoRoot: rootPath ) let unpinned = makeWorktree( id: "\(rootPath)/wt-unpinned", name: "unpinned", detail: "unpinned", repoRoot: rootPath ) let repository = makeRepository( rootPath: rootPath, name: "Repo", worktrees: [ main, pinned, unpinned, ]) var state = RepositoriesFeature.State(repositories: [repository]) state.pinnedWorktreeIDs = [pinned.id] state.worktreeOrderByRepository = [repository.id: [unpinned.id]]
let items = CommandPaletteFeature.commandPaletteItems(from: state) let selectIDs = items.compactMap { item in if case .worktreeSelect(let id) = item.kind { return id } return nil } expectNoDifference(selectIDs, [main.id, pinned.id, unpinned.id]) }
@Test func commandPaletteItems_respectsRepositoryOrder() { let repoAPath = "/tmp/repo-a" let repoBPath = "/tmp/repo-b" let mainA = makeWorktree( id: repoAPath, name: "repo-a", detail: "main", repoRoot: repoAPath, workingDirectory: repoAPath ) let mainB = makeWorktree( id: repoBPath, name: "repo-b", detail: "main", repoRoot: repoBPath, workingDirectory: repoBPath ) let repoA = makeRepository(rootPath: repoAPath, name: "Repo A", worktrees: [mainA]) let repoB = makeRepository(rootPath: repoBPath, name: "Repo B", worktrees: [mainB]) var state = RepositoriesFeature.State(repositories: [repoA, repoB]) state.repositoryRoots = [repoB.rootURL, repoA.rootURL]
let items = CommandPaletteFeature.commandPaletteItems(from: state) let selectIDs = items.compactMap { item in if case .worktreeSelect(let id) = item.kind { return id } return nil } expectNoDifference(selectIDs, [mainB.id, mainA.id]) }
@Test func showsGlobalItemsWhenQueryEmpty() { let openSettings = makeItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings ) let newWorktree = makeItem( id: "global.new-worktree", title: "New Worktree", subtitle: nil, kind: .newWorktree ) let selectFox = makeItem( id: "worktree.fox.select", title: "Repo / fox", subtitle: "main", kind: .worktreeSelect("wt-fox") ) let deleteFox = makeItem( id: "worktree.fox.delete", title: "Delete Worktree", subtitle: "fox", kind: .deleteWorktree("wt-fox", "repo-fox") ) let changeFoxIcon = makeItem( id: "worktree.fox.change-icon", title: "Change Tab Icon...", subtitle: "fox", kind: .changeFocusedTabIcon("wt-fox") )
let result = CommandPaletteFeature.filterItems( items: [openSettings, newWorktree, selectFox, deleteFox, changeFoxIcon], query: "" ) expectNoDifference(result.map(\.id), [openSettings.id, newWorktree.id]) }
@Test func queryKeepsSelectionWhenEmpty() async { var state = CommandPaletteFeature.State() state.query = "fox" state.selectedIndex = 1 let store = TestStore(initialState: state) { CommandPaletteFeature() }
await store.send(.binding(.set(\.query, ""))) { $0.query = "" $0.selectedIndex = 1 } }
@Test func queryRanksByFuzzyScoreAcrossAllItems() { let openSettings = makeItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings ) let selectSettings = makeItem( id: "worktree.settings.select", title: "Repo / settings", subtitle: "main", kind: .worktreeSelect("wt-settings") )
expectNoDifference( CommandPaletteFeature.filterItems(items: [selectSettings, openSettings], query: "set"), [selectSettings, openSettings] ) }
@Test func fuzzyRanksPrefixAndShorterLabelFirst() { let short = makeItem( id: "worktree.set.select", title: "Set", subtitle: nil, kind: .worktreeSelect("wt-set") ) let long = makeItem( id: "worktree.settings.select", title: "Settings", subtitle: nil, kind: .worktreeSelect("wt-settings") )
expectNoDifference( CommandPaletteFeature.filterItems(items: [long, short], query: "set"), [short, long] ) }
@Test func fuzzyMatchesSubtitleWhenLabelDoesNot() { let item = makeItem( id: "worktree.fox.select", title: "Repo / fox", subtitle: "main", kind: .worktreeSelect("wt-fox") )
expectNoDifference( CommandPaletteFeature.filterItems(items: [item], query: "main"), [item] ) }
@Test func fuzzyMatchesMultiplePieces() { let item = makeItem( id: "worktree.fox.select", title: "Repo / fox", subtitle: "main", kind: .worktreeSelect("wt-fox") )
expectNoDifference( CommandPaletteFeature.filterItems(items: [item], query: "repo main"), [item] ) }
@Test func commandPaletteDraftActionRanksFirst() { let rootPath = "/tmp/repo" let worktree = makeWorktree(id: "\(rootPath)/wt-draft", name: "draft", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) state.worktreeInfoByID[worktree.id] = WorktreeInfoEntry( addedLines: nil, removedLines: nil, pullRequest: makePullRequest(isDraft: true) )
let items = CommandPaletteFeature.commandPaletteItems(from: state) let ordered = CommandPaletteFeature.filterItems(items: items, query: "")
#expect(!ordered.isEmpty, "Should generate command palette items") #expect( ordered.first?.kind == .markPullRequestReady(worktree.id), "Draft PR action should rank first" ) } @Test func commandPaletteFailingActionRanksFirst() { let rootPath = "/tmp/repo" let worktree = makeWorktree(id: "\(rootPath)/wt-failing", name: "failing", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) let failingCheck = GithubPullRequestStatusCheck( detailsUrl: "https://example.com/check/1", status: "COMPLETED", conclusion: "FAILURE", state: nil ) state.worktreeInfoByID[worktree.id] = WorktreeInfoEntry( addedLines: nil, removedLines: nil, pullRequest: makePullRequest(checks: [failingCheck]) )
let items = CommandPaletteFeature.commandPaletteItems(from: state) let ordered = CommandPaletteFeature.filterItems(items: items, query: "")
#expect(!ordered.isEmpty, "Should generate command palette items") #expect( ordered.first?.kind == .copyFailingJobURL(worktree.id), "Copy failing job URL action should rank first when details URL is available" ) } @Test func commandPaletteFailingActionFallsBackToLogsWhenCheckURLMissing() { let rootPath = "/tmp/repo" let worktree = makeWorktree(id: "\(rootPath)/wt-failing", name: "failing", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) let failingCheck = GithubPullRequestStatusCheck( status: "COMPLETED", conclusion: "FAILURE", state: nil ) state.worktreeInfoByID[worktree.id] = WorktreeInfoEntry( addedLines: nil, removedLines: nil, pullRequest: makePullRequest(checks: [failingCheck]) )
let items = CommandPaletteFeature.commandPaletteItems(from: state) let ordered = CommandPaletteFeature.filterItems(items: items, query: "")
#expect(!ordered.isEmpty, "Should generate command palette items") if case .copyCiFailureLogs(let wtID) = ordered.first?.kind { #expect( wtID == worktree.id, "CI failure logs action should rank first when check URL missing") } else { Issue.record( "Expected copyCiFailureLogs as first item, got \(String(describing: ordered.first?.kind))") } } @Test func commandPaletteMergeActionRanksFirstWhenMergeable() { let rootPath = "/tmp/repo" let worktree = makeWorktree(id: "\(rootPath)/wt-merge", name: "merge", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) state.worktreeInfoByID[worktree.id] = WorktreeInfoEntry( addedLines: nil, removedLines: nil, pullRequest: makePullRequest( mergeable: "MERGEABLE", mergeStateStatus: "CLEAN" ) )
let items = CommandPaletteFeature.commandPaletteItems(from: state) let ordered = CommandPaletteFeature.filterItems(items: items, query: "")
#expect(!ordered.isEmpty, "Should generate command palette items") if case .mergePullRequest(let wtID) = ordered.first?.kind { #expect(wtID == worktree.id, "Merge PR action should rank first when mergeable") } else { Issue.record( "Expected mergePullRequest as first item, got \(String(describing: ordered.first?.kind))") } } @Test func commandPaletteShowsCloseActionForOpenPullRequest() { let rootPath = "/tmp/repo" let worktree = makeWorktree(id: "\(rootPath)/wt-close", name: "close", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) state.worktreeInfoByID[worktree.id] = WorktreeInfoEntry( addedLines: nil, removedLines: nil, pullRequest: makePullRequest(state: "OPEN") )
let items = CommandPaletteFeature.commandPaletteItems(from: state) let closeItem = items.first(where: { if case .closePullRequest = $0.kind { return true } return false }) #expect(closeItem != nil) #expect(closeItem?.subtitle == "PR") if case .some(.closePullRequest(let closeWorktreeID)) = closeItem?.kind { #expect(closeWorktreeID == worktree.id) } else { Issue.record("Expected close pull request command palette action") } }
@Test func commandPaletteDoesNotShowCloseActionForMergedPullRequest() { let rootPath = "/tmp/repo" let worktree = makeWorktree(id: "\(rootPath)/wt-merged", name: "merged", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) state.worktreeInfoByID[worktree.id] = WorktreeInfoEntry( addedLines: nil, removedLines: nil, pullRequest: makePullRequest(state: "MERGED") )
let items = CommandPaletteFeature.commandPaletteItems(from: state) #expect( !items.contains(where: { if case .closePullRequest = $0.kind { return true } return false }) ) }
@Test func commandPaletteDoesNotShowMergeActionWhenBlocked() { let rootPath = "/tmp/repo" let worktree = makeWorktree(id: "\(rootPath)/wt-blocked", name: "blocked", repoRoot: rootPath) let repository = makeRepository(rootPath: rootPath, name: "Repo", worktrees: [worktree]) var state = RepositoriesFeature.State(repositories: [repository]) state.selection = .worktree(worktree.id) state.worktreeInfoByID[worktree.id] = WorktreeInfoEntry( addedLines: nil, removedLines: nil, pullRequest: makePullRequest( mergeable: "UNKNOWN", mergeStateStatus: "BLOCKED" ) )
let items = CommandPaletteFeature.commandPaletteItems(from: state) #expect( !items.contains(where: { if case .mergePullRequest = $0.kind { return true } return false }) ) }
@Test func recencyBreaksFuzzyTiesWithinGroup() { let now = Date(timeIntervalSince1970: 1_000_000) let recent = makeItem( id: "global.recent", title: "Open", subtitle: nil, kind: .openRepository ) let older = makeItem( id: "global.older", title: "Open", subtitle: nil, kind: .openSettings ) let recency: [CommandPaletteItem.ID: TimeInterval] = [ recent.id: now.timeIntervalSince1970 - 1 * 86_400, older.id: now.timeIntervalSince1970 - 10 * 86_400, ]
expectNoDifference( CommandPaletteFeature.filterItems( items: [older, recent], query: "open", recencyByID: recency, now: now ), [recent, older] ) }
@Test func prowlItemsBeatGhosttyItemsWhenScoresTie() { let prowlItem = makeItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings ) let ghosttyItem = makeItem( id: "ghostty.open-settings|Open Settings", title: "Open Settings", subtitle: nil, kind: .ghosttyCommand("open_settings"), priorityTier: CommandPaletteItem.defaultPriorityTier + 100 )
expectNoDifference( CommandPaletteFeature.filterItems( items: [ghosttyItem, prowlItem], query: "open settings" ), [prowlItem, ghosttyItem] ) }
// MARK: - Unified Ranking Tests
@Test func worktreeOutranksGlobalWhenBetterMatch() { let checkForUpdates = makeItem( id: "global.check-for-updates", title: "Check for Updates", subtitle: nil, kind: .checkForUpdates ) let worktreeFox = makeItem( id: "worktree.fox.select", title: "Repo / fox", subtitle: nil, kind: .worktreeSelect("wt-fox") )
expectNoDifference( CommandPaletteFeature.filterItems(items: [checkForUpdates, worktreeFox], query: "fox"), [worktreeFox] ) }
@Test func worktreeExactPrefixOutranksGlobalSubstringMatch() { let openSettings = makeItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings ) let worktreeOpen = makeItem( id: "worktree.open.select", title: "open", subtitle: nil, kind: .worktreeSelect("wt-open") )
let result = CommandPaletteFeature.filterItems( items: [openSettings, worktreeOpen], query: "open" ) #expect(result.first?.id == worktreeOpen.id) }
@Test func globalAndWorktreeItemsInterleavedByScore() { let openRepo = makeItem( id: "global.open-repository", title: "Open Repository", subtitle: nil, kind: .openRepository ) let worktreeRepo = makeItem( id: "worktree.repo.select", title: "repo", subtitle: nil, kind: .worktreeSelect("wt-repo") ) let refreshWorktrees = makeItem( id: "global.refresh-worktrees", title: "Refresh Worktrees", subtitle: nil, kind: .refreshWorktrees )
let result = CommandPaletteFeature.filterItems( items: [openRepo, worktreeRepo, refreshWorktrees], query: "repo" )
#expect(result.contains { $0.id == worktreeRepo.id }) #expect(result.contains { $0.id == openRepo.id }) #expect(!result.contains { $0.id == refreshWorktrees.id }) }
@Test func nonMatchingItemsExcludedRegardlessOfType() { let checkForUpdates = makeItem( id: "global.check-for-updates", title: "Check for Updates", subtitle: nil, kind: .checkForUpdates ) let worktreeFox = makeItem( id: "worktree.fox.select", title: "Repo / fox", subtitle: nil, kind: .worktreeSelect("wt-fox") )
expectNoDifference( CommandPaletteFeature.filterItems(items: [checkForUpdates, worktreeFox], query: "zzz"), [] ) }
@Test func multipleWorktreesCanAppearBeforeGlobalItems() { let openSettings = makeItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings ) let worktreeAlpha = makeItem( id: "worktree.alpha.select", title: "set", subtitle: nil, kind: .worktreeSelect("wt-alpha") ) let worktreeBeta = makeItem( id: "worktree.beta.select", title: "sett", subtitle: nil, kind: .worktreeSelect("wt-beta") )
let result = CommandPaletteFeature.filterItems( items: [openSettings, worktreeAlpha, worktreeBeta], query: "set" )
#expect(result.count == 3) #expect(result[0].id == worktreeAlpha.id) #expect(result[1].id == worktreeBeta.id) }
@Test func priorityTierBreaksTiesAcrossItemTypes() { let prAction = makeItem( id: "pr.merge", title: "Merge PR", subtitle: "Ready", kind: .mergePullRequest("wt-1"), priorityTier: 0 ) let worktreeMerge = makeItem( id: "worktree.merge.select", title: "Merge", subtitle: nil, kind: .worktreeSelect("wt-merge") )
let result = CommandPaletteFeature.filterItems( items: [worktreeMerge, prAction], query: "merge" )
#expect(result.count == 2) let prIndex = result.firstIndex { $0.id == prAction.id }! let wtIndex = result.firstIndex { $0.id == worktreeMerge.id }! #expect(wtIndex < prIndex) }
@Test func recencyBreaksTiesAcrossItemTypes() { let now = Date(timeIntervalSince1970: 1_000_000) let globalItem = makeItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings ) let worktreeItem = makeItem( id: "worktree.settings.select", title: "Repo / settings", subtitle: nil, kind: .worktreeSelect("wt-settings") ) let recency: [CommandPaletteItem.ID: TimeInterval] = [ worktreeItem.id: now.timeIntervalSince1970 - 1 * 86_400, globalItem.id: now.timeIntervalSince1970 - 20 * 86_400, ]
let result = CommandPaletteFeature.filterItems( items: [globalItem, worktreeItem], query: "settings", recencyByID: recency, now: now )
#expect(result.first?.id == worktreeItem.id) }
@Test func worktreeWithLabelMatchOutranksGlobalWithDescriptionMatch() { let globalItem = makeItem( id: "global.pr.open", title: "Open PR on GitHub", subtitle: "deploy-fixes", kind: .openPullRequest("wt-1") ) let worktreeItem = makeItem( id: "worktree.deploy.select", title: "Repo / deploy-fixes", subtitle: nil, kind: .worktreeSelect("wt-deploy") )
let result = CommandPaletteFeature.filterItems( items: [globalItem, worktreeItem], query: "deploy" )
#expect(result.first?.id == worktreeItem.id) }
@Test func shorterWorktreeLabelWinsOverLongerGlobalLabel() { let globalItem = makeItem( id: "global.new-worktree", title: "New Worktree", subtitle: nil, kind: .newWorktree ) let worktreeItem = makeItem( id: "worktree.new.select", title: "new", subtitle: nil, kind: .worktreeSelect("wt-new") )
let result = CommandPaletteFeature.filterItems( items: [globalItem, worktreeItem], query: "new" )
#expect(result.first?.id == worktreeItem.id) }
@Test func emptyQueryShowsSuggestionItemsAndHidesContextualOnes() { let checkForUpdates = makeItem( id: "global.check-for-updates", title: "Check for Updates", subtitle: nil, kind: .checkForUpdates ) let worktreeFox = makeItem( id: "worktree.fox.select", title: "Repo / fox", subtitle: nil, kind: .worktreeSelect("wt-fox") ) let prAction = makeItem( id: "pr.open", title: "Open PR on GitHub", subtitle: "PR title", kind: .openPullRequest("wt-1"), priorityTier: 2 )
let result = CommandPaletteFeature.filterItems( items: [checkForUpdates, worktreeFox, prAction], query: "" )
#expect(result.contains { $0.id == checkForUpdates.id }) #expect(!result.contains { $0.id == worktreeFox.id }) #expect(result.contains { $0.id == prAction.id }) }
@Test func whitespaceOnlyQueryTreatedAsEmpty() { let checkForUpdates = makeItem( id: "global.check-for-updates", title: "Check for Updates", subtitle: nil, kind: .checkForUpdates ) let worktreeFox = makeItem( id: "worktree.fox.select", title: "Repo / fox", subtitle: nil, kind: .worktreeSelect("wt-fox") )
let emptyResult = CommandPaletteFeature.filterItems( items: [checkForUpdates, worktreeFox], query: "" ) let whitespaceResult = CommandPaletteFeature.filterItems( items: [checkForUpdates, worktreeFox], query: " " )
expectNoDifference(emptyResult, whitespaceResult) }
@Test func inputOrderDoesNotAffectScoreBasedRanking() { let globalItem = makeItem( id: "global.open-settings", title: "Open Settings", subtitle: nil, kind: .openSettings ) let worktreeItem = makeItem( id: "worktree.open.select", title: "open", subtitle: nil, kind: .worktreeSelect("wt-open") )
let resultAB = CommandPaletteFeature.filterItems( items: [globalItem, worktreeItem], query: "open" ) let resultBA = CommandPaletteFeature.filterItems( items: [worktreeItem, globalItem], query: "open" )
#expect(resultAB.first?.id == resultBA.first?.id) }
@Test func activateDispatchesDelegateAndUpdatesRecency() async { var state = CommandPaletteFeature.State() state.isPresented = true state.query = "bear" state.selectedIndex = 1 let item = makeItem( id: "global.open-repository", title: "Open Repository", subtitle: nil, kind: .openRepository ) let store = TestStore(initialState: state) { CommandPaletteFeature() } let now = Date(timeIntervalSince1970: 1_234_567) store.dependencies.date = .constant(now)
await store.send(.activateItem(item)) { $0.isPresented = false $0.query = "" $0.selectedIndex = nil $0.recencyByItemID[item.id] = now.timeIntervalSince1970 } await store.receive(.delegate(.openRepository)) }
@Test func activateGhosttyCommandDispatchesDelegate() async { let now = Date(timeIntervalSince1970: 7_654_321) let item = makeItem( id: "ghostty.goto_split:right|Focus Split Right", title: "Focus Split Right", subtitle: nil, kind: .ghosttyCommand("goto_split:right") ) var state = CommandPaletteFeature.State() state.isPresented = true let store = TestStore(initialState: state) { CommandPaletteFeature() } store.dependencies.date = .constant(now)
await store.send(.activateItem(item)) { $0.isPresented = false $0.query = "" $0.selectedIndex = nil $0.recencyByItemID[item.id] = now.timeIntervalSince1970 } await store.receive(.delegate(.ghosttyCommand("goto_split:right"))) }
@Test func activateOutgoingChangesDispatchesDelegate() async { let now = Date(timeIntervalSince1970: 7_654_322) let item = makeItem( id: CommandPaletteItemID.globalOutgoingChanges, title: "Outgoing Changes", subtitle: nil, kind: .outgoingChanges ) var state = CommandPaletteFeature.State() state.isPresented = true let store = TestStore(initialState: state) { CommandPaletteFeature() } store.dependencies.date = .constant(now)
await store.send(.activateItem(item)) { $0.isPresented = false $0.query = "" $0.selectedIndex = nil $0.recencyByItemID[item.id] = now.timeIntervalSince1970 } await store.receive(.delegate(.showOutgoingChanges)) }}
private func matchesSupportedLocalization( _ value: String?, english: String, simplifiedChinese: String) -> Bool { value == english || value == simplifiedChinese}
private func makeWorktree( id: String, name: String, detail: String = "detail", repoRoot: String, workingDirectory: String? = nil) -> Worktree { Worktree( id: id, name: name, detail: detail, workingDirectory: URL(fileURLWithPath: workingDirectory ?? id), repositoryRootURL: URL(fileURLWithPath: repoRoot) )}
private func makeRepository( rootPath: String, name: String, kind: Repository.Kind = .git, worktrees: [Worktree]) -> Repository { let rootURL = URL(fileURLWithPath: rootPath) return Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: name, kind: kind, worktrees: IdentifiedArray(uniqueElements: worktrees) )}
private func makeItem( id: String, title: String, subtitle: String?, kind: CommandPaletteItem.Kind, keywords: [String] = [], priorityTier: Int = CommandPaletteItem.defaultPriorityTier) -> CommandPaletteItem { CommandPaletteItem( id: id, title: title, subtitle: subtitle, kind: kind, category: testCategory(for: kind), defaultSuggestion: testDefaultSuggestion(for: kind), keywords: keywords, priorityTier: priorityTier )}
private func testCategory(for kind: CommandPaletteItem.Kind) -> CommandPaletteItem.Category { switch kind { case .checkForUpdates, .openSettings, .openRepository, .newWorkspace, .installCLI, .openRepositorySettings, .editWorkspace: return .app case .newWorktree, .refreshWorktrees, .viewArchivedWorktrees, .changeFocusedTabIcon, .runScript, .stopRunScript, .togglePinWorktree, .renameBranch, .deleteWorktree, .runCustomCommand: return .worktree case .jumpToLatestUnread, .worktreeSelect, .revealInFinder, .copyPath, .revealInSidebar: return .navigation case .openPullRequest, .openRepositoryOnCodeHost, .markPullRequestReady, .mergePullRequest, .closePullRequest, .copyFailingJobURL, .copyCiFailureLogs, .rerunFailedJobs, .openFailingCheckDetails: return .pullRequest case .ghosttyCommand, .launchAgentProfile, .runWorkflow: return .terminal case .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, .expandCanvasCard, .arrangeCanvasCards, .organizeCanvasCards, .tileCanvasCards, .selectAllCanvasCards, .toggleShelf, .showDiff, .outgoingChanges: return .view #if DEBUG case .debugTestToast, .debugSimulateUpdateFound, .debugLightDockNotificationDot: return .debug #endif }}
private func testDefaultSuggestion(for kind: CommandPaletteItem.Kind) -> Bool { switch kind { case .checkForUpdates, .openSettings, .openRepository, .newWorkspace, .installCLI, .newWorktree, .refreshWorktrees, .viewArchivedWorktrees, .jumpToLatestUnread, .openPullRequest, .markPullRequestReady, .mergePullRequest, .closePullRequest, .copyFailingJobURL, .copyCiFailureLogs, .rerunFailedJobs, .openFailingCheckDetails, .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, .expandCanvasCard, .arrangeCanvasCards, .organizeCanvasCards, .tileCanvasCards, .selectAllCanvasCards, .toggleShelf, .showDiff, .outgoingChanges, .revealInFinder, .copyPath, .revealInSidebar, .runScript, .stopRunScript, .togglePinWorktree, .renameBranch, .openRepositorySettings, .editWorkspace: return true case .worktreeSelect, .changeFocusedTabIcon, .ghosttyCommand, .openRepositoryOnCodeHost, .deleteWorktree, .runCustomCommand, .launchAgentProfile, .runWorkflow: return false #if DEBUG case .debugTestToast, .debugSimulateUpdateFound, .debugLightDockNotificationDot: return true #endif }}
private func makePullRequest( state: String = "OPEN", isDraft: Bool = false, reviewDecision: String? = nil, mergeable: String? = nil, mergeStateStatus: String? = nil, checks: [GithubPullRequestStatusCheck] = []) -> GithubPullRequest { GithubPullRequest( number: 1, title: "PR", state: state, additions: 0, deletions: 0, isDraft: isDraft, reviewDecision: reviewDecision, mergeable: mergeable, mergeStateStatus: mergeStateStatus, updatedAt: nil, url: "https://example.com/pull/1", headRefName: "feature", baseRefName: "main", commitsCount: 1, authorLogin: "khoi", statusCheckRollup: checks.isEmpty ? nil : GithubPullRequestStatusCheckRollup(checks: checks) )}