native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481import ComposableArchitectureimport Dependenciesimport DependenciesTestSupportimport Foundationimport ProwlCLISharedimport Sharingimport Testing
@testable import Prowl
@MainActorstruct AppFeatureSettingsSelectionTests { @Test func selectingRepositoryCreatesRepositorySettingsState() async { let repository = Repository( id: "repo-id", rootURL: URL(fileURLWithPath: "/tmp/repo"), name: "Repo", worktrees: [] ) let store = TestStore( initialState: AppFeature.State( repositories: RepositoriesFeature.State(repositories: [repository]), settings: SettingsFeature.State() ) ) { AppFeature() }
await store.send(.settings(.setSelection(.repository(repository.id)))) { $0.settings.selection = .repository(repository.id) $0.settings.repositorySettings = RepositorySettingsFeature.State( rootURL: repository.rootURL, repositoryID: repository.id, repositoryKind: repository.kind, settings: .default, userSettings: .default ) } }
@Test func selectingMissingRepositoryClearsRepositorySettingsState() async { let store = TestStore( initialState: AppFeature.State( repositories: RepositoriesFeature.State(repositories: []), settings: SettingsFeature.State() ) ) { AppFeature() }
await store.send(.settings(.setSelection(.repository("missing")))) { $0.settings.selection = .repository("missing") $0.settings.repositorySettings = nil } }
@Test func selectingPlainRepositoryCreatesPlainRepositorySettingsState() async { let repository = Repository( id: "folder-id", rootURL: URL(fileURLWithPath: "/tmp/folder"), name: "Folder", kind: .plain, worktrees: [] ) let store = TestStore( initialState: AppFeature.State( repositories: RepositoriesFeature.State(repositories: [repository]), settings: SettingsFeature.State() ) ) { AppFeature() }
await store.send(.settings(.setSelection(.repository(repository.id)))) { $0.settings.selection = .repository(repository.id) $0.settings.repositorySettings = RepositorySettingsFeature.State( rootURL: repository.rootURL, repositoryID: repository.id, repositoryKind: .plain, settings: .default, userSettings: .default ) } }
@Test(.dependencies) func selectingRepositorySeedsAppearanceSynchronously() async { // Regression: selecting a repo whose appearance is already in // @Shared used to construct a State with `.empty` appearance and // load asynchronously via .task. The async hop raced with the // user's first click, sometimes wiping previously-saved fields. // The State must now carry the appearance from frame zero. let storage = SettingsTestStorage() let appearancesURL = URL(fileURLWithPath: "/tmp/appearances-\(UUID().uuidString).json") let savedAppearance = RepositoryAppearance( icon: .sfSymbol("hammer.fill"), color: .blue ) let repository = Repository( id: "appearance-repo", rootURL: URL(fileURLWithPath: "/tmp/appearance-repo"), name: "AppearanceRepo", worktrees: [] )
await withDependencies { $0.settingsFileStorage = storage.storage $0.repositoryAppearancesFileURL = appearancesURL } operation: { @Shared(.repositoryAppearances) var appearances $appearances.withLock { $0[repository.id] = savedAppearance }
let store = TestStore( initialState: AppFeature.State( repositories: RepositoriesFeature.State(repositories: [repository]), settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.settingsFileStorage = storage.storage $0.repositoryAppearancesFileURL = appearancesURL }
await store.send(.settings(.setSelection(.repository(repository.id)))) { $0.settings.selection = .repository(repository.id) $0.settings.repositorySettings = RepositorySettingsFeature.State( rootURL: repository.rootURL, repositoryID: repository.id, repositoryKind: repository.kind, settings: .default, userSettings: .default, appearance: savedAppearance ) } } }
@Test func selectingNonRepositoryClearsRepositorySettingsState() async { let repository = Repository( id: "repo-id", rootURL: URL(fileURLWithPath: "/tmp/repo"), name: "Repo", worktrees: [] ) var state = AppFeature.State( repositories: RepositoriesFeature.State(repositories: [repository]), settings: SettingsFeature.State() ) state.settings.selection = .repository(repository.id) state.settings.repositorySettings = RepositorySettingsFeature.State( rootURL: repository.rootURL, repositoryKind: repository.kind, settings: .default, userSettings: .default ) let store = TestStore(initialState: state) { AppFeature() }
await store.send(.settings(.setSelection(.general))) { $0.settings.selection = .general $0.settings.repositorySettings = nil } }
@Test func showingShortcutNavigatesThroughAppSettingsSelection() async { var state = AppFeature.State(settings: SettingsFeature.State()) state.settings.selection = .notifications let commandID = AppShortcuts.CommandID.toggleAgentIsland let store = TestStore(initialState: state) { AppFeature() }
await store.send(.settings(.showShortcutButtonTapped(commandID: commandID))) { $0.settings.shortcutNavigationTargetCommandID = commandID } await store.receive(\.settings.setSelection) { $0.settings.selection = .shortcuts } }
@Test(.dependencies) func islandSettingsOpensDisplayAndCollapsesRosterWithoutFocusingAPane() async { let shown = LockIsolated(false) let surfaced = LockIsolated(false) var state = AppFeature.State(settings: SettingsFeature.State()) state.repositories.activeAgents.isIslandRosterExpanded = true state.repositories.activeAgents.islandNavigation.selectedEntryID = UUID() let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.settingsWindowClient.show = { shown.setValue(true) } $0.appLifecycleClient.surfaceMainWindow = { surfaced.setValue(true) return true } }
await store.send(.repositories(.activeAgents(.islandSettingsTapped))) { $0.repositories.activeAgents.isIslandRosterExpanded = false $0.repositories.activeAgents.islandNavigation = .init() } await store.receive(\.settings.setSelection) { $0.settings.selection = .agentDisplay } await store.finish() #expect(shown.value) #expect(surfaced.value) }
@Test(.dependencies) func openWorkflowSettingsSelectsWorkflowsBeforeShowingWindow() async { let shown = LockIsolated(false) var state = AppFeature.State(settings: SettingsFeature.State()) state.settings.selection = .commandLineTool let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.settingsWindowClient.show = { shown.withValue { $0 = true } } } await store.send(.openWorkflowSettings) await store.receive(\.settings.setSelection) { $0.settings.selection = .workflows $0.settings.workflows = .init() } await store.finish() #expect(shown.value) }
@Test(.dependencies) func openAgentProfilesSettingsSelectsProfiles() async { let shown = LockIsolated(false) var state = AppFeature.State(settings: SettingsFeature.State()) state.settings.selection = .commandLineTool let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0.settingsWindowClient.show = { shown.withValue { $0 = true } } }
await store.send(.openAgentProfilesSettings) await store.receive(\.settings.setSelection) { $0.settings.selection = .profiles $0.settings.agentProfiles = .init() } await store.finish()
#expect(shown.value) }
@Test(.dependencies) func settingsWorkflowStartKeepsItsWindowAndReturnsToItsDetail() async throws { let root = URL(filePath: "/tmp/settings-run") let worktree = Worktree( id: "settings-run", name: "main", detail: "", workingDirectory: root, repositoryRootURL: root) let repository = Repository(id: "repo", rootURL: root, name: "Repo", worktrees: [worktree]) let definition = try #require(WorkflowDocumentParser.parse(WorkflowStartContextTests.worktreeOnly).definition) let context = WorkflowStartContext( item: WorkflowStartCatalogItem( key: "user/worktree-only", scope: .user, fileURL: root.appending(path: "flow.yaml"), workflowID: definition.id, name: definition.name, workflowDescription: nil, icon: nil, validationFailure: nil), definition: definition, worktreeID: worktree.id, worktreeName: worktree.name, source: nil, launchRoles: [], pickRoles: [], cliInstalled: true, bindModeOverride: nil) var state = AppFeature.State( repositories: RepositoriesFeature.State(repositories: [repository]), settings: SettingsFeature.State()) state.settings.selection = .workflows state.settings.workflows = .init() let surfaced = LockIsolated(false) let focused = LockIsolated(false) let store = TestStore(initialState: state) { AppFeature() } withDependencies: { $0[WorkflowStartClient.self].context = { _, _, _ in context } $0.appLifecycleClient.surfaceMainWindow = { surfaced.setValue(true) return true } $0.terminalClient.send = { _ in focused.setValue(true) } } store.exhaustivity = .off await store.send( .settings( .workflows( .delegate( .runWorkflow( workflowKey: context.item.key, worktreeID: worktree.id, forceSheet: true))))) #expect(store.state.workflowStart?.context == context) #expect(store.state.workflowStartFromSettings) #expect(!surfaced.value) await store.send(.workflowStart(.presented(.cancelTapped))) await store.receive(\.workflowStart.presented.delegate) await store.finish() #expect(store.state.workflowStart == nil) #expect(!store.state.workflowStartFromSettings) #expect(store.state.settings.selection == .workflows) #expect(!focused.value) }
@Test(.dependencies) func workflowDetailsDeepLinkLoadsAndPushesTheExactFile() async throws { let directory = FileManager.default.temporaryDirectory .appending(path: "workflow-deep-link-\(UUID().uuidString)", directoryHint: .isDirectory) defer { try? FileManager.default.removeItem(at: directory) } try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) let url = directory.appending(path: "review.yaml") let yaml = """ schema: prowl.workflow/v1 id: review name: Review icon: magnifyingglass roles: author: source: current steps: - id: ask message: author text: "Review." """ try Data(yaml.utf8).write(to: url) let file = WorkflowDiscovery.parse( yaml, url: url, scope: .user, context: WorkflowValidationContext(scope: .user)) let entry = WorkflowCatalogEntry(file: file, shadowed: false) let item = try #require( WorkflowStartCatalogItem.make( entry: entry, disabledWorkflowIDs: [], repositoryRootPath: nil)) let scan = WorkflowSettingsScan( bundleDirectory: nil, userDirectory: directory, entries: [entry], repositories: []) let shown = LockIsolated(false) let store = TestStore(initialState: AppFeature.State(settings: SettingsFeature.State())) { AppFeature() } withDependencies: { $0[WorkflowSettingsClient.self].scan = { _ in scan } $0.settingsWindowClient.show = { shown.withValue { $0 = true } } } store.exhaustivity = .off
await store.send(.openWorkflowDetails(item, worktreeID: "unused")) await store.receive(\.settings.setSelection) await store.receive(\.settings.workflows.task) await store.receive(\.settings.workflows.showDetails) await store.finish()
#expect(store.state.settings.selection == .workflows) #expect(store.state.settings.workflows?.path.first?.row?.url == url) #expect(shown.value) }
@Test(.dependencies) func repositoryWorkflowDetailsDeepLinkUsesRepositorySettings() async throws { let rootURL = URL(fileURLWithPath: "/tmp/repository-workflow-deep-link") let workflowURL = WorkflowSources.repoDirectory(root: rootURL).appending(path: "review.yaml") let yaml = """ schema: prowl.workflow/v1 id: review name: Repository Review roles: author: source: current steps: - id: ask message: author text: "Review." """ let file = WorkflowDiscovery.parse( yaml, url: workflowURL, scope: .repo, context: WorkflowValidationContext(scope: .repo)) let entry = WorkflowCatalogEntry(file: file, shadowed: false) let item = try #require( WorkflowStartCatalogItem.make( entry: entry, disabledWorkflowIDs: [], repositoryRootPath: rootURL.path(percentEncoded: false))) let worktree = Worktree( id: "wt-1", name: "feature", detail: "", workingDirectory: rootURL, repositoryRootURL: rootURL) let repository = Repository( id: "repo-id", rootURL: rootURL, name: "Repo", worktrees: [worktree]) let scan = WorkflowSettingsScan( bundleDirectory: nil, userDirectory: URL(filePath: "/tmp/user-workflows"), entries: [], repositories: [ WorkflowSettingsRepositoryScan( repositoryID: repository.id, name: repository.name, rootPath: rootURL.path(percentEncoded: false), directory: WorkflowSources.repoDirectory(root: rootURL), entries: [entry]) ]) let shown = LockIsolated(false) let store = TestStore( initialState: AppFeature.State( repositories: RepositoriesFeature.State(repositories: [repository]), settings: SettingsFeature.State()) ) { AppFeature() } withDependencies: { $0[WorkflowSettingsClient.self].scan = { _ in scan } $0.settingsWindowClient.show = { shown.withValue { $0 = true } } } store.exhaustivity = .off
await store.send(.openWorkflowDetails(item, worktreeID: worktree.id)) await store.receive(\.settings.setSelection) await store.receive(\.settings.repositorySettings.workflowsAppeared) await store.receive(\.settings.repositorySettings.workflows.task) await store.receive(\.settings.repositorySettings.workflows.showDetails) await store.finish()
#expect(store.state.settings.selection == .repository(repository.id)) #expect(store.state.settings.repositorySettings?.workflows.path.first?.row?.url == workflowURL) #expect(shown.value) }
@Test(arguments: [SettingsSection.general, .agentDisplay, .commandLineTool]) func selectingAnotherSectionClearsAgentProfileEditorState(section: SettingsSection) async { let profile = AgentProfile(name: "Codex", runtime: .codex) var state = AppFeature.State(settings: SettingsFeature.State()) state.settings.selection = .profiles var agentProfiles = AgentProfilesFeature.State() agentProfiles.settings = UserGlobalSettings(customCommands: [], agentProfiles: [profile]) agentProfiles.path.append(AgentProfileEditorFeature.State(profile: profile)) state.settings.agentProfiles = agentProfiles let store = TestStore(initialState: state) { AppFeature() }
await store.send(.settings(.setSelection(section))) { $0.settings.selection = section $0.settings.agentProfiles = nil if section == .commandLineTool { $0.settings.agentSkills = .init() } } }
@Test func selectingCommandLineToolInitialisesAgentSkillsState() async { let store = TestStore(initialState: AppFeature.State(settings: SettingsFeature.State())) { AppFeature() }
await store.send(.settings(.setSelection(.commandLineTool))) { $0.settings.selection = .commandLineTool $0.settings.agentSkills = .init() } }
@Test(arguments: [SettingsSection.general, .agentDisplay, .profiles]) func selectingAnotherSectionClearsAgentSkillsState(section: SettingsSection) async { var state = AppFeature.State(settings: SettingsFeature.State()) state.settings.selection = .commandLineTool state.settings.agentSkills = .init() let store = TestStore(initialState: state) { AppFeature() }
await store.send(.settings(.setSelection(section))) { $0.settings.selection = section $0.settings.agentSkills = nil if section == .profiles { $0.settings.agentProfiles = .init() } } }}