native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284import ComposableArchitectureimport DependenciesTestSupportimport Foundationimport IdentifiedCollectionsimport Testing
@testable import Prowl
@MainActorstruct BatchedPullRequestRefreshReducerTests { @Test func refreshDispatchesViaCoordinatorUsingCurrentRemoteInfos() async { let context = makeContext() let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([]) let upstreamInfo = GithubRemoteInfo(host: "github.com", owner: "khoi", repo: "upstream")
let store = TestStore(initialState: context.state) { RepositoriesFeature() } withDependencies: { $0.gitClient.githubRemoteInfos = { _ in [context.remoteInfo, upstreamInfo] } $0.githubCLI.resolveRemoteInfo = { _ in Issue.record("gh resolveRemoteInfo should not run when git remotes resolve") return nil } $0.githubCLI.batchPullRequests = { _, _, _, _, _, _ in Issue.record("Legacy batchPullRequests should not run on coordinator path") return [:] } $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) }
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh( repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs ) ) ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshRequested) { $0.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] } await store.receive(\.githubIntegration.pullRequestRefreshBatchCountResolved) { $0.prRefreshBatchCountsByRepositoryID[context.repository.id] = 1 $0.prRefreshRemotePrioritiesByRepositoryID[context.repository.id] = [ "github.com/khoi/alpha": 0, "github.com/khoi/upstream": 1, ] } await store.finish()
let snapshot = enqueued.value #expect(snapshot.count == 1) let request = snapshot[0] #expect(request.host == "github.com") #expect(request.repositories == [context.remoteInfo, upstreamInfo]) #expect(request.branches == ["main", "feature"]) }
@Test func refreshListsChecksOnlyForTheSelectedWorktree() async { let context = makeContext() var initialState = context.state initialState.selection = .worktree(context.featureWorktree.id) let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([])
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.gitClient.githubRemoteInfos = { _ in [context.remoteInfo] } $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) } store.exhaustivity = .off
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh( repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs ) ) ) await store.finish()
#expect(enqueued.value.map(\.detailBranches) == [[context.featureWorktree.name]]) }
@Test func periodicRefreshAsksOnlyForDueWorktrees() async { let context = makeContext() let now = Date(timeIntervalSince1970: 1_000_000) var initialState = context.state var merged = WorktreeInfoEntry() merged.pullRequest = makePullRequestFixture(state: "MERGED") initialState.worktreeInfoByID[context.featureWorktree.id] = merged // The merged pull request was answered a minute ago; the branch without one ten minutes ago. initialState.pullRequestRefreshCheckpointByWorktreeID = [ context.featureWorktree.id: makeCheckpoint( branch: "feature", pullRequest: merged.pullRequest, answeredAt: now.addingTimeInterval(-60) ), context.mainWorktree.id: makeCheckpoint(branch: "main", answeredAt: now.addingTimeInterval(-600)), ] let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([])
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = now $0.gitClient.githubRemoteInfos = { _ in [context.remoteInfo] } $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) } store.exhaustivity = .off
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh(repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs) ) ) await store.finish()
#expect(enqueued.value.map(\.branches) == [["main"]]) #expect(enqueued.value.map(\.worktreeIDs) == [[context.mainWorktree.id]]) }
@Test func periodicRefreshWithNothingDueSendsNoQuery() async { let context = makeContext() let now = Date(timeIntervalSince1970: 1_000_000) var initialState = context.state initialState.pullRequestRefreshCheckpointByWorktreeID = [ context.featureWorktree.id: makeCheckpoint(branch: "feature", answeredAt: now.addingTimeInterval(-60)), context.mainWorktree.id: makeCheckpoint(branch: "main", answeredAt: now.addingTimeInterval(-60)), ]
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = now $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { _ in Issue.record("Nothing is due, so nothing should be enqueued") }, cancelHost: { _ in }, reset: {} ) }
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh(repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs) ) ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshRequested) await store.finish() }
@Test func selectedWorktreeIsAskedForEvenWhenRecentlyAnswered() async { let context = makeContext() let now = Date(timeIntervalSince1970: 1_000_000) var initialState = context.state initialState.selection = .worktree(context.featureWorktree.id) initialState.pullRequestRefreshCheckpointByWorktreeID = [ context.featureWorktree.id: makeCheckpoint(branch: "feature", answeredAt: now.addingTimeInterval(-5)), context.mainWorktree.id: makeCheckpoint(branch: "main", answeredAt: now.addingTimeInterval(-5)), ] let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([])
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = now $0.gitClient.githubRemoteInfos = { _ in [context.remoteInfo] } $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) } store.exhaustivity = .off
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh(repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs) ) ) await store.finish()
#expect(enqueued.value.map(\.branches) == [["feature"]]) }
@Test func markedWorktreeIsAskedDespiteARecentAnswerAndThenUnmarked() async { let context = makeContext() let now = Date(timeIntervalSince1970: 1_000_000) var initialState = context.state var merged = WorktreeInfoEntry() merged.pullRequest = makePullRequestFixture(state: "MERGED") initialState.worktreeInfoByID[context.featureWorktree.id] = merged // An answer from the old remote, or before an action, recorded a fresh time. initialState.pullRequestRefreshCheckpointByWorktreeID = [ context.featureWorktree.id: makeCheckpoint( branch: "feature", pullRequest: merged.pullRequest, answeredAt: now.addingTimeInterval(-10) ), context.mainWorktree.id: makeCheckpoint(branch: "main", answeredAt: now.addingTimeInterval(-10)), ] initialState.pullRequestRefreshForcedWorktreeIDs = [context.featureWorktree.id] let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([])
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = now $0.gitClient.githubRemoteInfos = { _ in [context.remoteInfo] } $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) } store.exhaustivity = .off
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh(repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs) ) ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshRequested) { $0.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] $0.pullRequestRefreshForcedWorktreeIDs = [] $0.sentPullRequestRefreshMarks = [context.repository.id: [context.featureWorktree.id]] } await store.finish()
#expect(enqueued.value.map(\.branches) == [["feature"]]) }
@Test func markSurvivesARefreshThatNeverReachesGitHub() async { let context = makeContext() let now = Date(timeIntervalSince1970: 1_000_000) var initialState = context.state initialState.pullRequestRefreshCheckpointByWorktreeID = [ context.featureWorktree.id: makeCheckpoint(branch: "feature", answeredAt: now.addingTimeInterval(-10)) ] initialState.pullRequestRefreshForcedWorktreeIDs = [context.featureWorktree.id]
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = now // No GitHub remote resolves, so the refresh completes without a query. $0.gitClient.githubRemoteInfos = { _ in [] } $0.githubCLI.resolveRemoteInfo = { _ in nil } $0.pullRequestRefreshCoordinator = .unimplemented } store.exhaustivity = .off
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh(repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs) ) ) await store.finish() await store.skipReceivedActions()
#expect(store.state.pullRequestRefreshForcedWorktreeIDs == [context.featureWorktree.id]) #expect(store.state.sentPullRequestRefreshMarks.isEmpty) }
@Test func answeredBranchDropsItsMarkAndAnUnansweredOneKeepsIt() async { let context = makeContext() let now = Date(timeIntervalSince1970: 1_000_000) var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] initialState.sentPullRequestRefreshMarks = [ context.repository.id: [context.featureWorktree.id, context.mainWorktree.id] ]
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = now $0.pullRequestRefreshCoordinator = .unimplemented } store.exhaustivity = .off
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: ["feature": makePullRequestFixture()], confirmedNoPrBranches: [] ) )) ) #expect( store.state.sentPullRequestRefreshMarks == [context.repository.id: [context.mainWorktree.id]] ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) await store.finish()
#expect(store.state.pullRequestRefreshForcedWorktreeIDs == [context.mainWorktree.id]) #expect(store.state.sentPullRequestRefreshMarks.isEmpty) }
@Test func markSurvivesWhenOneHostConfirmsAndAnotherFails() async { let context = makeContext() var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] initialState.prRefreshBatchCountsByRepositoryID[context.repository.id] = 2 initialState.sentPullRequestRefreshMarks = [context.repository.id: [context.featureWorktree.id]]
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = refreshDate $0.pullRequestRefreshCoordinator = .unimplemented } store.exhaustivity = .off
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: [:], confirmedNoPrBranches: ["feature"] ) )) ) await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .failed(repositoryID: context.repository.id, worktreeIDs: context.worktreeIDs, message: "host down") )) ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) await store.finish()
// The failed host leaves the branch's status unknown: no answer is recorded and the mark is back. #expect(store.state.pullRequestRefreshCheckpointByWorktreeID.isEmpty) #expect(store.state.pullRequestRefreshForcedWorktreeIDs == [context.featureWorktree.id]) }
@Test func pullRequestActionMarksItsWorktreeForTheNextRefresh() async { let context = makeContext() let store = TestStore(initialState: context.state) { RepositoriesFeature() } store.exhaustivity = .off
await store.send(.githubIntegration(.delayedPullRequestRefresh(context.featureWorktree.id))) { $0.pullRequestRefreshForcedWorktreeIDs = [context.featureWorktree.id] } // The action's delayed refresh runs on the wall clock; under load it may already be done. await store.skipInFlightEffects(strict: false) }
@Test func answeredRefreshRecordsWhenEachBranchWasAnswered() async { let context = makeContext() let now = Date(timeIntervalSince1970: 1_000_000) var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id]
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = now $0.pullRequestRefreshCoordinator = .unimplemented } store.exhaustivity = .off
// GitHub answered for "feature"; "main" stayed unknown after a partial failure. await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: ["feature": makePullRequestFixture()], confirmedNoPrBranches: [] ) )) ) { $0.pullRequestRefreshCheckpointByWorktreeID = [ context.featureWorktree.id: makeCheckpoint( branch: "feature", pullRequest: makePullRequestFixture(), answeredAt: now ) ] } await store.finish() }
@Test func periodicRefreshAsksAboutASwitchedBranchAtOnce() async { let context = makeContext() let now = Date(timeIntervalSince1970: 1_000_000) var initialState = context.state var merged = WorktreeInfoEntry() merged.pullRequest = makePullRequestFixture(state: "MERGED") initialState.worktreeInfoByID[context.featureWorktree.id] = merged // GitHub answered a minute ago for the merged branch this worktree was on; it is on "feature" now. initialState.pullRequestRefreshCheckpointByWorktreeID = [ context.featureWorktree.id: makeCheckpoint( branch: "merged-feature", pullRequest: merged.pullRequest, answeredAt: now.addingTimeInterval(-60) ), context.mainWorktree.id: makeCheckpoint(branch: "main", answeredAt: now.addingTimeInterval(-60)), ] let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([])
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = now $0.gitClient.githubRemoteInfos = { _ in [context.remoteInfo] } $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) } store.exhaustivity = .off
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh(repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs) ) ) await store.finish()
#expect(enqueued.value.map(\.branches) == [["feature"]]) }
@Test func unknownMergeabilityKeepsThePullRequestActiveWhileTheDisplayKeepsTheKnownState() async { let context = makeContext() let now = Date(timeIntervalSince1970: 1_000_000) var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] var known = WorktreeInfoEntry() known.pullRequest = makePullRequestFixture(mergeable: "MERGEABLE", mergeStateStatus: "CLEAN") initialState.worktreeInfoByID[context.featureWorktree.id] = known let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([])
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = now $0.gitClient.githubRemoteInfos = { _ in [context.remoteInfo] } $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) } store.exhaustivity = .off
// GitHub is computing mergeability again; the display keeps the known state meanwhile. await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: ["feature": makePullRequestFixture(mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN")], confirmedNoPrBranches: ["main"] ) )) ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) await store.finish()
#expect(store.state.worktreeInfoByID[context.featureWorktree.id]?.pullRequest?.mergeable == "MERGEABLE") #expect(store.state.pullRequestRefreshCheckpointByWorktreeID[context.featureWorktree.id]?.interval == nil)
// A minute later only the pull request GitHub is still computing is asked about. store.dependencies.date.now = now.addingTimeInterval(60) await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh(repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs) ) ) await store.finish()
#expect(enqueued.value.map(\.branches) == [["feature"]]) }
@Test func partialAnswerShowsItsPullRequestsButRecordsNoCheckpointAndKeepsTheMark() async { let context = makeContext() var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] initialState.sentPullRequestRefreshMarks = [context.repository.id: [context.featureWorktree.id]] let merged = makePullRequestFixture(state: "MERGED")
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = refreshDate $0.pullRequestRefreshCoordinator = .unimplemented } store.exhaustivity = .off
// The preferred remote and its fallback failed; a secondary remote answered with a merged PR. await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: ["feature": merged], confirmedNoPrBranches: [], isPartial: true ) )) ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) await store.finish()
#expect(store.state.worktreeInfoByID[context.featureWorktree.id]?.pullRequest == merged) #expect(store.state.pullRequestRefreshCheckpointByWorktreeID.isEmpty) #expect(store.state.pullRequestRefreshForcedWorktreeIDs == [context.featureWorktree.id]) #expect(store.state.sentPullRequestRefreshMarks.isEmpty) }
@Test func refreshWaitsForAllHostBatchesBeforeCompleting() async { let context = makeContext() let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([]) let githubPullRequest = makePullRequestFixture() let enterpriseInfo = GithubRemoteInfo(host: "ghe.example", owner: "khoi", repo: "alpha")
let store = TestStore(initialState: context.state) { RepositoriesFeature() } withDependencies: { $0.date.now = refreshDate $0.gitClient.githubRemoteInfos = { _ in [context.remoteInfo, enterpriseInfo] } $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) }
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh( repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs ) ) ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshRequested) { $0.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] } await store.receive(\.githubIntegration.pullRequestRefreshBatchCountResolved) { $0.prRefreshBatchCountsByRepositoryID[context.repository.id] = 2 $0.prRefreshRemotePrioritiesByRepositoryID[context.repository.id] = [ "ghe.example/khoi/alpha": 1, "github.com/khoi/alpha": 0, ] }
#expect(Set(enqueued.value.map(\.host)) == ["github.com", "ghe.example"])
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: ["feature": githubPullRequest], confirmedNoPrBranches: [] ) )) ) { $0.prRefreshBatchCountsByRepositoryID[context.repository.id] = 1 $0.prRefreshResultsByRepositoryID[context.repository.id] = ["feature": githubPullRequest] $0.prRefreshNoPrBranchesByID[context.repository.id] = [] $0.prRefreshResultPrioritiesByRepositoryID[context.repository.id] = ["feature": .max] }
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: [:], confirmedNoPrBranches: [] ) )) ) { $0.pullRequestRefreshCheckpointByWorktreeID[context.featureWorktree.id] = makeCheckpoint( branch: "feature", pullRequest: githubPullRequest, answeredAt: refreshDate ) $0.prRefreshBatchCountsByRepositoryID = [:] $0.prRefreshResultsByRepositoryID = [:] $0.prRefreshNoPrBranchesByID = [:] $0.prRefreshResultPrioritiesByRepositoryID = [:] } await store.receive(\.githubIntegration.repositoryPullRequestsLoaded) { var entry = WorktreeInfoEntry() entry.pullRequest = githubPullRequest $0.worktreeInfoByID[context.featureWorktree.id] = entry } await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) { $0.inFlightPullRequestRefreshRepositoryIDs = [] $0.prRefreshBatchCountsByRepositoryID = [:] $0.prRefreshRemotePrioritiesByRepositoryID = [:] } await store.finish() }
@Test func refreshPrefersHigherPriorityRemoteWhenHostBatchResultsRace() async { let context = makeContext() let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([]) let enterpriseInfo = GithubRemoteInfo(host: "github.enterprise.test", owner: "khoi", repo: "alpha") let enterprisePullRequest = makePullRequestFixture( title: "Enterprise PR", url: "https://github.enterprise.test/khoi/alpha/pull/8" ) let originPullRequest = makePullRequestFixture( title: "Origin PR", url: "https://github.com/khoi/alpha/pull/7" )
let store = TestStore(initialState: context.state) { RepositoriesFeature() } withDependencies: { $0.date.now = refreshDate $0.gitClient.githubRemoteInfos = { _ in [context.remoteInfo, enterpriseInfo] } $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) }
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh( repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs ) ) ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshRequested) { $0.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] } await store.receive(\.githubIntegration.pullRequestRefreshBatchCountResolved) { $0.prRefreshBatchCountsByRepositoryID[context.repository.id] = 2 $0.prRefreshRemotePrioritiesByRepositoryID[context.repository.id] = [ "github.com/khoi/alpha": 0, "github.enterprise.test/khoi/alpha": 1, ] }
#expect(Set(enqueued.value.map(\.host)) == ["github.com", "github.enterprise.test"])
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: ["feature": enterprisePullRequest], confirmedNoPrBranches: [] ) )) ) { $0.prRefreshBatchCountsByRepositoryID[context.repository.id] = 1 $0.prRefreshResultsByRepositoryID[context.repository.id] = ["feature": enterprisePullRequest] $0.prRefreshNoPrBranchesByID[context.repository.id] = [] $0.prRefreshResultPrioritiesByRepositoryID[context.repository.id] = ["feature": 1] }
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: ["feature": originPullRequest], confirmedNoPrBranches: [] ) )) ) { $0.pullRequestRefreshCheckpointByWorktreeID[context.featureWorktree.id] = makeCheckpoint( branch: "feature", pullRequest: originPullRequest, answeredAt: refreshDate ) $0.prRefreshBatchCountsByRepositoryID = [:] $0.prRefreshResultsByRepositoryID = [:] $0.prRefreshNoPrBranchesByID = [:] $0.prRefreshResultPrioritiesByRepositoryID = [:] } await store.receive(\.githubIntegration.repositoryPullRequestsLoaded) { var entry = WorktreeInfoEntry() entry.pullRequest = originPullRequest $0.worktreeInfoByID[context.featureWorktree.id] = entry } await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) { $0.inFlightPullRequestRefreshRepositoryIDs = [] $0.prRefreshBatchCountsByRepositoryID = [:] $0.prRefreshRemotePrioritiesByRepositoryID = [:] } await store.finish() }
@Test func refreshResolvesRemoteInfosOnFirstRun() async { let context = makeContext() let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([]) let initialState = context.state
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.gitClient.githubRemoteInfos = { _ in [context.remoteInfo] } $0.githubCLI.resolveRemoteInfo = { _ in Issue.record("gh resolveRemoteInfo should not run when git remotes resolve") return nil } $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) }
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh( repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs ) ) ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshRequested) { $0.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] } await store.receive(\.githubIntegration.pullRequestRefreshBatchCountResolved) { $0.prRefreshBatchCountsByRepositoryID[context.repository.id] = 1 $0.prRefreshRemotePrioritiesByRepositoryID[context.repository.id] = [ "github.com/khoi/alpha": 0 ] } await store.finish()
#expect(enqueued.value.count == 1) }
@Test func refreshPreservesPullRequestsWhenGithubRemotesUnavailable() async { let context = makeContext() let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([]) let stalePullRequest = makePullRequestFixture(url: "https://github.com/khoi/alpha/pull/7") var initialState = context.state var staleEntry = WorktreeInfoEntry() staleEntry.pullRequest = stalePullRequest initialState.worktreeInfoByID[context.featureWorktree.id] = staleEntry
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.gitClient.githubRemoteInfos = { _ in [] } $0.githubCLI.resolveRemoteInfo = { _ in nil } $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) }
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh( repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs ) ) ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshRequested) { $0.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] } await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) { $0.inFlightPullRequestRefreshRepositoryIDs = [] } await store.finish()
#expect(enqueued.value.isEmpty) }
@Test func coordinatorOutcomeRefreshedTranslatesToLoadedAndCompleted() async { let context = makeContext() var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id]
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = refreshDate $0.pullRequestRefreshCoordinator = .unimplemented }
let pullRequest = makePullRequestFixture() let outcome = PullRequestRefreshCoordinator.Outcome.refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: ["feature": pullRequest], confirmedNoPrBranches: [] )
await store.send(.githubIntegration(.pullRequestRefreshBatchOutcome(outcome))) { $0.pullRequestRefreshCheckpointByWorktreeID[context.featureWorktree.id] = makeCheckpoint( branch: "feature", pullRequest: pullRequest, answeredAt: refreshDate ) } await store.receive(\.githubIntegration.repositoryPullRequestsLoaded) { var entry = WorktreeInfoEntry() entry.pullRequest = pullRequest $0.worktreeInfoByID[context.featureWorktree.id] = entry } await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) { $0.inFlightPullRequestRefreshRepositoryIDs = [] } await store.finish() }
@Test func coordinatorOutcomeFailedClearsInFlight() async { let context = makeContext() var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id]
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.pullRequestRefreshCoordinator = .unimplemented }
let outcome = PullRequestRefreshCoordinator.Outcome.failed( repositoryID: context.repository.id, worktreeIDs: context.worktreeIDs, message: "boom" )
await store.send(.githubIntegration(.pullRequestRefreshBatchOutcome(outcome))) await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) { $0.inFlightPullRequestRefreshRepositoryIDs = [] } await store.finish() }
@Test func rateLimitRetryTimeFollowsTheGate() async { let context = makeContext() let retryAt = Date(timeIntervalSince1970: 1_000_060) let store = TestStore(initialState: context.state) { RepositoriesFeature() }
await store.send(.githubIntegration(.rateLimitRetryTimeChanged(retryAt))) { $0.githubRateLimitedUntil = retryAt } await store.send(.githubIntegration(.rateLimitRetryTimeChanged(nil))) { $0.githubRateLimitedUntil = nil } }
@Test func taskForwardsTheGateRetryTimes() async { let context = makeContext() let retryAt = Date(timeIntervalSince1970: 1_000_060) let store = TestStore(initialState: context.state) { RepositoriesFeature() } withDependencies: { $0.githubCLI.rateLimitRetryTimes = { AsyncStream { continuation in continuation.yield(retryAt) continuation.yield(nil) continuation.finish() } } }
store.exhaustivity = .off await store.send(.task) await store.receive(\.githubIntegration.rateLimitRetryTimeChanged) { $0.githubRateLimitedUntil = retryAt } await store.receive(\.githubIntegration.rateLimitRetryTimeChanged) { $0.githubRateLimitedUntil = nil } }
@Test func coordinatorOutcomeConfirmedNoPrClearsStalePullRequest() async { let context = makeContext() let stalePullRequest = makePullRequestFixture(url: "https://github.com/khoi/alpha/pull/7") var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] var staleEntry = WorktreeInfoEntry() staleEntry.pullRequest = stalePullRequest initialState.worktreeInfoByID[context.featureWorktree.id] = staleEntry
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = refreshDate $0.pullRequestRefreshCoordinator = .unimplemented }
let outcome = PullRequestRefreshCoordinator.Outcome.refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: [:], confirmedNoPrBranches: ["feature"] )
await store.send(.githubIntegration(.pullRequestRefreshBatchOutcome(outcome))) { $0.pullRequestRefreshCheckpointByWorktreeID[context.featureWorktree.id] = makeCheckpoint( branch: "feature", answeredAt: refreshDate ) } await store.receive(\.githubIntegration.repositoryPullRequestsLoaded) { $0.worktreeInfoByID.removeValue(forKey: context.featureWorktree.id) } await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) { $0.inFlightPullRequestRefreshRepositoryIDs = [] } await store.finish() }
@Test func coordinatorOutcomeUnknownBranchStatusPreservesPullRequest() async { let context = makeContext() let stalePullRequest = makePullRequestFixture(url: "https://github.com/khoi/alpha/pull/7") var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] var staleEntry = WorktreeInfoEntry() staleEntry.pullRequest = stalePullRequest initialState.worktreeInfoByID[context.featureWorktree.id] = staleEntry
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.pullRequestRefreshCoordinator = .unimplemented }
let outcome = PullRequestRefreshCoordinator.Outcome.refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: [:], confirmedNoPrBranches: [] )
await store.send(.githubIntegration(.pullRequestRefreshBatchOutcome(outcome))) await store.receive(\.githubIntegration.repositoryPullRequestsLoaded) await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) { $0.inFlightPullRequestRefreshRepositoryIDs = [] } await store.finish()
#expect(store.state.worktreeInfoByID[context.featureWorktree.id]?.pullRequest == stalePullRequest) }
@Test func confirmedNoPrClearIsSuppressedWhenAnotherHostBatchFailedFirst() async { let context = makeContext() let stalePullRequest = makePullRequestFixture(url: "https://ghe.example/khoi/alpha/pull/9") var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] initialState.prRefreshBatchCountsByRepositoryID[context.repository.id] = 2 var staleEntry = WorktreeInfoEntry() staleEntry.pullRequest = stalePullRequest initialState.worktreeInfoByID[context.featureWorktree.id] = staleEntry
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = refreshDate $0.pullRequestRefreshCoordinator = .unimplemented }
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .failed( repositoryID: context.repository.id, worktreeIDs: context.worktreeIDs, message: "enterprise host down" ) )) ) { $0.prRefreshFailedBatchRepositoryIDs = [context.repository.id] $0.prRefreshBatchCountsByRepositoryID[context.repository.id] = 1 }
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: [:], confirmedNoPrBranches: ["feature"] ) )) ) { $0.prRefreshBatchCountsByRepositoryID = [:] $0.prRefreshFailedBatchRepositoryIDs = [] } await store.receive(\.githubIntegration.repositoryPullRequestsLoaded) await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) { $0.inFlightPullRequestRefreshRepositoryIDs = [] } await store.finish()
#expect(store.state.worktreeInfoByID[context.featureWorktree.id]?.pullRequest == stalePullRequest) }
@Test func confirmedNoPrClearIsDiscardedWhenFinalHostBatchFails() async { let context = makeContext() let stalePullRequest = makePullRequestFixture(url: "https://ghe.example/khoi/alpha/pull/9") var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] initialState.prRefreshBatchCountsByRepositoryID[context.repository.id] = 2 var staleEntry = WorktreeInfoEntry() staleEntry.pullRequest = stalePullRequest initialState.worktreeInfoByID[context.featureWorktree.id] = staleEntry
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = refreshDate $0.pullRequestRefreshCoordinator = .unimplemented }
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: [:], confirmedNoPrBranches: ["feature"] ) )) ) { $0.prRefreshBatchCountsByRepositoryID[context.repository.id] = 1 $0.prRefreshResultsByRepositoryID[context.repository.id] = [:] $0.prRefreshNoPrBranchesByID[context.repository.id] = ["feature"] $0.prRefreshResultPrioritiesByRepositoryID[context.repository.id] = [:] }
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .failed( repositoryID: context.repository.id, worktreeIDs: context.worktreeIDs, message: "enterprise host down" ) )) ) { $0.prRefreshBatchCountsByRepositoryID = [:] $0.prRefreshResultsByRepositoryID = [:] $0.prRefreshNoPrBranchesByID = [:] $0.prRefreshResultPrioritiesByRepositoryID = [:] } await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) { $0.inFlightPullRequestRefreshRepositoryIDs = [] } await store.finish()
#expect(store.state.worktreeInfoByID[context.featureWorktree.id]?.pullRequest == stalePullRequest) }
@Test func pullRequestFromLaterHostBatchOverridesEarlierConfirmedNoPr() async { let context = makeContext() let pullRequest = makePullRequestFixture() var initialState = context.state initialState.inFlightPullRequestRefreshRepositoryIDs = [context.repository.id] initialState.prRefreshBatchCountsByRepositoryID[context.repository.id] = 2
let store = TestStore(initialState: initialState) { RepositoriesFeature() } withDependencies: { $0.date.now = refreshDate $0.pullRequestRefreshCoordinator = .unimplemented }
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: [:], confirmedNoPrBranches: ["feature"] ) )) ) { $0.prRefreshBatchCountsByRepositoryID[context.repository.id] = 1 $0.prRefreshResultsByRepositoryID[context.repository.id] = [:] $0.prRefreshNoPrBranchesByID[context.repository.id] = ["feature"] $0.prRefreshResultPrioritiesByRepositoryID[context.repository.id] = [:] }
await store.send( .githubIntegration( .pullRequestRefreshBatchOutcome( .refreshed( repositoryID: context.repository.id, repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs, prsByBranch: ["feature": pullRequest], confirmedNoPrBranches: [] ) )) ) { $0.pullRequestRefreshCheckpointByWorktreeID[context.featureWorktree.id] = makeCheckpoint( branch: "feature", pullRequest: pullRequest, answeredAt: refreshDate ) $0.prRefreshBatchCountsByRepositoryID = [:] $0.prRefreshResultsByRepositoryID = [:] $0.prRefreshNoPrBranchesByID = [:] $0.prRefreshResultPrioritiesByRepositoryID = [:] } await store.receive(\.githubIntegration.repositoryPullRequestsLoaded) { var entry = WorktreeInfoEntry() entry.pullRequest = pullRequest $0.worktreeInfoByID[context.featureWorktree.id] = entry } await store.receive(\.githubIntegration.repositoryPullRequestRefreshCompleted) { $0.inFlightPullRequestRefreshRepositoryIDs = [] } await store.finish() }
@Test(.dependencies) func refreshSkippedWhenPullRequestStateFetchDisabled() async { let context = makeContext() let enqueued = LockIsolated<[PullRequestRefreshCoordinator.Request]>([])
@Shared(.repositorySettings(context.repoRootURL)) var repositorySettings $repositorySettings.withLock { $0.fetchPullRequestState = false }
let store = TestStore(initialState: context.state) { RepositoriesFeature() } withDependencies: { $0.pullRequestRefreshCoordinator = PullRequestRefreshCoordinatorClient( enqueue: { request in enqueued.withValue { $0.append(request) } }, cancelHost: { _ in }, reset: {} ) }
await store.send( .worktreeInfoEvent( .repositoryPullRequestRefresh( repositoryRootURL: context.repoRootURL, worktreeIDs: context.worktreeIDs ) ) ) await store.receive(\.githubIntegration.repositoryPullRequestRefreshRequested) await store.finish()
#expect(enqueued.value.isEmpty) }}
// MARK: - Fixtures
@MainActorprivate func makeContext() -> RefreshTestContext { let repoRoot = "/tmp/coord-repo-\(UUID().uuidString)" let mainWorktree = Worktree( id: repoRoot, name: "main", detail: "detail", workingDirectory: URL(fileURLWithPath: repoRoot), repositoryRootURL: URL(fileURLWithPath: repoRoot) ) let featureWorktree = Worktree( id: "\(repoRoot)/feature", name: "feature", detail: "detail", workingDirectory: URL(fileURLWithPath: "\(repoRoot)/feature"), repositoryRootURL: URL(fileURLWithPath: repoRoot) ) let repository = Repository( id: repoRoot, rootURL: URL(fileURLWithPath: repoRoot), name: "alpha", worktrees: IdentifiedArrayOf(uniqueElements: [mainWorktree, featureWorktree]) ) var state = RepositoriesFeature.State() state.repositories = [repository] state.repositoryRoots = [repository.rootURL] state.githubIntegrationAvailability = .available return RefreshTestContext( repoRoot: repoRoot, repository: repository, mainWorktree: mainWorktree, featureWorktree: featureWorktree, remoteInfo: GithubRemoteInfo(host: "github.com", owner: "khoi", repo: "alpha"), state: state )}
nonisolated private let refreshDate = Date(timeIntervalSince1970: 1_700_000_000)
@MainActorprivate struct RefreshTestContext { let repoRoot: String let repository: Repository let mainWorktree: Worktree let featureWorktree: Worktree let remoteInfo: GithubRemoteInfo let state: RepositoriesFeature.State
var repoRootURL: URL { URL(fileURLWithPath: repoRoot) } var worktreeIDs: [Worktree.ID] { [mainWorktree.id, featureWorktree.id] }}
nonisolated private func makeCheckpoint( branch: String, pullRequest: GithubPullRequest? = nil, answeredAt: Date) -> PullRequestRefreshCadence.Checkpoint { PullRequestRefreshCadence.Checkpoint(branch: branch, pullRequest: pullRequest, answeredAt: answeredAt)}
nonisolated private func makePullRequestFixture( title: String = "Coord PR", url: String = "https://example.com/coord-pr/7", state: String = "OPEN", mergeable: String? = nil, mergeStateStatus: String? = nil) -> GithubPullRequest { GithubPullRequest( number: 7, title: title, state: state, additions: 0, deletions: 0, isDraft: false, reviewDecision: nil, mergeable: mergeable, mergeStateStatus: mergeStateStatus, updatedAt: nil, url: url, headRefName: "feature", baseRefName: "main", commitsCount: 1, authorLogin: "khoi", statusCheckRollup: nil )}