mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
fix(tasks): cap advertised GitHub pages at the search result window
GitHub's Search API rejects requests past its first-1000-results window with HTTP 422, but totalPages was derived from the raw total_count, so the pagination bar advertised pages that could never load and clicks on them silently did nothing (#11485). Cap per-repo advertised pages at floor(1000 / perRepoLimit), and when a page load comes back empty, say so with a toast instead of ignoring the click — clamping the advertised count only when no fetch threw, so transient failures don't shrink the bar.
This commit is contained in:
@@ -6165,7 +6165,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
setPaginationLoading(true)
|
||||
setLoadingTargetPage(target)
|
||||
try {
|
||||
const { items } = await fetchWorkItemsNextPage(
|
||||
const { items, failedCount } = await fetchWorkItemsNextPage(
|
||||
repoArgs,
|
||||
githubPerRepoPageLimit,
|
||||
githubPageSize,
|
||||
@@ -6176,6 +6176,22 @@ export default function TaskPage(): React.JSX.Element {
|
||||
return
|
||||
}
|
||||
if (items.length === 0) {
|
||||
// Why: staying on the current page without saying so reads as a dead
|
||||
// click (#11485). Empty with no thrown fetch means the target is past
|
||||
// GitHub's reachable search window, so stop advertising it; thrown
|
||||
// fetches may be transient, so keep the page count.
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.TaskPage.loadPageUnreachable',
|
||||
'Page {{value0}} could not be loaded from GitHub.',
|
||||
{ value0: String(target + 1) }
|
||||
)
|
||||
)
|
||||
if (failedCount === 0) {
|
||||
setCountedTotalPages((previous) =>
|
||||
previous !== null && previous > target ? target : previous
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
setPages((previous) => {
|
||||
|
||||
@@ -1889,7 +1889,8 @@
|
||||
"75a38d7df8": "GitHub data is temporarily unavailable. Its API may be down, rate-limited, or unreachable. Please try again shortly.",
|
||||
"noGithubSourceDetected": "No GitHub source detected for",
|
||||
"noGithubSourceDetectedHint": "it may have no GitHub remote, or the source could not be resolved.",
|
||||
"jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again."
|
||||
"jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again.",
|
||||
"loadPageUnreachable": "Page {{value0}} could not be loaded from GitHub."
|
||||
},
|
||||
"Terminal": {
|
||||
"73768427cf": "Close",
|
||||
|
||||
@@ -6727,6 +6727,24 @@ describe('createGitHubSlice.fetchWorkItems source/error envelope', () => {
|
||||
expect(result).toEqual({ totalCount: 101, totalPages: 3 })
|
||||
})
|
||||
|
||||
it('caps advertised pages at the GitHub search result window', async () => {
|
||||
const store = createTestStore()
|
||||
mockApi.gh.countWorkItems.mockResolvedValueOnce(1170).mockResolvedValueOnce(1)
|
||||
|
||||
const result = await store.getState().countWorkItemsAcrossRepos(
|
||||
[
|
||||
{ repoId: 'large-repo', path: '/local/large' },
|
||||
{ repoId: 'small-repo', path: '/local/small' }
|
||||
],
|
||||
'is:issue',
|
||||
30
|
||||
)
|
||||
|
||||
// 1170 results → 39 naive pages, but the Search API 422s past its
|
||||
// 1000-result window; only floor(1000 / 30) = 33 pages are reachable (#11485).
|
||||
expect(result).toEqual({ totalCount: 1171, totalPages: 33 })
|
||||
})
|
||||
|
||||
it('rejects oversized work-item queries before cache keys or provider calls', async () => {
|
||||
const store = createTestStore()
|
||||
const secret = 'github-work-items-secret'
|
||||
|
||||
@@ -655,6 +655,8 @@ const CHECKS_CACHE_TTL = 60_000 // 1 minute — checks change more frequently
|
||||
const EMPTY_CHECKS_CACHE_TTL = 10_000
|
||||
// Why: the work-item list is a browse surface, not a source of truth, so 60s staleness is fine (SWR keeps it current).
|
||||
const WORK_ITEMS_CACHE_TTL = 60_000
|
||||
// GitHub's Search API serves at most the first 1000 results; deeper requests 422.
|
||||
const GITHUB_SEARCH_RESULT_WINDOW = 1000
|
||||
// Why: long-lived (matches repos.ts) so the user has time to read + act on persist failures before the toast vanishes.
|
||||
const ERROR_TOAST_DURATION = 60_000
|
||||
|
||||
@@ -2839,6 +2841,10 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
||||
return { totalCount: 0, totalPages: 0 }
|
||||
}
|
||||
const normalizedLimit = Math.max(1, Math.floor(perRepoLimit))
|
||||
// Why: GitHub's Search API rejects requests past its 1000-result window with
|
||||
// HTTP 422, so pages whose range crosses it are unreachable — advertising
|
||||
// them yields page clicks that can never navigate.
|
||||
const maxReachablePages = Math.max(1, Math.floor(GITHUB_SEARCH_RESULT_WINDOW / normalizedLimit))
|
||||
const counts = await Promise.all(
|
||||
repos.map(async (r) => {
|
||||
// Why: same stampede cap as item-fetch — without a slot a 90-repo selection fires 90 concurrent count IPCs before the main-side rate-limit guard sees the first 403.
|
||||
@@ -2870,7 +2876,8 @@ export const createGitHubSlice: StateCreator<AppState, [], [], GitHubSlice> = (s
|
||||
totalCount: counts.reduce((sum, count) => sum + count, 0),
|
||||
// Why: repos advance independently by page, so take the max across repos — a sum/page-width undercounts when one repo owns most results.
|
||||
totalPages: counts.reduce(
|
||||
(maxPages, count) => Math.max(maxPages, Math.ceil(count / normalizedLimit)),
|
||||
(maxPages, count) =>
|
||||
Math.max(maxPages, Math.min(Math.ceil(count / normalizedLimit), maxReachablePages)),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user