diff --git a/src/renderer/src/store/slices/github.test.ts b/src/renderer/src/store/slices/github.test.ts index abda4867324..62ac741f85d 100644 --- a/src/renderer/src/store/slices/github.test.ts +++ b/src/renderer/src/store/slices/github.test.ts @@ -6322,6 +6322,32 @@ describe('createGitHubSlice.fetchWorkItems source/error envelope', () => { } }) + it('ignores an ineligible SSH repo when every GitHub source is unavailable', async () => { + const store = createTestStore() + const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + mockApi.gh.listWorkItems + .mockRejectedValueOnce(new Error(GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE)) + .mockRejectedValueOnce(new Error('HTTP 503: Service Unavailable')) + + try { + const result = await store.getState().fetchWorkItemsAcrossRepos( + [ + { repoId: 'ssh-repo', path: '/server/ssh-repo' }, + { repoId: 'github-repo', path: '/server/github-repo' } + ], + 24, + 100, + '' + ) + + expect(result.items).toEqual([]) + expect(result.failedCount).toBe(1) + expect(result.githubUnavailable).toBe(true) + } finally { + consoleWarn.mockRestore() + } + }) + it('keeps the partial-failure count when another GitHub repo still loads', async () => { const store = createTestStore() const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) diff --git a/src/renderer/src/store/slices/github.ts b/src/renderer/src/store/slices/github.ts index 0aec3ae22fe..5c39a5e5b23 100644 --- a/src/renderer/src/store/slices/github.ts +++ b/src/renderer/src/store/slices/github.ts @@ -2866,6 +2866,7 @@ export const createGitHubSlice: StateCreator = (s const state = get() let failedCount = 0 let unavailableFailureCount = 0 + let skippedSourceCount = 0 const perProjectResults = await Promise.all( repos.map(async (r) => { try { @@ -2881,6 +2882,7 @@ export const createGitHubSlice: StateCreator = (s // Why: must use perRepoLimit (not displayLimit) so the cache key // matches what fetchWorkItems wrote. if (isGitHubWorkItemsSshRemoteRequiredError(err)) { + skippedSourceCount += 1 return [] as GitHubWorkItem[] } const key = @@ -2911,10 +2913,12 @@ export const createGitHubSlice: StateCreator = (s const merged = sortWorkItemsByNumber(perProjectResults.flat()).slice(0, displayLimit) // Why: one repo can fail with a 5xx while another succeeds (or fails for a // permission reason). Only claim a global availability failure when every - // selected repo failed for a GitHub reachability reason; otherwise keep the - // exact count. + // eligible source failed for a GitHub reachability reason; intentionally + // skipped SSH repos are not GitHub sources for this calculation. const githubUnavailable = - failedCount > 0 && failedCount === repos.length && unavailableFailureCount === failedCount + failedCount > 0 && + failedCount === repos.length - skippedSourceCount && + unavailableFailureCount === failedCount return { items: merged, failedCount, githubUnavailable } }, diff --git a/src/shared/github-api-availability.test.ts b/src/shared/github-api-availability.test.ts index 0d5e459765e..1b104525e46 100644 --- a/src/shared/github-api-availability.test.ts +++ b/src/shared/github-api-availability.test.ts @@ -18,6 +18,8 @@ describe('classifyGitHubUnavailable', () => { 'request to https://api.github.com failed, reason: getaddrinfo ENOTFOUND api.github.com', 'dial tcp: lookup api.github.com: no such host', 'connect ETIMEDOUT 140.82.112.5:443', + 'TimeoutError: request aborted', + 'NetworkError when attempting to fetch resource', 'fetch failed', 'socket hang up', 'could not resolve host: api.github.com', diff --git a/src/shared/github-api-availability.ts b/src/shared/github-api-availability.ts index 7af7d1df1d8..b44b1ea4d47 100644 --- a/src/shared/github-api-availability.ts +++ b/src/shared/github-api-availability.ts @@ -23,7 +23,7 @@ const SERVER_ERROR_PATTERN = // both Node (ENOTFOUND/ECONNRESET) and the gh Go client ("dial tcp", "i/o // timeout", "no such host") shapes. const NETWORK_PATTERN = - /\btimeout\b|\btimed out\b|\bno such host\b|could not resolve host|could not resolve to a host|\bnetwork\b|\bconnection (?:refused|reset)\b|\bdial tcp\b|\bi\/o timeout\b|\bfetch failed\b|\bsocket hang up\b|ENOTFOUND|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH/i + /timeout|\btimed out\b|\bno such host\b|could not resolve host|could not resolve to a host|network|\bconnection (?:refused|reset)\b|\bdial tcp\b|\bi\/o timeout\b|\bfetch failed\b|\bsocket hang up\b|ENOTFOUND|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH/i /** * Classify a gh/Octokit error message as a GitHub-reachability problem, or