diff --git a/src/main/jira/client.test.ts b/src/main/jira/client.test.ts index ab949441e..713311b8a 100644 --- a/src/main/jira/client.test.ts +++ b/src/main/jira/client.test.ts @@ -364,6 +364,13 @@ describe('Jira client credential storage', () => { }) }) + it('does not treat Jira permission failures as credential revocation', async () => { + const jira = await loadClientModule() + + expect(jira.isAuthError(new jira.JiraApiError('Unauthorized', 401))).toBe(true) + expect(jira.isAuthError(new jira.JiraApiError('Forbidden', 403))).toBe(false) + }) + it('bridges proxy environment settings before Jira connect requests', async () => { netFetchMock.mockResolvedValueOnce( new Response( diff --git a/src/main/jira/client.ts b/src/main/jira/client.ts index a0e705290..d56e32d35 100644 --- a/src/main/jira/client.ts +++ b/src/main/jira/client.ts @@ -574,5 +574,7 @@ export function clearToken(siteId: string): void { } export function isAuthError(error: unknown): boolean { - return error instanceof JiraApiError && (error.status === 401 || error.status === 403) + // Why: Jira returns 403 for project/API permission gaps even when /myself + // succeeds, so only 401 means the saved credential itself is invalid. + return error instanceof JiraApiError && error.status === 401 } diff --git a/src/renderer/src/store/slices/jira.test.ts b/src/renderer/src/store/slices/jira.test.ts index 565467e05..ebe427f44 100644 --- a/src/renderer/src/store/slices/jira.test.ts +++ b/src/renderer/src/store/slices/jira.test.ts @@ -390,4 +390,16 @@ describe('createJiraSlice credential errors', () => { expect(store.getState().jiraStatus.credentialError).toBeUndefined() }) }) + + it('keeps Jira connected when an issue read hits endpoint-level forbidden access', async () => { + const store = createTestStore() + store.setState({ + jiraStatus: { connected: true, viewer: null, selectedSiteId: 'site-1' } + }) + jiraListIssues.mockRejectedValueOnce(new Error('Forbidden')) + + await expect(store.getState().listJiraIssues('assigned', 30)).resolves.toEqual([]) + + expect(store.getState().jiraStatus.connected).toBe(true) + }) }) diff --git a/src/renderer/src/store/slices/jira.ts b/src/renderer/src/store/slices/jira.ts index 9892ee8e7..1498f312e 100644 --- a/src/renderer/src/store/slices/jira.ts +++ b/src/renderer/src/store/slices/jira.ts @@ -55,7 +55,9 @@ function evictStaleEntries( function looksLikeAuthError(error: unknown): boolean { const msg = error instanceof Error ? error.message : String(error) - return /authenticat|unauthorized|forbidden|401|403/i.test(msg) + // Why: Jira 403 commonly means endpoint/project access is denied while the + // saved token is still valid; do not flip Settings back to disconnected. + return /authenticat|unauthorized|401/i.test(msg) } type InflightJiraReadRequest = {