From 950b3eb514b24b82debdc0350f36ae8166cbc732 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sun, 2 Aug 2026 11:32:35 -0700 Subject: [PATCH] Extract GitHub search result window error pattern to shared constant Extract the 1000-result window detection pattern to a single source of truth so the classifier and consumer stay synchronized. The pattern is the only signal separating a permanently unreachable page from a transient validation failure, so drift or trimming silently demotes window 422s to generic failures and stops capping the advertised page count (#11485). --- src/main/github/gh-utils.test.ts | 10 ++++++---- src/renderer/src/components/TaskPage.tsx | 9 +++++++-- .../store/slices/github-work-items-query-bounds.ts | 1 + src/renderer/src/store/slices/github.ts | 8 ++++++-- src/shared/github-work-items-query-bounds.ts | 11 +++++++++++ 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/main/github/gh-utils.test.ts b/src/main/github/gh-utils.test.ts index 8ce81b189c4..a08580babe4 100644 --- a/src/main/github/gh-utils.test.ts +++ b/src/main/github/gh-utils.test.ts @@ -38,6 +38,7 @@ import { __resetLocalGitConfigSignatureCacheForTests, readLocalGitConfigSignature } from './local-git-config-signature' +import { GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN } from '../../shared/github-work-items-query-bounds' describe('github owner/repo resolution', () => { beforeEach(() => { @@ -797,14 +798,15 @@ describe('gh error classification', () => { }) // Why: the renderer detects the Search API 1000-result window by matching - // /first 1000 search results/i against this message (#11485) — trimming the - // raw stderr out of the validation_error copy would silently downgrade every - // window 422 to a generic failure. This pins the contract. + // GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN against this message (#11485) — + // trimming the raw stderr out of the validation_error copy, or drifting the + // pattern off GitHub's real wording, would silently downgrade every window + // 422 to a generic failure. The stderr stays verbatim so this pins both ends. it('keeps the search-window phrase in validation_error list messages', () => { const stderr = 'Command failed: gh api --hostname github.com search/issues\nValidation Failed: Only the first 1000 search results are available (HTTP 422)' const classified = classifyListIssuesError(stderr) expect(classified.type).toBe('validation_error') - expect(classified.message).toMatch(/first 1000 search results/i) + expect(classified.message).toMatch(GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN) }) }) diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 69c8051b717..5761bcfc9eb 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -3211,7 +3211,12 @@ export default function TaskPage(): React.JSX.Element { ) // Why: see buildSelectedReposKey — array-identity deps re-fire on every - // repos:changed even when the selection is unchanged. + // repos:changed even when the selection is unchanged. The context part is + // resolved as GitHub, but every provider-independent field (projectId, + // hostId, projectHostSetupId, repoId) is identical across providers, so the + // GitLab effect can key off this too — it passes no gitlabProjectRef, so its + // context carries no providerIdentity of its own. Thread a projectRef into + // that call and this key needs a GitLab-scoped part. const selectedReposKey = useMemo( () => buildSelectedReposKey(selectedRepos, (r) => getTaskPageRepoSourceContext(r, 'github')), [selectedRepos] @@ -4967,7 +4972,7 @@ export default function TaskPage(): React.JSX.Element { return () => { stale = true } - // eslint-disable-next-line react-hooks/exhaustive-deps -- selectedReposKey encodes the only selectedRepos fields read above; keying off the array ref would re-run on every parent render. + // eslint-disable-next-line react-hooks/exhaustive-deps -- selectedReposKey covers every selectedRepos field read above (see its GitHub-scoped-context note); keying off the array ref would re-run on every parent render. }, [taskSource, gitlabView, activeGitlabFilter, gitlabRefreshNonce, selectedReposKey]) // Why: Todos fetch has its own effect — different trigger (no chip filter) and data path (gl.todos is user-scoped, not repo-scoped). diff --git a/src/renderer/src/store/slices/github-work-items-query-bounds.ts b/src/renderer/src/store/slices/github-work-items-query-bounds.ts index c60529c3e57..0ac2107a8a2 100644 --- a/src/renderer/src/store/slices/github-work-items-query-bounds.ts +++ b/src/renderer/src/store/slices/github-work-items-query-bounds.ts @@ -1,4 +1,5 @@ export { + GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN, GITHUB_WORK_ITEMS_QUERY_MAX_BYTES, isGitHubWorkItemsQueryTooLarge } from '../../../../shared/github-work-items-query-bounds' diff --git a/src/renderer/src/store/slices/github.ts b/src/renderer/src/store/slices/github.ts index 99a17723044..5ebe76f2c87 100644 --- a/src/renderer/src/store/slices/github.ts +++ b/src/renderer/src/store/slices/github.ts @@ -53,7 +53,10 @@ import { rightSidebarShowsPullRequestData } from '@/lib/right-sidebar-visibility import { hostedReviewInfoFromGitHubPRInfo } from '../../../../shared/hosted-review-github' import { getHostedReviewCacheKey, linkedReviewHintKey } from './hosted-review-cache-identity' import { getGitHubPRCacheKey, getGitHubRepoCacheKey } from './github-cache-key' -import { isGitHubWorkItemsQueryTooLarge } from './github-work-items-query-bounds' +import { + GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN, + isGitHubWorkItemsQueryTooLarge +} from './github-work-items-query-bounds' import { classifyGitHubUnavailable } from '../../../../shared/github-api-availability' import { isMacAppDataPath } from '@/lib/passive-macos-app-data-access' import { translate } from '@/i18n/i18n' @@ -2824,7 +2827,8 @@ export const createGitHubSlice: StateCreator = (s // Why: only the 1000-result-window 422 may drive the unreachable // clamp; demote other validation errors so they read as failures. errorTypes.push( - type === 'validation_error' && !/first 1000 search results/i.test(message) + type === 'validation_error' && + !GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN.test(message) ? 'unknown' : type ) diff --git a/src/shared/github-work-items-query-bounds.ts b/src/shared/github-work-items-query-bounds.ts index 4d3ae6e6d49..48f90bceb38 100644 --- a/src/shared/github-work-items-query-bounds.ts +++ b/src/shared/github-work-items-query-bounds.ts @@ -2,6 +2,17 @@ import { isClipboardTextByteLengthOverLimit } from './clipboard-text' export const GITHUB_WORK_ITEMS_QUERY_MAX_BYTES = 8 * 1024 +/** + * GitHub's Search API only pages through the first 1000 matches; beyond that it + * 422s with "Only the first 1000 search results are available". Matching that + * free-text wording is the ONLY signal separating a permanently unreachable page + * from a transient failure (#11485), so the phrase is pinned here rather than + * inlined: if GitHub rewords it, window 422s silently demote to generic + * failures and the advertised page count stops being capped. + * `gh-utils.test.ts` asserts the classified message still carries this phrase. + */ +export const GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN = /first 1000 search results/i + export function isGitHubWorkItemsQueryTooLarge( query: string, maxBytes = GITHUB_WORK_ITEMS_QUERY_MAX_BYTES