native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363import ComposableArchitectureimport DependenciesTestSupportimport Foundationimport ProwlCLISharedimport Sharingimport Testing
@testable import Prowl
@MainActorstruct AppFeatureAgentProfileTests { @Test(.dependencies) func launchSendsPlanAndDefersMemoryToTheLaunchedEvent() async throws { let worktree = makeWorktree() let repositories = makeRepositoriesState(worktree: worktree) let profile = AgentProfile(name: "Codex · Work", runtime: .codex, model: "gpt-5.4") let storage = SettingsTestStorage() let sent = LockIsolated<[TerminalClient.Command]>([])
let (store, repoSettings) = withDependencies { $0.settingsFileStorage = storage.storage } operation: { @Shared(.userGlobalSettings) var globalSettings $globalSettings.withLock { $0.agentProfiles = [profile] } @Shared(.userRepositorySettings(worktree.repositoryRootURL)) var repoSettings let store = TestStore( initialState: AppFeature.State( repositories: repositories, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } } return (store, $repoSettings) }
await store.send(.launchAgentProfile(profile.id)) await store.finish()
let expectedPlan = try AgentProfileLaunchPlanner.plan( for: profile, homeBaseDirectory: ProwlPaths.agentProfileHomesDirectory ) #expect(sent.value == [.launchAgentProfile(worktree, plan: expectedPlan)]) // Dispatch must not record launch memory: a launch that fails to create a // surface would otherwise shift Recommended (docs-ai 053/005). #expect(repoSettings.wrappedValue.lastLaunchedAgentProfileID == nil)
await store.send(.terminalEvent(.agentProfileLaunched(worktreeID: worktree.id, profileID: profile.id))) #expect(repoSettings.wrappedValue.lastLaunchedAgentProfileID == profile.id) }
@Test(.dependencies) func launchFailedEventShowsWarningToastWithoutRecordingMemory() async { let worktree = makeWorktree() let repositories = makeRepositoriesState(worktree: worktree) let storage = SettingsTestStorage()
let (store, repoSettings) = withDependencies { $0.settingsFileStorage = storage.storage } operation: { @Shared(.userRepositorySettings(worktree.repositoryRootURL)) var repoSettings let store = TestStore( initialState: AppFeature.State( repositories: repositories, settings: SettingsFeature.State() ) ) { AppFeature() } return (store, $repoSettings) } // The toast auto-dismiss effect sleeps a real clock; non-exhaustive mode // lets the test end without draining it. store.exhaustivity = .off
await store.send( .terminalEvent(.agentProfileLaunchFailed(worktreeID: worktree.id, profileName: "Broken")) ) await store.receive(\.repositories.showToast) #expect(store.state.repositories.statusToast == .warning("Couldn't launch “Broken”")) #expect(repoSettings.wrappedValue.lastLaunchedAgentProfileID == nil) }
@Test(.dependencies) func degradedHookEventShowsOneNonBlockingWarningToast() async { let worktree = makeWorktree() let repositories = makeRepositoriesState(worktree: worktree) let storage = SettingsTestStorage() let store = withDependencies { $0.settingsFileStorage = storage.storage } operation: { TestStore( initialState: AppFeature.State( repositories: repositories, settings: SettingsFeature.State() ) ) { AppFeature() } } store.exhaustivity = .off
await store.send( .terminalEvent( .agentProfileLaunchWarning( worktreeID: worktree.id, profileName: "Codex", message: "Notifier resolver unavailable." ) ) ) await store.receive(\.repositories.showToast) #expect( store.state.repositories.statusToast == .warning("“Codex” launched without managed signals. Notifier resolver unavailable.") ) }
@Test(.dependencies) func launchIgnoresDisabledOrUnknownProfiles() async { let worktree = makeWorktree() let repositories = makeRepositoriesState(worktree: worktree) let disabled = AgentProfile(name: "Disabled", isEnabled: false, runtime: .claude) let storage = SettingsTestStorage() let sent = LockIsolated<[TerminalClient.Command]>([])
let store = withDependencies { $0.settingsFileStorage = storage.storage } operation: { @Shared(.userGlobalSettings) var globalSettings $globalSettings.withLock { $0.agentProfiles = [disabled] } return TestStore( initialState: AppFeature.State( repositories: repositories, settings: SettingsFeature.State() ) ) { AppFeature() } withDependencies: { $0.terminalClient.send = { command in sent.withValue { $0.append(command) } } } }
await store.send(.launchAgentProfile(disabled.id)) await store.send(.launchAgentProfile(UUID())) #expect(sent.value.isEmpty) }
@Test(.dependencies) func seedingRunsOnceAndOnlyForInstalledRuntimes() { let storage = SettingsTestStorage() withDependencies { $0.settingsFileStorage = storage.storage } operation: { AgentProfileSeeder.seedIfNeeded { runtime in runtime == .codex }
@Shared(.userGlobalSettings) var settings #expect(settings.didSeedAgentProfiles) #expect(settings.agentProfiles.map(\.name) == ["Codex"]) #expect(settings.agentProfiles.first?.runtime == .codex) #expect(settings.agentProfiles.first?.bindsDedicatedHome == false)
// Deleting the seed must not respawn it on the next launch. $settings.withLock { $0.agentProfiles = [] } AgentProfileSeeder.seedIfNeeded { _ in true } #expect(settings.agentProfiles.isEmpty) } }
@Test(.dependencies) func paletteBuildsLaunchItemsWithRecommendedFirst() { let worktree = makeWorktree() let repositories = makeRepositoriesState(worktree: worktree) let storage = SettingsTestStorage() let localStorage = RepositoryLocalSettingsTestStorage() withDependencies { $0.settingsFileStorage = storage.storage $0.repositoryLocalSettingsStorage = localStorage.storage } operation: { let first = AgentProfile(name: "First", runtime: .codex) let second = AgentProfile(name: "Second", runtime: .claude, icon: "wand.and.stars") let disabled = AgentProfile(name: "Hidden", isEnabled: false, runtime: .claude) @Shared(.userGlobalSettings) var settings $settings.withLock { $0.agentProfiles = [first, second, disabled] } @Shared(.userRepositorySettings(worktree.repositoryRootURL)) var repoSettings $repoSettings.withLock { $0.defaultAgentProfileID = second.id }
let items = agentProfileLaunchItems(repositories, launchWarning: { _ in nil })
#expect(items.map(\.title) == ["Launch Agent: Second", "Launch Agent: First"]) #expect(items.first?.kind == .launchAgentProfile(second.id)) #expect(items.first?.subtitle?.hasPrefix("Recommended") == true) #expect(items.last?.subtitle?.hasPrefix("Recommended") == false) #expect(items.first?.agentProfileIconSource == second.iconSource) #expect(items.last?.agentProfileIconSource == first.iconSource) } }
@Test(.dependencies) func paletteAnnotatesButNeverBlocksUnavailableRuntimes() { let worktree = makeWorktree() let repositories = makeRepositoriesState(worktree: worktree) let storage = SettingsTestStorage() let localStorage = RepositoryLocalSettingsTestStorage() withDependencies { $0.settingsFileStorage = storage.storage $0.repositoryLocalSettingsStorage = localStorage.storage } operation: { let profile = AgentProfile(name: "Codex", runtime: .codex) @Shared(.userGlobalSettings) var settings $settings.withLock { $0.agentProfiles = [profile] }
let items = agentProfileLaunchItems(repositories, launchWarning: { _ in "Codex may not be installed" })
// The soft heuristic surfaces as a subtitle warning; the row stays a // normal, activatable launch item (docs-ai 053/005). #expect(items.map(\.title) == ["Launch Agent: Codex"]) #expect(items.first?.subtitle?.contains("may not be installed") == true) #expect(items.first?.kind == .launchAgentProfile(profile.id)) } }
@Test(.dependencies) func paletteBuildsLaunchItemsForFocusedCanvasCard() { let worktree = makeWorktree() var repositories = makeRepositoriesState(worktree: worktree) // Canvas: no selected terminal worktree, only the focused card passed as // the action target. repositories.selection = nil let storage = SettingsTestStorage() let localStorage = RepositoryLocalSettingsTestStorage() withDependencies { $0.settingsFileStorage = storage.storage $0.repositoryLocalSettingsStorage = localStorage.storage } operation: { let profile = AgentProfile(name: "Codex", runtime: .codex) @Shared(.userGlobalSettings) var settings $settings.withLock { $0.agentProfiles = [profile] }
#expect(agentProfileLaunchItems(repositories, launchWarning: { _ in nil }).isEmpty) let items = agentProfileLaunchItems( repositories, actionTargetWorktreeID: worktree.id, launchWarning: { _ in nil } ) #expect(items.map(\.title) == ["Launch Agent: Codex"]) #expect(items.first?.subtitle?.contains(worktree.name) == true) } }
@Test func actionTargetResolverPrefersSelectionThenExplicitTerminalTarget() { let worktree = makeWorktree() var repositories = makeRepositoriesState(worktree: worktree)
// Selection wins over any explicit target. #expect( repositories.actionTargetTerminalWorktree(explicitTargetID: "unknown")?.id == worktree.id )
// Without a selection, the explicit target resolves through the full // terminal-target path — including synthesized plain-folder worktrees. repositories.selection = nil #expect( repositories.actionTargetTerminalWorktree(explicitTargetID: worktree.id)?.id == worktree.id ) let rootURL = URL(fileURLWithPath: "/tmp/plain-folder") let plain = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "plain-folder", kind: .plain, worktrees: [] ) repositories.repositories.append(plain) #expect( repositories.actionTargetTerminalWorktree(explicitTargetID: plain.id)?.workingDirectory == rootURL ) #expect(repositories.actionTargetTerminalWorktree(explicitTargetID: nil) == nil) }
@Test(.dependencies) func paletteBuildsLaunchItemsForFocusedPlainFolderCanvasCard() { // A runnable plain folder's terminal target ID is its repository ID; the // factory must resolve it through the same synthesized-worktree path the // launch action uses. let rootURL = URL(fileURLWithPath: "/tmp/plain-folder") let plain = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: "plain-folder", kind: .plain, worktrees: [] ) var repositories = RepositoriesFeature.State() repositories.repositories = IdentifiedArray(uniqueElements: [plain]) repositories.selection = nil let storage = SettingsTestStorage() let localStorage = RepositoryLocalSettingsTestStorage() withDependencies { $0.settingsFileStorage = storage.storage $0.repositoryLocalSettingsStorage = localStorage.storage } operation: { let profile = AgentProfile(name: "Codex", runtime: .codex) @Shared(.userGlobalSettings) var settings $settings.withLock { $0.agentProfiles = [profile] }
let items = agentProfileLaunchItems( repositories, actionTargetWorktreeID: plain.id, launchWarning: { _ in nil } ) #expect(items.map(\.title) == ["Launch Agent: Codex"]) #expect(items.first?.subtitle?.contains("plain-folder") == true) } }
@Test func launchedSurfaceEntryShowsProfileName() { var entry = ActiveAgentEntry( id: UUID(), worktreeID: "/tmp/repo/wt-1", worktreeName: "wt-1", workingDirectory: nil, tabID: TerminalTabID(), paneTitle: "codex", surfaceID: UUID(), paneIndex: 1, iconLookupToken: "codex", agent: .codex, session: nil, rawState: .idle, displayState: .idle, lastChangedAt: Date() ) #expect(entry.displayName == "codex")
entry.launchProfileName = "Codex · Work" #expect(entry.displayName == "Codex · Work") }
private func makeWorktree() -> Worktree { Worktree( id: "/tmp/repo/wt-1", name: "wt-1", detail: "detail", workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt-1"), repositoryRootURL: URL(fileURLWithPath: "/tmp/repo") ) }
private func makeRepositoriesState(worktree: Worktree) -> RepositoriesFeature.State { let rootURL = worktree.repositoryRootURL let repository = Repository( id: rootURL.path(percentEncoded: false), rootURL: rootURL, name: rootURL.lastPathComponent, worktrees: IdentifiedArray(uniqueElements: [worktree]) ) var repositoriesState = RepositoriesFeature.State() repositoriesState.repositories = IdentifiedArray(uniqueElements: [repository]) repositoriesState.selection = .worktree(worktree.id) return repositoriesState }}