Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions src/hooks/ai-generation/graphComposer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,32 @@ describe('graphComposer', () => {
expect(warnSpy).toHaveBeenCalled();
});

it('maps unknown errors to a stable message', () => {
expect(toErrorMessage(new Error('boom'))).toBe('boom');
expect(toErrorMessage('bad')).toBe('bad');
expect(toErrorMessage('')).toBe('An unexpected error occurred. Please try again.');
it('maps a raw model-not-found response to a user-friendly message', () => {
const rawError =
'models/gemini-3-pro is not found for API version v1beta, or is not supported for generateContent. Service: generativelanguage.googleapis.com (404)';
const message = toErrorMessage(new Error(rawError));

expect(message).toBe(
'The selected AI model is unavailable or no longer supported. Please select another model and try again.'
);
expect(message).not.toContain('generativelanguage.googleapis.com');
});

it.each([
['INVALID API KEY', 'Invalid or missing API key. Please check your AI settings.'],
['Request failed with status 403', 'Access forbidden. Please check your API key permissions.'],
['Quota exceeded', 'Rate limit exceeded. Please wait a moment and try again.'],
['Provider returned 503', 'AI provider server error. Please try again later.'],
['Failed to fetch', 'Network error. Please check your internet connection.'],
])('maps %s without exposing provider details', (rawError, expectedMessage) => {
expect(toErrorMessage(new Error(rawError))).toBe(expectedMessage);
});

it('does not expose unknown error details', () => {
const expectedMessage = 'The AI request failed. Please try again or choose a different model.';

expect(toErrorMessage(new Error('upstream service secret detail'))).toBe(expectedMessage);
expect(toErrorMessage('raw provider response')).toBe(expectedMessage);
expect(toErrorMessage('')).toBe(expectedMessage);
});
});
50 changes: 30 additions & 20 deletions src/hooks/ai-generation/graphComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,46 +122,56 @@ export function toFinalEdges(
export function toErrorMessage(error: unknown): string {
if (error instanceof Error) {
const message = error.message;
const normalizedMessage = message.toLowerCase();

if (
message.includes('401') ||
message.includes('invalid api key') ||
message.includes('API key')
normalizedMessage.includes('model') &&
(normalizedMessage.includes('404') ||
normalizedMessage.includes('not found') ||
normalizedMessage.includes('unavailable') ||
normalizedMessage.includes('not supported'))
) {
return 'The selected AI model is unavailable or no longer supported. Please select another model and try again.';
}

if (
normalizedMessage.includes('401') ||
normalizedMessage.includes('invalid api key') ||
normalizedMessage.includes('api key')
) {
return 'Invalid or missing API key. Please check your AI settings.';
}
if (message.includes('403') || message.includes('Forbidden')) {
if (normalizedMessage.includes('403') || normalizedMessage.includes('forbidden')) {
return 'Access forbidden. Please check your API key permissions.';
}
if (message.includes('429') || message.includes('rate limit') || message.includes('quota')) {
if (
normalizedMessage.includes('429') ||
normalizedMessage.includes('rate limit') ||
normalizedMessage.includes('quota')
) {
return 'Rate limit exceeded. Please wait a moment and try again.';
}
if (message.includes('500') || message.includes('internal server error')) {
if (
/\b5\d{2}\b/.test(normalizedMessage) ||
normalizedMessage.includes('internal server error')
) {
return 'AI provider server error. Please try again later.';
}
if (
message.includes('network') ||
message.includes('fetch') ||
message.includes('Failed to fetch')
normalizedMessage.includes('network') ||
normalizedMessage.includes('fetch')
) {
return 'Network error. Please check your internet connection.';
}
if (message.includes('timeout') || message.includes('timed out')) {
if (normalizedMessage.includes('timeout') || normalizedMessage.includes('timed out')) {
return 'Request timed out. Please try again.';
}
if (message.includes('cursor') || message.includes('parse')) {
if (normalizedMessage.includes('cursor') || normalizedMessage.includes('parse')) {
return 'Failed to parse AI response. Please try a different prompt.';
}
if (message.includes('context length') || message.includes('token')) {
if (normalizedMessage.includes('context length') || normalizedMessage.includes('token')) {
return 'Prompt too long. Try simplifying your request.';
}

if (message) {
return message;
}
}
if (typeof error === 'string' && error) {
return error;
}
return 'An unexpected error occurred. Please try again.';
return 'The AI request failed. Please try again or choose a different model.';
}