diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts index 8cbb66a7916..831d66fb2f8 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts @@ -214,14 +214,15 @@ export class FeatureDevController { ) let defaultMessage - const isDenyListedError = denyListedErrors.some((err) => errorMessage.includes(err)) + const isDenyListedError = denyListedErrors.some((denyListedError) => err.message.includes(denyListedError)) - switch (err.code) { - case ContentLengthError.errorName: + switch (err.constructor.name) { + case ContentLengthError.name: this.messenger.sendAnswer({ type: 'answer', tabID: message.tabID, message: err.message + messageWithConversationId(session?.conversationIdUnsafe), + canBeVoted: true, }) this.messenger.sendAnswer({ type: 'system-prompt', @@ -235,14 +236,14 @@ export class FeatureDevController { ], }) break - case MonthlyConversationLimitError.errorName: + case MonthlyConversationLimitError.name: this.messenger.sendMonthlyLimitError(message.tabID) break - case FeatureDevServiceError.errorName: - case UploadCodeError.errorName: - case UserMessageNotFoundError.errorName: - case TabIdNotFoundError.errorName: - case PrepareRepoFailedError.errorName: + case FeatureDevServiceError.name: + case UploadCodeError.name: + case UserMessageNotFoundError.name: + case TabIdNotFoundError.name: + case PrepareRepoFailedError.name: this.messenger.sendErrorMessage( errorMessage, message.tabID, @@ -250,11 +251,11 @@ export class FeatureDevController { session?.conversationIdUnsafe ) break - case PromptRefusalException.errorName: - case ZipFileError.errorName: + case PromptRefusalException.name: + case ZipFileError.name: this.messenger.sendErrorMessage(errorMessage, message.tabID, 0, session?.conversationIdUnsafe, true) break - case NoChangeRequiredException.errorName: + case NoChangeRequiredException.name: this.messenger.sendAnswer({ type: 'answer', tabID: message.tabID, @@ -263,11 +264,12 @@ export class FeatureDevController { }) // Allow users to re-work the task description. return this.newTask(message) - case CodeIterationLimitError.errorName: + case CodeIterationLimitError.name: this.messenger.sendAnswer({ type: 'answer', tabID: message.tabID, message: err.message + messageWithConversationId(session?.conversationIdUnsafe), + canBeVoted: true, }) this.messenger.sendAnswer({ type: 'system-prompt', @@ -282,7 +284,7 @@ export class FeatureDevController { ], }) break - case UploadURLExpired.errorName: + case UploadURLExpired.name: this.messenger.sendAnswer({ type: 'answer', tabID: message.tabID, diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts index 78d436a7e34..cac7f6ca571 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/messenger/messenger.ts @@ -85,6 +85,7 @@ export class Messenger { type: 'answer', tabID: tabID, message: showDefaultMessage ? errorMessage : i18n('AWS.amazonq.featureDev.error.technicalDifficulties'), + canBeVoted: true, }) this.sendFeedback(tabID) return diff --git a/packages/core/src/amazonqFeatureDev/errors.ts b/packages/core/src/amazonqFeatureDev/errors.ts index b2152e73863..06e994080f3 100644 --- a/packages/core/src/amazonqFeatureDev/errors.ts +++ b/packages/core/src/amazonqFeatureDev/errors.ts @@ -17,8 +17,6 @@ export class ConversationIdNotFoundError extends ToolkitError { } export class TabIdNotFoundError extends ToolkitError { - static errorName = 'TabIdNotFoundError' - constructor() { super(i18n('AWS.amazonq.featureDev.error.tabIdNotFoundError'), { code: 'TabIdNotFound', @@ -26,12 +24,6 @@ export class TabIdNotFoundError extends ToolkitError { } } -export class PanelLoadError extends ToolkitError { - constructor() { - super(`${featureName} UI panel failed to load`, { code: 'PanelLoadFailed' }) - } -} - export class WorkspaceFolderNotFoundError extends ToolkitError { constructor() { super(i18n('AWS.amazonq.featureDev.error.workspaceFolderNotFoundError'), { @@ -41,7 +33,6 @@ export class WorkspaceFolderNotFoundError extends ToolkitError { } export class UserMessageNotFoundError extends ToolkitError { - static errorName = 'UserMessageNotFoundError' constructor() { super(i18n('AWS.amazonq.featureDev.error.userMessageNotFoundError'), { code: 'MessageNotFound', @@ -58,7 +49,6 @@ export class SelectedFolderNotInWorkspaceFolderError extends ToolkitError { } export class PromptRefusalException extends ToolkitError { - static errorName = 'PromptRefusalException' constructor() { super(i18n('AWS.amazonq.featureDev.error.promptRefusalException'), { code: 'PromptRefusalException', @@ -67,7 +57,6 @@ export class PromptRefusalException extends ToolkitError { } export class NoChangeRequiredException extends ToolkitError { - static errorName = 'NoChangeRequiredException' constructor() { super(i18n('AWS.amazonq.featureDev.error.noChangeRequiredException'), { code: 'NoChangeRequiredException', @@ -76,14 +65,12 @@ export class NoChangeRequiredException extends ToolkitError { } export class FeatureDevServiceError extends ToolkitError { - static errorName = 'FeatureDevServiceError' constructor(message: string, code: string) { super(message, { code }) } } export class PrepareRepoFailedError extends ToolkitError { - static errorName = 'PrepareRepoFailedError' constructor() { super(i18n('AWS.amazonq.featureDev.error.prepareRepoFailedError'), { code: 'PrepareRepoFailed', @@ -92,14 +79,12 @@ export class PrepareRepoFailedError extends ToolkitError { } export class UploadCodeError extends ToolkitError { - static errorName = 'UploadCodeError' constructor(statusCode: string) { super(uploadCodeError, { code: `UploadCode-${statusCode}` }) } } export class UploadURLExpired extends ToolkitError { - static errorName = 'UploadURLExpired' constructor() { super(i18n('AWS.amazonq.featureDev.error.uploadURLExpired'), { code: 'UploadURLExpired' }) } @@ -112,30 +97,26 @@ export class IllegalStateTransition extends ToolkitError { } export class ContentLengthError extends ToolkitError { - static errorName = 'ContentLengthError' constructor() { - super(i18n('AWS.amazonq.featureDev.error.contentLengthError'), { code: ContentLengthError.errorName }) + super(i18n('AWS.amazonq.featureDev.error.contentLengthError'), { code: ContentLengthError.name }) } } export class ZipFileError extends ToolkitError { - static errorName = 'ZipFileError' constructor() { - super(i18n('AWS.amazonq.featureDev.error.zipFileError'), { code: ZipFileError.errorName }) + super(i18n('AWS.amazonq.featureDev.error.zipFileError'), { code: ZipFileError.name }) } } export class CodeIterationLimitError extends ToolkitError { - static errorName = 'CodeIterationLimitError' constructor() { - super(i18n('AWS.amazonq.featureDev.error.codeIterationLimitError'), { code: CodeIterationLimitError.errorName }) + super(i18n('AWS.amazonq.featureDev.error.codeIterationLimitError'), { code: CodeIterationLimitError.name }) } } export class MonthlyConversationLimitError extends ToolkitError { - static errorName = 'MonthlyConversationLimitError' constructor(message: string) { - super(message, { code: MonthlyConversationLimitError.errorName }) + super(message, { code: MonthlyConversationLimitError.name }) } } diff --git a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts index 1f72aa6f270..2cba1816ffd 100644 --- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts +++ b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts @@ -14,13 +14,27 @@ import { Session } from '../../../../amazonqFeatureDev/session/session' import { Prompter } from '../../../../shared/ui/prompter' import { assertTelemetry, toFile } from '../../../testUtil' import { + CodeIterationLimitError, + ContentLengthError, + createUserFacingErrorMessage, + FeatureDevServiceError, + MonthlyConversationLimitError, NoChangeRequiredException, + PrepareRepoFailedError, + PromptRefusalException, SelectedFolderNotInWorkspaceFolderError, + TabIdNotFoundError, + UploadCodeError, + UploadURLExpired, + UserMessageNotFoundError, + ZipFileError, } from '../../../../amazonqFeatureDev/errors' import { CodeGenState, PrepareCodeGenState } from '../../../../amazonqFeatureDev/session/sessionState' import { FeatureDevClient } from '../../../../amazonqFeatureDev/client/featureDev' import { createAmazonQUri } from '../../../../amazonq/commons/diff' import { AuthUtil } from '../../../../codewhisperer' +import { featureName, messageWithConversationId } from '../../../../amazonqFeatureDev' +import { i18n } from '../../../../shared/i18n-helper' let mockGetCodeGeneration: sinon.SinonStub describe('Controller', () => { @@ -209,7 +223,6 @@ describe('Controller', () => { }) it('accepts valid source folders under a workspace root', async () => { - const controllerSetup = await createController() sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) sinon.stub(vscode.workspace, 'getWorkspaceFolder').returns(controllerSetup.workspaceFolder) const expectedSourceRoot = path.join(controllerSetup.workspaceFolder.uri.fsPath, 'src') @@ -385,22 +398,113 @@ describe('Controller', () => { } describe('processErrorChatMessage', function () { - it('should handle NoChangeRequiredException', async function () { - const noChangeRequiredException = new NoChangeRequiredException() - sinon.stub(session, 'preloader').throws(noChangeRequiredException) + const runs = [ + { name: 'ContentLengthError', error: new ContentLengthError() }, + { + name: 'MonthlyConversationLimitError', + error: new MonthlyConversationLimitError('Service Quota Exceeded'), + }, + { + name: 'FeatureDevServiceError', + error: new FeatureDevServiceError( + i18n('AWS.amazonq.featureDev.error.codeGen.default'), + 'GuardrailsException' + ), + }, + { name: 'UploadCodeError', error: new UploadCodeError('403: Forbiden') }, + { name: 'UserMessageNotFoundError', error: new UserMessageNotFoundError() }, + { name: 'TabIdNotFoundError', error: new TabIdNotFoundError() }, + { name: 'PrepareRepoFailedError', error: new PrepareRepoFailedError() }, + { name: 'PromptRefusalException', error: new PromptRefusalException() }, + { name: 'ZipFileError', error: new ZipFileError() }, + { name: 'CodeIterationLimitError', error: new CodeIterationLimitError() }, + { name: 'UploadURLExpired', error: new UploadURLExpired() }, + { name: 'NoChangeRequiredException', error: new NoChangeRequiredException() }, + { name: 'default', error: new Error() }, + ] + + function createTestErrorMessage(message: string) { + return createUserFacingErrorMessage(`${featureName} request failed: ${message}`) + } + + async function verifyException(error: Error) { + sinon.stub(session, 'preloader').throws(error) const sendAnswerSpy = sinon.stub(controllerSetup.messenger, 'sendAnswer') + const sendErrorMessageSpy = sinon.stub(controllerSetup.messenger, 'sendErrorMessage') + const sendMonthlyLimitErrorSpy = sinon.stub(controllerSetup.messenger, 'sendMonthlyLimitError') await fireChatMessage() - assert.strictEqual( - sendAnswerSpy.calledWith({ - type: 'answer', - tabID, - message: noChangeRequiredException.message, - canBeVoted: true, - }), - true - ) + switch (error.constructor.name) { + case ContentLengthError.name: + assert.ok( + sendAnswerSpy.calledWith({ + type: 'answer', + tabID, + message: error.message + messageWithConversationId(session?.conversationIdUnsafe), + canBeVoted: true, + }) + ) + break + case MonthlyConversationLimitError.name: + assert.ok(sendMonthlyLimitErrorSpy.calledWith(tabID)) + break + case FeatureDevServiceError.name: + case UploadCodeError.name: + case UserMessageNotFoundError.name: + case TabIdNotFoundError.name: + case PrepareRepoFailedError.name: + assert.ok( + sendErrorMessageSpy.calledWith( + createTestErrorMessage(error.message), + tabID, + session?.retries, + session?.conversationIdUnsafe + ) + ) + break + case PromptRefusalException.name: + case ZipFileError.name: + assert.ok( + sendErrorMessageSpy.calledWith( + createTestErrorMessage(error.message), + tabID, + 0, + session?.conversationIdUnsafe, + true + ) + ) + break + case NoChangeRequiredException.name: + case CodeIterationLimitError.name: + case UploadURLExpired.name: + assert.ok( + sendAnswerSpy.calledWith({ + type: 'answer', + tabID, + message: error.message, + canBeVoted: true, + }) + ) + break + default: + assert.ok( + sendErrorMessageSpy.calledWith( + i18n('AWS.amazonq.featureDev.error.codeGen.default'), + tabID, + session?.retries, + session?.conversationIdUnsafe, + true + ) + ) + break + } + } + + runs.forEach((run) => { + it(`should handle ${run.name}`, async function () { + await verifyException(run.error) + }) }) }) })