native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337import ComposableArchitectureimport DependenciesTestSupportimport Foundationimport Sharingimport Testing
@testable import Prowl
@MainActorstruct AgentProfilesFeatureTests { @Test(.dependencies) func taskLoadsSettingsWithoutPushingAnEditor() async { let profile = AgentProfile(name: "Codex", runtime: .codex) let storage = SettingsTestStorage() let store = withDependencies { $0.settingsFileStorage = storage.storage } operation: { @Shared(.userGlobalSettings) var settings $settings.withLock { $0.agentProfiles = [profile] } return TestStore(initialState: AgentProfilesFeature.State()) { AgentProfilesFeature() } }
// The root page is the list; a non-empty path would push the drill-in. await store.send(.task) await store.receive(\.settingsLoaded) { $0.settings.agentProfiles = [profile] } #expect(store.state.path.isEmpty) }
@Test(.dependencies) func addProfileAppendsOpensEditorAndPersists() async { let storage = SettingsTestStorage() let store = withDependencies { $0.settingsFileStorage = storage.storage } operation: { TestStore(initialState: AgentProfilesFeature.State()) { AgentProfilesFeature() } withDependencies: { $0.uuid = .incrementing } }
let expected = AgentProfile( id: UUID(0), name: "Claude Code", runtime: .claude ) await store.send(.addProfile(.claude)) { $0.settings.agentProfiles = [expected] $0.path.append(AgentProfileEditorFeature.State(profile: expected)) } await store.receive(\.delegate.settingsChanged) }
@Test(.dependencies) func systemBackPopsTheEditorPath() async { let profile = AgentProfile(name: "Codex", runtime: .codex) let storage = SettingsTestStorage() let store = withDependencies { $0.settingsFileStorage = storage.storage } operation: { @Shared(.userGlobalSettings) var settings $settings.withLock { $0.agentProfiles = [profile] } var initial = AgentProfilesFeature.State() initial.settings = settings return TestStore(initialState: initial) { AgentProfilesFeature() } }
await store.send( .path(.push(id: 0, state: AgentProfileEditorFeature.State(profile: profile))) ) { $0.path.append(AgentProfileEditorFeature.State(profile: profile)) } await store.send(.path(.popFrom(id: 0))) { $0.path.removeAll() } }
@Test(.dependencies) func profileEditedDelegateUpdatesTheListAndSanitizesPersistence() async { let profile = AgentProfile(name: "Codex", runtime: .codex) let storage = SettingsTestStorage() let (store, persisted) = withDependencies { $0.settingsFileStorage = storage.storage } operation: { @Shared(.userGlobalSettings) var settings $settings.withLock { $0.agentProfiles = [profile] } var initial = AgentProfilesFeature.State() initial.settings = settings initial.path.append(AgentProfileEditorFeature.State(profile: profile)) let store = TestStore(initialState: initial) { AgentProfilesFeature() } return (store, $settings) }
// A blank in-progress name reaches the list state but never the disk — // the persisted profile keeps its last non-blank name. var edited = profile edited.name = " " await store.send( .path(.element(id: 0, action: .delegate(.profileEdited(edited)))) ) { $0.settings.agentProfiles[0].name = " " } await store.receive(\.delegate.settingsChanged) #expect(persisted.wrappedValue.agentProfiles.count == 1) #expect(persisted.wrappedValue.agentProfiles.first?.name == "Codex")
// Typing the new name persists it normally. edited.name = "Codex · Deep" await store.send( .path(.element(id: 0, action: .delegate(.profileEdited(edited)))) ) { $0.settings.agentProfiles[0].name = "Codex · Deep" } await store.receive(\.delegate.settingsChanged) #expect(persisted.wrappedValue.agentProfiles.first?.name == "Codex · Deep") }
@Test(.dependencies) func editorRemovalDelegatePopsAndTrashesTheHome() async { var bound = AgentProfile(name: "Codex · Work", runtime: .codex) bound.bindsDedicatedHome = true let storage = SettingsTestStorage() let trashed = LockIsolated<[AgentProfile.ID]>([]) let store = withDependencies { $0.settingsFileStorage = storage.storage } operation: { @Shared(.userGlobalSettings) var settings $settings.withLock { $0.agentProfiles = [bound] } var initial = AgentProfilesFeature.State() initial.settings = settings initial.path.append(AgentProfileEditorFeature.State(profile: bound)) return TestStore(initialState: initial) { AgentProfilesFeature() } withDependencies: { $0[AgentProfileHomeClient.self].trashHome = { id in trashed.withValue { $0.append(id) } } } }
// Removal pops the editor path back to the list and trashes in the parent, so // the dismissal cannot cancel the file operation. await store.send( .path(.element(id: 0, action: .delegate(.removeProfile(bound.id, trashFiles: true)))) ) { $0.settings.agentProfiles = [] $0.path.removeAll() } await store.receive(\.delegate.settingsChanged) await store.finish() #expect(trashed.value == [bound.id]) }
@Test(.dependencies) func editorRemovalDelegateCanKeepFilesOnDisk() async { let preset = AgentProfile(name: "Claude", runtime: .claude) let storage = SettingsTestStorage() let trashed = LockIsolated<[AgentProfile.ID]>([]) let store = withDependencies { $0.settingsFileStorage = storage.storage } operation: { @Shared(.userGlobalSettings) var settings $settings.withLock { $0.agentProfiles = [preset] } var initial = AgentProfilesFeature.State() initial.settings = settings initial.path.append(AgentProfileEditorFeature.State(profile: preset)) return TestStore(initialState: initial) { AgentProfilesFeature() } withDependencies: { $0[AgentProfileHomeClient.self].trashHome = { id in trashed.withValue { $0.append(id) } } } }
await store.send( .path(.element(id: 0, action: .delegate(.removeProfile(preset.id, trashFiles: false)))) ) { $0.settings.agentProfiles = [] $0.path.removeAll() } await store.receive(\.delegate.settingsChanged) await store.finish() #expect(trashed.value.isEmpty) }
@Test(.dependencies) func togglingEnabledPersistsFromTheList() async { let profile = AgentProfile(name: "Codex", runtime: .codex) let storage = SettingsTestStorage() let (store, persisted) = withDependencies { $0.settingsFileStorage = storage.storage } operation: { @Shared(.userGlobalSettings) var settings $settings.withLock { $0.agentProfiles = [profile] } var initial = AgentProfilesFeature.State() initial.settings = settings let store = TestStore(initialState: initial) { AgentProfilesFeature() } return (store, $settings) }
var edited = store.state.settings edited.agentProfiles[0].isEnabled = false await store.send(.binding(.set(\.settings, edited))) { $0.settings.agentProfiles[0].isEnabled = false } await store.receive(\.delegate.settingsChanged) #expect(persisted.wrappedValue.agentProfiles.first?.isEnabled == false) }
@Test func launchConfigRootOnlyAppliesToTheLaunchedRuntime() { let home = URL(fileURLWithPath: "/base/agent-profiles/ABC", isDirectory: true) let identity = WorktreeTerminalState.SurfaceLaunchProfile( profileID: UUID(), name: "Codex · Work", runtime: .codex, dedicatedHome: home ) #expect(identity.configRoot(forDetected: .codex) == home) // A different agent started manually in the same pane uses its default // home; handing it the profile home would break session attribution. #expect(identity.configRoot(forDetected: .claude) == nil)
let geminiRoot = home.appending(path: ".gemini", directoryHint: .isDirectory) let nestedIdentity = WorktreeTerminalState.SurfaceLaunchProfile( profileID: UUID(), name: "Gemini · Work", runtime: .gemini, dedicatedHome: home, sessionConfigRoot: geminiRoot ) #expect(nestedIdentity.configRoot(forDetected: .gemini) == geminiRoot) }
@Test(.dependencies) func moveReordersFallbackPriority() async { let first = AgentProfile(name: "First", runtime: .codex) let second = AgentProfile(name: "Second", runtime: .claude) let storage = SettingsTestStorage() let store = withDependencies { $0.settingsFileStorage = storage.storage } operation: { @Shared(.userGlobalSettings) var settings $settings.withLock { $0.agentProfiles = [first, second] } var initial = AgentProfilesFeature.State() initial.settings = settings return TestStore(initialState: initial) { AgentProfilesFeature() } }
await store.send(.moveProfiles(IndexSet(integer: 1), 0)) { $0.settings.agentProfiles = [second, first] } await store.receive(\.delegate.settingsChanged) }
@Test(.dependencies) func repositoryDefaultAgentProfilePersistsThroughDedicatedAction() async throws { let rootURL = URL(fileURLWithPath: "/tmp/repo-\(UUID().uuidString)") let localStorage = RepositoryLocalSettingsTestStorage() let profileID = UUID() let store = TestStore( initialState: RepositorySettingsFeature.State( rootURL: rootURL, repositoryKind: .plain, settings: .default, userSettings: .default ) ) { RepositorySettingsFeature() } withDependencies: { $0.repositoryLocalSettingsStorage = localStorage.storage }
await store.send(.setDefaultAgentProfileID(profileID)) { $0.userSettings.defaultAgentProfileID = profileID } await store.receive(\.delegate.settingsChanged)
let persisted = try JSONDecoder().decode( UserRepositorySettings.self, from: #require(localStorage.data(at: ProwlPaths.userRepositorySettingsURL(for: rootURL))) ) #expect(persisted.defaultAgentProfileID == profileID)
await store.send(.setDefaultAgentProfileID(nil)) { $0.userSettings.defaultAgentProfileID = nil } await store.receive(\.delegate.settingsChanged) }
@Test(.dependencies) func repositorySettingsWritebacksPreserveExternalLaunchMemory() async { let rootURL = URL(fileURLWithPath: "/tmp/repo-\(UUID().uuidString)") let localStorage = RepositoryLocalSettingsTestStorage() let launched = UUID() let designated = UUID() let (store, shared) = withDependencies { $0.repositoryLocalSettingsStorage = localStorage.storage } operation: { let store = TestStore( initialState: RepositorySettingsFeature.State( rootURL: rootURL, repositoryKind: .plain, settings: .default, userSettings: .default ) ) { RepositorySettingsFeature() } @Shared(.userRepositorySettings(rootURL)) var userRepositorySettings // A profile launch writes the memory while Settings stays open with a // stale snapshot. $userRepositorySettings.withLock { $0.lastLaunchedAgentProfileID = launched } return (store, $userRepositorySettings) }
await store.send(.setDefaultAgentProfileID(designated)) { $0.userSettings.defaultAgentProfileID = designated } await store.receive(\.delegate.settingsChanged) #expect(shared.wrappedValue.lastLaunchedAgentProfileID == launched) #expect(shared.wrappedValue.defaultAgentProfileID == designated)
// The whole-struct binding writeback must also preserve (and refresh) it. var user = store.state.userSettings user.disabledGlobalCommandIDs = ["global-build"] await store.send(.binding(.set(\.userSettings, user))) { $0.userSettings.disabledGlobalCommandIDs = ["global-build"] $0.userSettings.lastLaunchedAgentProfileID = launched } await store.receive(\.delegate.settingsChanged) #expect(shared.wrappedValue.lastLaunchedAgentProfileID == launched) }}