native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
13 kB · 386 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387import ComposableArchitectureimport Foundationimport Testing
@testable import supacode
@MainActorstruct WorkspaceEditorFeatureTests { private let rootURL = URL(fileURLWithPath: "/tmp/workspaces/checkout-flow", isDirectory: true)
private var appEntry: ProjectWorkspaceRepositoryEntry { ProjectWorkspaceRepositoryEntry( id: "app", name: "App", role: "macOS app", path: "app", sourceKind: .existingPath, sourceLocation: "/tmp/source/app", branchName: "codex/checkout", baseRef: "main" ) }
private var apiEntry: ProjectWorkspaceRepositoryEntry { ProjectWorkspaceRepositoryEntry( id: "api", name: "API", path: "api", sourceKind: .remote, sourceLocation: "git@github.com:onevcat/api.git", baseRef: "origin/main" ) }
private var workspace: ProjectWorkspace { ProjectWorkspace( id: rootURL.path(percentEncoded: false), title: "Checkout Flow", description: "Ship the flow", taskLinks: ["https://example.com/issues/1", "PROWL-7"], repositories: [appEntry, apiEntry] ) }
private var sharedCandidate: ProjectWorkspaceCreationRepository { ProjectWorkspaceCreationRepository( id: "/tmp/source/shared", name: "Shared", rootURL: URL(fileURLWithPath: "/tmp/source/shared") ) }
private func makeEditState() -> WorkspaceEditorFeature.State { WorkspaceEditorFeature.State( editing: workspace, rootURL: rootURL, repositoryID: "/tmp/workspaces/checkout-flow", openedRepositoryCandidates: [sharedCandidate] ) }
@Test func editModePopulatesFromWorkspaceMetadata() { let state = makeEditState()
#expect(state.mode == .edit(repositoryID: "/tmp/workspaces/checkout-flow")) #expect(state.title == "Checkout Flow") #expect(state.description == "Ship the flow") #expect(state.taskLinksText == "https://example.com/issues/1\nPROWL-7") #expect(state.taskLinks == ["https://example.com/issues/1", "PROWL-7"]) #expect(state.rootPath == "/tmp/workspaces/checkout-flow") #expect(state.isRootPathDirty) #expect(state.existingRepositories.map(\.id) == ["app", "api"]) #expect(state.existingRepositories[id: "app"]?.role == "macOS app") #expect(state.existingRepositories[id: "app"]?.offersBranchDeletion == true) #expect(state.existingRepositories[id: "api"]?.offersBranchDeletion == false) #expect(state.repositories.isEmpty) #expect(state.remainingRepositoryCount == 2) #expect(state.memberEditor == nil) }
@Test func titleChangeInEditModeKeepsRootPath() async { let store = TestStore(initialState: makeEditState()) { WorkspaceEditorFeature() }
await store.send(.titleChanged("Renamed")) { $0.title = "Renamed" } #expect(store.state.rootPath == "/tmp/workspaces/checkout-flow") }
@Test func taskLinksTextParsesOneLinkPerLineAndDropsBlanks() async { let store = TestStore(initialState: makeEditState()) { WorkspaceEditorFeature() }
await store.send(.taskLinksTextChanged(" https://example.com/issues/2 \n\n PROWL-42\n ")) { $0.taskLinksText = " https://example.com/issues/2 \n\n PROWL-42\n " } #expect(store.state.taskLinks == ["https://example.com/issues/2", "PROWL-42"]) }
@Test func addRepositoryPresentsTheMemberEditorWithUnusedCandidates() async { var state = makeEditState() state.repositories = [sharedCandidate] let store = TestStore(initialState: state) { WorkspaceEditorFeature() }
// The only candidate is already added, so the picker offers nothing. await store.send(.addRepositoryButtonTapped) { $0.memberEditor = .add(openedCandidates: []) } }
@Test func memberEditorCommitAddsANewRowAndReplacesAReEditedOne() async { let store = TestStore(initialState: makeEditState()) { WorkspaceEditorFeature() }
await store.send(.addRepositoryButtonTapped) { $0.memberEditor = .add(openedCandidates: [self.sharedCandidate]) } await store.send(.memberEditor(.presented(.delegate(.commitAdded(sharedCandidate))))) { $0.repositories = [self.sharedCandidate] $0.memberEditor = nil } #expect(store.state.orderedMemberKeys == [.existing("app"), .existing("api"), .added("/tmp/source/shared")])
var renamed = sharedCandidate renamed.name = "Shared Kit" renamed.role = "library" await store.send(.editMember(.added("/tmp/source/shared"))) { $0.memberEditor = .editAdded(self.sharedCandidate) } await store.send(.memberEditor(.presented(.delegate(.commitAdded(renamed))))) { $0.repositories = [renamed] $0.memberEditor = nil } await store.send(.editMember(.added("/tmp/source/shared"))) { $0.memberEditor = .editAdded(renamed) } await store.send(.memberEditor(.presented(.delegate(.removeAdded("/tmp/source/shared"))))) { $0.repositories = [] $0.memberEditor = nil } }
@Test func editingAnExistingMemberRoundTripsNameRoleAndRemoval() async { let store = TestStore(initialState: makeEditState()) { WorkspaceEditorFeature() } var app = WorkspaceEditorExistingRepository(entry: appEntry) app.name = "Mac App" app.role = "" app.removal = .init(deleteFiles: true, deleteBranch: true)
await store.send(.editMember(.existing("app"))) { $0.memberEditor = .editExisting(WorkspaceEditorExistingRepository(entry: self.appEntry)) } await store.send(.memberEditor(.presented(.delegate(.commitExisting(app))))) { $0.existingRepositories[id: "app"] = app $0.memberEditor = nil } #expect(store.state.remainingRepositoryCount == 1) await store.send(.undoRemoval("app")) { $0.existingRepositories[id: "app"]?.removal = nil } var reopened = app reopened.removal = nil await store.send(.editMember(.existing("app"))) { $0.memberEditor = .editExisting(reopened) } await store.send(.memberEditor(.presented(.delegate(.cancel)))) { $0.memberEditor = nil } }
@Test func removeMemberMarksExistingAndDropsAdded() async { var state = makeEditState() state.repositories = [sharedCandidate] let store = TestStore(initialState: state) { WorkspaceEditorFeature() }
await store.send(.removeMember(.existing("api"))) { $0.existingRepositories[id: "api"]?.removal = .init() } await store.send(.removeMember(.added("/tmp/source/shared"))) { $0.repositories = [] } await store.send(.removeMember(.existing("missing"))) #expect(store.state.hasPendingRemovals) #expect(store.state.remainingRepositoryCount == 1) }
@Test func membersCanBeReorderedAcrossExistingAndAddedRows() async { var state = makeEditState() state.repositories = [sharedCandidate] let store = TestStore(initialState: state) { WorkspaceEditorFeature() }
#expect(store.state.orderedMemberKeys == [.existing("app"), .existing("api"), .added("/tmp/source/shared")]) await store.send(.memberMovedUp(.existing("app"))) await store.send(.memberMovedUp(.added("/tmp/source/shared"))) { $0.memberOrder = [.existing("app"), .added("/tmp/source/shared"), .existing("api")] } await store.send(.memberMovedUp(.added("/tmp/source/shared"))) { $0.memberOrder = [.added("/tmp/source/shared"), .existing("app"), .existing("api")] } await store.send(.memberMovedDown(.existing("api"))) await store.send(.removeMember(.added("/tmp/source/shared"))) { $0.repositories = [] } #expect(store.state.orderedMemberKeys == [.existing("app"), .existing("api")]) }
@Test func submitRefusesToRemoveTheLastMember() async { var state = makeEditState() state.existingRepositories[id: "app"]?.removal = .init() state.existingRepositories[id: "api"]?.removal = .init() let store = TestStore(initialState: state) { WorkspaceEditorFeature() }
await store.send(.submitButtonTapped) { $0.validationMessage = "Add at least one repository." $0.validationTarget = .repositories } }
@Test func submitInEditModeBuildsUpdateRequestInRowOrder() async { let now = Date(timeIntervalSince1970: 1_700_000_000) var state = makeEditState() state.title = " Checkout Flow v2 " state.description = " Ship it \n" state.taskLinksText = " https://example.com/issues/2 \n\n" state.existingRepositories[id: "app"]?.name = " Mac App " state.existingRepositories[id: "app"]?.role = "" state.existingRepositories[id: "api"]?.removal = .init(deleteFiles: true, deleteBranch: false) var shared = sharedCandidate shared.role = " library " state.repositories = [shared] state.memberOrder = [.added("/tmp/source/shared"), .existing("app"), .existing("api")] let store = TestStore(initialState: state) { WorkspaceEditorFeature() } withDependencies: { $0.date.now = now }
var renamedApp = appEntry renamedApp.name = "Mac App" renamedApp.role = nil await store.send(.submitButtonTapped) await store.receive( .delegate( .submit( .update( ProjectWorkspaceUpdateRequest( rootURL: rootURL, title: "Checkout Flow v2", description: "Ship it", taskLinks: ["https://example.com/issues/2"], members: [ .added( ProjectWorkspaceRepositoryPlan( id: "/tmp/source/shared", name: "Shared", role: "library", path: nil, sourceKind: .existingPath, sourceLocation: "/tmp/source/shared", checkout: .link )), .existing(renamedApp), ], removals: [ ProjectWorkspaceRepositoryRemoval(entry: apiEntry, deleteFiles: true, deleteBranch: false) ], updatedAt: now ) ) ) ) ) }
@Test func submitInCreateModeCarriesDescriptionLinksRoleAndOrder() async { var repoA = ProjectWorkspaceCreationRepository( id: "/tmp/repo-a", name: "Repo A", rootURL: URL(fileURLWithPath: "/tmp/repo-a")) repoA.role = "app" let repoB = ProjectWorkspaceCreationRepository( id: "/tmp/repo-b", name: "Repo B", rootURL: URL(fileURLWithPath: "/tmp/repo-b")) var state = WorkspaceEditorFeature.State( repositories: [repoA, repoB], title: " Multi Repo ", rootPath: " /tmp/multi-repo-workspace " ) state.description = "Just two" state.taskLinksText = "PROWL-1" state.memberOrder = [.added("/tmp/repo-b"), .added("/tmp/repo-a")] let store = TestStore(initialState: state) { WorkspaceEditorFeature() }
await store.send(.submitButtonTapped) await store.receive( .delegate( .submit( .create( ProjectWorkspaceCreationDraft( title: "Multi Repo", description: "Just two", taskLinks: ["PROWL-1"], rootURL: URL(filePath: "/tmp/multi-repo-workspace", directoryHint: .isDirectory), repositories: [ ProjectWorkspaceRepositoryPlan( id: "/tmp/repo-b", name: "Repo B", path: nil, sourceKind: .existingPath, sourceLocation: "/tmp/repo-b", checkout: .link ), ProjectWorkspaceRepositoryPlan( id: "/tmp/repo-a", name: "Repo A", role: "app", path: nil, sourceKind: .existingPath, sourceLocation: "/tmp/repo-a", checkout: .link ), ] ) ) ) ) ) }
@Test func submitReportsAStaleInvalidRow() async { var state = makeEditState() state.repositories = [ ProjectWorkspaceCreationRepository( id: "remote", name: "Remote", sourceKind: .remote, sourceLocation: "git@github.com:onevcat/x.git", checkoutMode: .useExistingRef ) ] let store = TestStore(initialState: state) { WorkspaceEditorFeature() }
await store.send(.submitButtonTapped) { $0.validationMessage = "Choose an existing branch for Remote." $0.validationTarget = .repositories } }
@Test func createModeFolderFollowsTitleUntilChosen() async { let store = TestStore( initialState: WorkspaceEditorFeature.State(repositories: [], title: "Workspace", rootPath: "/tmp/x") ) { WorkspaceEditorFeature() } store.exhaustivity = .off
await store.send(.titleChanged("Checkout Flow")) { $0.title = "Checkout Flow" $0.rootPath = ProjectWorkspace.workspaceRootPath(folderName: "Checkout-Flow", suffix: nil) } await store.receive(\.automaticRootPathResolved) await store.send(.rootPathChosen("/tmp/custom")) { $0.rootPath = "/tmp/custom" $0.isRootPathDirty = true } await store.send(.titleChanged("Other")) { $0.title = "Other" } #expect(store.state.rootPath == "/tmp/custom") }}