native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
24 kB · 792 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793import ComposableArchitectureimport DependenciesTestSupportimport Foundationimport IdentifiedCollectionsimport ProwlCLISharedimport Sharingimport Testing
@testable import supacode
@MainActorstruct AppFeatureCustomCommandTests { @Test(.dependencies) func shellScriptCommandCreatesTabWithInput() async { let worktree = makeWorktree() let sent = LockIsolated<[TerminalClient.Command]>([]) var state = AppFeature.State( repositories: makeRepositoriesState(worktree: worktree), settings: SettingsFeature.State() ) state.selectedCustomCommands = effective( UserCustomCommand( title: "Test", systemImage: "checkmark.circle", command: "swift test", execution: .shellScript, shortcut: nil, ) )
let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.runCustomCommand(state.selectedCustomCommands[0].id)) await store.finish()
#expect( sent.value == [ .createTabWithInput( worktree, input: "swift test", runSetupScriptIfNew: false, autoCloseOnSuccess: false, customCommandName: "Test", customCommandIcon: "checkmark.circle" ) ], ) }
@Test(.dependencies) func terminalInputCommandSendsRawCommandText() async { let worktree = makeWorktree() let sent = LockIsolated<[TerminalClient.Command]>([]) var state = AppFeature.State( repositories: makeRepositoriesState(worktree: worktree), settings: SettingsFeature.State() ) state.selectedCustomCommands = effective( UserCustomCommand( title: "Watch", systemImage: "terminal", command: "pnpm test --watch", execution: .terminalInput, shortcut: nil, ) )
let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.runCustomCommand(state.selectedCustomCommands[0].id)) await store.finish()
#expect( sent.value == [ .insertText(worktree, text: "pnpm test --watch") ], ) }
@Test(.dependencies) func splitCommandCreatesSplitWithInput() async { let worktree = makeWorktree() let sent = LockIsolated<[TerminalClient.Command]>([]) var state = AppFeature.State( repositories: makeRepositoriesState(worktree: worktree), settings: SettingsFeature.State() ) state.selectedCustomCommands = effective( UserCustomCommand( title: "Tail", systemImage: "doc.text", command: "tail -f logs", execution: .split, splitDirection: .down, shortcut: nil, ) )
let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.runCustomCommand(state.selectedCustomCommands[0].id)) await store.finish()
#expect( sent.value == [ .createSplitWithInput( worktree, direction: .down, input: "tail -f logs", autoCloseOnSuccess: false, customCommandName: "Tail", customCommandIcon: "doc.text" ) ], ) }
@Test(.dependencies) func closeOnSuccessFlagIsForwarded() async { let worktree = makeWorktree() let sent = LockIsolated<[TerminalClient.Command]>([]) var state = AppFeature.State( repositories: makeRepositoriesState(worktree: worktree), settings: SettingsFeature.State() ) state.selectedCustomCommands = effective( UserCustomCommand( title: "Build", systemImage: "hammer", command: "make build", execution: .shellScript, closeOnSuccess: true, shortcut: nil, ), UserCustomCommand( title: "Lint", systemImage: "checkmark", command: "make lint", execution: .split, splitDirection: .right, closeOnSuccess: true, shortcut: nil, ), )
let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.runCustomCommand(state.selectedCustomCommands[0].id)) await store.send(.runCustomCommand(state.selectedCustomCommands[1].id)) await store.finish()
#expect( sent.value == [ .createTabWithInput( worktree, input: "make build", runSetupScriptIfNew: false, autoCloseOnSuccess: true, customCommandName: "Build", customCommandIcon: "hammer" ), .createSplitWithInput( worktree, direction: .right, input: "make lint", autoCloseOnSuccess: true, customCommandName: "Lint", customCommandIcon: "checkmark" ), ], ) }
@Test func userCustomCommandDecodesWithoutNewFields() throws { let legacyJSON = """ { "id": "abc", "title": "Legacy", "systemImage": "terminal", "command": "echo hi", "execution": "shellScript" } """ let data = Data(legacyJSON.utf8) let decoded = try JSONDecoder().decode(UserCustomCommand.self, from: data) #expect(decoded.splitDirection == .right) #expect(decoded.closeOnSuccess == false) #expect(decoded.execution == .shellScript) #expect(decoded.isEnabled) }
@Test(.dependencies) func invalidCommandIndexDoesNothing() async { let worktree = makeWorktree() let sent = LockIsolated<[TerminalClient.Command]>([]) let state = AppFeature.State( repositories: makeRepositoriesState(worktree: worktree), settings: SettingsFeature.State() )
let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.runCustomCommand(.init(source: .repository, commandID: "missing"))) await store.finish()
#expect(sent.value.isEmpty) }
@Test(.dependencies) func globalCustomCommandExecutesForSelectedWorktree() async { let worktree = makeWorktree() let sent = LockIsolated<[TerminalClient.Command]>([]) var state = AppFeature.State( repositories: makeRepositoriesState(worktree: worktree), settings: SettingsFeature.State() ) let global = EffectiveCustomCommand( source: .global, command: UserCustomCommand( id: "global-build", title: "Build Everywhere", systemImage: "globe", command: "make build", execution: .shellScript, shortcut: nil ) ) state.selectedCustomCommands = [global]
let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.runCustomCommand(global.id)) await store.finish()
#expect( sent.value == [ .createTabWithInput( worktree, input: "make build", runSetupScriptIfNew: false, autoCloseOnSuccess: false, customCommandName: "Build Everywhere", customCommandIcon: "globe" ) ] ) }
@Test(.dependencies) func supportsCustomCommandBeyondLegacyThreeItemLimit() async { let worktree = makeWorktree() let sent = LockIsolated<[TerminalClient.Command]>([]) var state = AppFeature.State( repositories: makeRepositoriesState(worktree: worktree), settings: SettingsFeature.State() ) state.selectedCustomCommands = effective( UserCustomCommand( title: "One", systemImage: "1.circle", command: "echo one", execution: .shellScript, shortcut: nil, ), UserCustomCommand( title: "Two", systemImage: "2.circle", command: "echo two", execution: .shellScript, shortcut: nil, ), UserCustomCommand( title: "Three", systemImage: "3.circle", command: "echo three", execution: .shellScript, shortcut: nil, ), UserCustomCommand( title: "Four", systemImage: "4.circle", command: "echo four", execution: .shellScript, shortcut: nil, ), UserCustomCommand( title: "Five", systemImage: "5.circle", command: "echo five", execution: .shellScript, shortcut: nil, ), )
let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.runCustomCommand(state.selectedCustomCommands[4].id)) await store.finish()
#expect( sent.value == [ .createTabWithInput( worktree, input: "echo five", runSetupScriptIfNew: false, autoCloseOnSuccess: false, customCommandName: "Five", customCommandIcon: "5.circle" ) ], ) }
@Test(.dependencies) func defaultTerminalIconIsTreatedAsUnsetForAutoDetection() async { let worktree = makeWorktree() let sent = LockIsolated<[TerminalClient.Command]>([]) var state = AppFeature.State( repositories: makeRepositoriesState(worktree: worktree), settings: SettingsFeature.State() ) state.selectedCustomCommands = effective( UserCustomCommand( title: "Default", systemImage: "terminal", command: "npm test", execution: .shellScript, shortcut: nil, ) )
let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.runCustomCommand(state.selectedCustomCommands[0].id)) await store.finish()
// The model's "terminal" placeholder should not be pinned as a // script icon — the auto-detector should remain free to brand the // tab from the executed command itself. #expect( sent.value == [ .createTabWithInput( worktree, input: "npm test", runSetupScriptIfNew: false, autoCloseOnSuccess: false, customCommandName: "Default", customCommandIcon: nil ) ], ) }
@Test(.dependencies) func emptyIconIsTreatedAsUnsetForAutoDetection() async { let worktree = makeWorktree() let sent = LockIsolated<[TerminalClient.Command]>([]) var state = AppFeature.State( repositories: makeRepositoriesState(worktree: worktree), settings: SettingsFeature.State() ) state.selectedCustomCommands = effective( UserCustomCommand( title: "Blank", systemImage: " ", command: "swift test", execution: .shellScript, shortcut: nil, ) )
let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.runCustomCommand(state.selectedCustomCommands[0].id)) await store.finish()
#expect( sent.value == [ .createTabWithInput( worktree, input: "swift test", runSetupScriptIfNew: false, autoCloseOnSuccess: false, customCommandName: "Blank", customCommandIcon: nil ) ], ) }
@Test(.dependencies) func loadingUserSettingsKeepsCustomCommandsWithoutScript() async { let worktree = makeWorktree() let settings = UserRepositorySettings( customCommands: [ UserCustomCommand( title: "Empty", systemImage: "sparkles", command: "", execution: .shellScript, shortcut: nil ), UserCustomCommand( title: "Runnable", systemImage: "terminal", command: "echo hello", execution: .shellScript, shortcut: nil ), ] )
let store = TestStore( initialState: AppFeature.State( repositories: makeRepositoriesState(worktree: worktree), settings: SettingsFeature.State() ) ) { AppFeature() }
await store.send(.worktreeUserSettingsLoaded(settings, worktreeID: worktree.id)) { $0.selectedCustomCommands = effective(settings.customCommands) $0.resolvedKeybindings = KeybindingResolver.resolve( schema: .appResolverSchema(effectiveCustomCommands: effective(settings.customCommands)), migratedOverrides: LegacyCustomCommandShortcutMigration .migrate(commands: effective(settings.customCommands)) .overrides ) } }
@Test(.dependencies) func loadingUserSettingsExcludesDisabledCommandsFromDispatch() async { let worktree = makeWorktree() let localEnabled = UserCustomCommand( id: "local-enabled", title: "Build", systemImage: "hammer", command: "make build", execution: .shellScript, shortcut: nil ) let localDisabled = UserCustomCommand( id: "local-disabled", title: "Test", systemImage: "checkmark", command: "make test", execution: .shellScript, shortcut: nil, isEnabled: false ) let globalEnabled = UserCustomCommand( id: "global-enabled", title: "Lint", systemImage: "checkmark.circle", command: "make lint", execution: .shellScript, shortcut: nil ) let globalDisabled = UserCustomCommand( id: "global-disabled", title: "Format", systemImage: "paintbrush", command: "make format", execution: .shellScript, shortcut: nil, isEnabled: false ) let globalOptedOut = UserCustomCommand( id: "global-opted-out", title: "Deploy", systemImage: "paperplane", command: "make deploy", execution: .shellScript, shortcut: nil ) let settings = UserRepositorySettings( customCommands: [localEnabled, localDisabled], disabledGlobalCommandIDs: ["global-opted-out"] ) let expectedCommands = [ EffectiveCustomCommand(source: .repository, command: localEnabled), EffectiveCustomCommand(source: .global, command: globalEnabled), ] let store = withDependencies { $0.defaultFileStorage = .inMemory } operation: { @Shared(.userGlobalSettings) var globalSettings $globalSettings.withLock { $0 = UserGlobalSettings(customCommands: [globalEnabled, globalDisabled, globalOptedOut]) } return TestStore( initialState: AppFeature.State( repositories: makeRepositoriesState(worktree: worktree), settings: SettingsFeature.State() ) ) { AppFeature() } }
await store.send(.worktreeUserSettingsLoaded(settings, worktreeID: worktree.id)) { $0.selectedCustomCommands = expectedCommands $0.resolvedKeybindings = KeybindingResolver.resolve( schema: .appResolverSchema(effectiveCustomCommands: expectedCommands), migratedOverrides: LegacyCustomCommandShortcutMigration.migrate(commands: expectedCommands).overrides ) }
await store.send(.runCustomCommand(.init(source: .repository, commandID: "local-disabled"))) await store.send(.runCustomCommand(.init(source: .global, commandID: "global-disabled"))) await store.send(.runCustomCommand(.init(source: .global, commandID: "global-opted-out"))) await store.finish() }
@Test(.dependencies) func customCommandUsesCanvasFocusedWorktree() async { let worktree = makeWorktree() let sent = LockIsolated<[TerminalClient.Command]>([]) var repositories = makeRepositoriesState(worktree: worktree) repositories.selection = .canvas var state = AppFeature.State( repositories: repositories, settings: SettingsFeature.State() ) state.selectedCustomCommands = effective( UserCustomCommand( title: "Canvas Build", systemImage: "hammer", command: "make build", execution: .shellScript, shortcut: nil ) )
let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.canvasFocusedWorktreeID = { worktree.id } $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } }
await store.send(.runCustomCommand(state.selectedCustomCommands[0].id)) await store.finish()
#expect( sent.value == [ .createTabWithInput( worktree, input: "make build", runSetupScriptIfNew: false, autoCloseOnSuccess: false, customCommandName: "Canvas Build", customCommandIcon: "hammer" ) ] ) }
@Test(.dependencies) func canvasFocusLoadsFocusedWorktreeCustomCommands() async { let worktree = makeWorktree() var repositories = makeRepositoriesState(worktree: worktree) repositories.selection = .canvas let settings = UserRepositorySettings( customCommands: [ UserCustomCommand( title: "Canvas Test", systemImage: "checkmark.circle", command: "swift test", execution: .terminalInput, shortcut: nil ) ] ) let store = withDependencies { $0.defaultFileStorage = .inMemory } operation: { @Shared(.userRepositorySettings(worktree.repositoryRootURL)) var userRepositorySettings $userRepositorySettings.withLock { $0 = settings } return TestStore( initialState: AppFeature.State( repositories: repositories, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.canvasFocusedWorktreeID = { worktree.id } } }
// Canvas focus applies both repository and user settings in a single reduce // pass so the toolbar updates atomically with the focus change. await store.send(.canvasFocusedWorktreeChanged(worktree.id)) { $0.openActionSelection = expectedDefaultOpenAction() $0.selectedCustomCommands = effective(settings.customCommands) $0.resolvedKeybindings = KeybindingResolver.resolve( schema: .appResolverSchema(effectiveCustomCommands: effective(settings.customCommands)), migratedOverrides: LegacyCustomCommandShortcutMigration .migrate(commands: effective(settings.customCommands)) .overrides ) } await store.finish() }
@Test(.dependencies) func canvasFocusWithMultipleReposUsesPrimaryFocusedWorktree() async { let worktreeA = makeWorktree(id: "/tmp/repo-a/wt-a", name: "wt-a", repoRoot: "/tmp/repo-a") let worktreeB = makeWorktree(id: "/tmp/repo-b/wt-b", name: "wt-b", repoRoot: "/tmp/repo-b") let commandA = UserCustomCommand( title: "Repo A Build", systemImage: "a.circle", command: "make repo-a", execution: .shellScript, shortcut: nil ) let commandB = UserCustomCommand( title: "Repo B Build", systemImage: "b.circle", command: "make repo-b", execution: .shellScript, shortcut: nil ) let sent = LockIsolated<[TerminalClient.Command]>([]) let store = withDependencies { $0.defaultFileStorage = .inMemory } operation: { @Shared(.repositorySettings(worktreeA.repositoryRootURL)) var repositorySettingsA @Shared(.repositorySettings(worktreeB.repositoryRootURL)) var repositorySettingsB @Shared(.userRepositorySettings(worktreeA.repositoryRootURL)) var userRepositorySettingsA @Shared(.userRepositorySettings(worktreeB.repositoryRootURL)) var userRepositorySettingsB $repositorySettingsA.withLock { $0.runScript = "npm run repo-a" } $repositorySettingsB.withLock { $0.runScript = "npm run repo-b" } $userRepositorySettingsA.withLock { $0.customCommands = [commandA] } $userRepositorySettingsB.withLock { $0.customCommands = [commandB] }
var repositories = makeRepositoriesState(worktrees: [worktreeA, worktreeB]) repositories.selection = .canvas var state = AppFeature.State( repositories: repositories, settings: SettingsFeature.State() ) state.selectedRunScript = "npm run repo-a" state.selectedCustomCommands = effective(commandA)
return TestStore(initialState: state) { AppFeature() } withDependencies: { $0.terminalClient.canvasFocusedWorktreeID = { worktreeB.id } $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } } }
await store.send(.canvasFocusedWorktreeChanged(worktreeB.id)) { $0.openActionSelection = expectedDefaultOpenAction() $0.selectedRunScript = "npm run repo-b" $0.selectedCustomCommands = effective(commandB) $0.resolvedKeybindings = KeybindingResolver.resolve( schema: .appResolverSchema(effectiveCustomCommands: effective(commandB)), migratedOverrides: LegacyCustomCommandShortcutMigration .migrate(commands: effective(commandB)) .overrides ) }
await store.send(.runScript) await store.send(.runCustomCommand(store.state.selectedCustomCommands[0].id)) await store.finish()
#expect( sent.value == [ .runScript(worktreeB, script: "npm run repo-b"), .createTabWithInput( worktreeB, input: "make repo-b", runSetupScriptIfNew: false, autoCloseOnSuccess: false, customCommandName: "Repo B Build", customCommandIcon: "b.circle" ), ] ) }
private func makeWorktree() -> Worktree { makeWorktree(id: "/tmp/repo/wt-1", name: "wt-1", repoRoot: "/tmp/repo") }
private func effective(_ commands: UserCustomCommand...) -> [EffectiveCustomCommand] { effective(commands) }
private func effective(_ commands: [UserCustomCommand]) -> [EffectiveCustomCommand] { commands.map { EffectiveCustomCommand(source: .repository, command: $0) } }
private func makeWorktree(id: String, name: String, repoRoot: String) -> Worktree { Worktree( id: id, name: name, detail: "detail", workingDirectory: URL(fileURLWithPath: id), repositoryRootURL: URL(fileURLWithPath: repoRoot) ) }
private func makeRepositoriesState(worktree: Worktree) -> RepositoriesFeature.State { makeRepositoriesState(worktrees: [worktree]) }
private func makeRepositoriesState(worktrees: [Worktree]) -> RepositoriesFeature.State { let worktreesByRepository = Dictionary( grouping: worktrees, by: { $0.repositoryRootURL.path(percentEncoded: false) } ) let repositories = worktreesByRepository.keys.sorted().compactMap { rootPath in worktreesByRepository[rootPath].map { worktrees in let rootURL = URL(fileURLWithPath: rootPath) return Repository( id: rootPath, rootURL: rootURL, name: rootURL.lastPathComponent, worktrees: IdentifiedArray(uniqueElements: worktrees) ) } } var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = IdentifiedArray(uniqueElements: repositories) repositoriesState.selection = worktrees.first.map { .worktree($0.id) } return repositoriesState }
private func expectedDefaultOpenAction() -> OpenWorktreeAction { OpenWorktreeAction.fromSettingsID( RepositorySettings.default.openActionID, defaultEditorID: OpenWorktreeAction.normalizedDefaultEditorID( SettingsFile.default.global.defaultEditorID ) ) }}