diff --git a/gitnexus-web/src/components/RepoAnalyzer.tsx b/gitnexus-web/src/components/RepoAnalyzer.tsx index 1aca3a64a..9d44cdb91 100644 --- a/gitnexus-web/src/components/RepoAnalyzer.tsx +++ b/gitnexus-web/src/components/RepoAnalyzer.tsx @@ -29,6 +29,7 @@ import { import { AnalyzeProgress } from './AnalyzeProgress'; import { filterRepoFiles } from '@/lib/upload-filter'; import { useTranslation } from 'react-i18next'; +import { formatBackendError } from '../i18n/error-messages'; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -349,7 +350,7 @@ export const RepoAnalyzer = ({ variant, onComplete, onCancel }: RepoAnalyzerProp } catch (err) { // Unmount aborts the controller, so this also covers the unmounted case. if (controller.signal.aborted) return; - setValidationError(err instanceof Error ? err.message : t('errors:startAnalysisFailed')); + setValidationError(formatBackendError(err, t)); setPhase('error'); } }; @@ -430,7 +431,7 @@ export const RepoAnalyzer = ({ variant, onComplete, onCancel }: RepoAnalyzerProp // by the server's job timeout and terminal-job TTL sweep. if (controller.signal.aborted) return; setUploading(false); - setValidationError(err instanceof Error ? err.message : t('errors:startAnalysisFailed')); + setValidationError(formatBackendError(err, t)); setPhase('error'); } }; diff --git a/gitnexus-web/test/unit/repo-analyzer-complete-identity.test.tsx b/gitnexus-web/test/unit/repo-analyzer-complete-identity.test.tsx index ce0089c87..24e486b70 100644 --- a/gitnexus-web/test/unit/repo-analyzer-complete-identity.test.tsx +++ b/gitnexus-web/test/unit/repo-analyzer-complete-identity.test.tsx @@ -18,6 +18,17 @@ import { } from '../../src/services/backend-client'; vi.mock('../../src/services/backend-client', () => ({ + BackendError: class BackendError extends Error { + constructor( + message: string, + public readonly status: number, + public readonly code: string, + public readonly retryAfterMs?: number, + ) { + super(message); + this.name = 'BackendError'; + } + }, startAnalyze: vi.fn(), cancelAnalyze: vi.fn(), streamAnalyzeProgress: vi.fn(), diff --git a/gitnexus-web/test/unit/repo-analyzer-upload-race.test.tsx b/gitnexus-web/test/unit/repo-analyzer-upload-race.test.tsx index 094641b93..c604a102f 100644 --- a/gitnexus-web/test/unit/repo-analyzer-upload-race.test.tsx +++ b/gitnexus-web/test/unit/repo-analyzer-upload-race.test.tsx @@ -12,6 +12,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import { RepoAnalyzer } from '../../src/components/RepoAnalyzer'; import { i18nReady } from '../../src/i18n'; import { + BackendError, cancelAnalyze, startAnalyze, streamAnalyzeProgress, @@ -19,6 +20,17 @@ import { } from '../../src/services/backend-client'; vi.mock('../../src/services/backend-client', () => ({ + BackendError: class BackendError extends Error { + constructor( + message: string, + public readonly status: number, + public readonly code: string, + public readonly retryAfterMs?: number, + ) { + super(message); + this.name = 'BackendError'; + } + }, startAnalyze: vi.fn(), cancelAnalyze: vi.fn(), streamAnalyzeProgress: vi.fn(), @@ -161,6 +173,24 @@ describe('folder upload', () => { expect(screen.getByText('upload exploded')).toBeInTheDocument(); expect(streamAnalyzeProgress).not.toHaveBeenCalled(); }); + + it('formats origin-blocked upload failures with actionable guidance', async () => { + const d = deferred(); + vi.mocked(uploadFolder).mockReturnValue(d.promise); + + startUpload(); + await act(async () => { + d.reject( + new BackendError('This endpoint is restricted to same-host origins', 403, 'origin_blocked'), + ); + }); + + expect(screen.getByText(/Open GitNexus from the server's own address/)).toBeInTheDocument(); + expect( + screen.queryByText('This endpoint is restricted to same-host origins'), + ).not.toBeInTheDocument(); + expect(streamAnalyzeProgress).not.toHaveBeenCalled(); + }); }); describe('URL analyze', () => { @@ -215,4 +245,22 @@ describe('URL analyze', () => { expect(vi.mocked(streamAnalyzeProgress).mock.calls[0][0]).toBe('job-3'); expect(cancelAnalyze).not.toHaveBeenCalled(); }); + + it('formats origin-blocked analyze failures with actionable guidance', async () => { + const d = deferred(); + vi.mocked(startAnalyze).mockReturnValue(d.promise); + + startGithubAnalyze(); + await act(async () => { + d.reject( + new BackendError('This endpoint is restricted to same-host origins', 403, 'origin_blocked'), + ); + }); + + expect(screen.getByText(/Open GitNexus from the server's own address/)).toBeInTheDocument(); + expect( + screen.queryByText('This endpoint is restricted to same-host origins'), + ).not.toBeInTheDocument(); + expect(streamAnalyzeProgress).not.toHaveBeenCalled(); + }); });