From bdf9131584d8315e9880b403e6a178b1a02a9c1b Mon Sep 17 00:00:00 2001 From: Richard Li Date: Thu, 24 Apr 2025 14:30:03 -0700 Subject: [PATCH 1/2] fix(amazonq): don't reset profile config if validation fails to list profiles --- ...-31222d0f-5ff2-4495-9bf2-e354eabc82c7.json | 4 ++ .../QRegionProfileManagerTest.kt | 38 ++++++++++++------- .../amazonq/profile/QRegionProfileManager.kt | 16 ++++++-- 3 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 .changes/next-release/bugfix-31222d0f-5ff2-4495-9bf2-e354eabc82c7.json diff --git a/.changes/next-release/bugfix-31222d0f-5ff2-4495-9bf2-e354eabc82c7.json b/.changes/next-release/bugfix-31222d0f-5ff2-4495-9bf2-e354eabc82c7.json new file mode 100644 index 00000000000..de8cc0d43e2 --- /dev/null +++ b/.changes/next-release/bugfix-31222d0f-5ff2-4495-9bf2-e354eabc82c7.json @@ -0,0 +1,4 @@ +{ + "type" : "bugfix", + "description" : "Amazon Q: Fix issue where context menu items are not available after re-opening projects or restarting the IDE" +} \ No newline at end of file diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt index 628ab8e36c9..02b90162e11 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt @@ -15,6 +15,7 @@ import org.junit.Rule import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.stub @@ -191,20 +192,6 @@ class QRegionProfileManagerTest { @Test fun `validateProfile should cross validate selected profile with latest API response for current project and remove it if its not longer accessible`() { - val client = clientRule.create() - val mockResponse: SdkIterable = SdkIterable { - listOf( - Profile.builder().profileName("foo").arn("foo-arn-v2").build(), - Profile.builder().profileName("bar").arn("bar-arn").build(), - ).toMutableList().iterator() - } - val iterable: ListAvailableProfilesIterable = mock { - on { it.profiles() } doReturn mockResponse - } - client.stub { - onGeneric { listAvailableProfilesPaginator(any>()) } doReturn iterable - } - val activeConn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) ?: fail("connection shouldn't be null") val anotherConn = authRule.createConnection(ManagedSsoProfile(ssoRegion = "us-east-1", startUrl = "anotherUrl", scopes = Q_SCOPES)) @@ -214,6 +201,11 @@ class QRegionProfileManagerTest { this.connectionIdToActiveProfile[activeConn.id] = fooProfile this.connectionIdToActiveProfile[anotherConn.id] = barProfile } + resourceCache.addEntry(activeConn.getConnectionSettings(), QProfileResources.LIST_REGION_PROFILES, listOf( + QRegionProfile("foo", "foo-arn-v2"), + QRegionProfile("bar", "bar-arn"), + )) + sut.loadState(state) assertThat(sut.activeProfile(project)).isEqualTo(fooProfile) @@ -222,6 +214,24 @@ class QRegionProfileManagerTest { assertThat(sut.state.connectionIdToActiveProfile).isEqualTo(mapOf(anotherConn.id to barProfile)) } + @Test + fun `validateProfile does not clear profile if profiles cannot be listed`() { + val activeConn = + ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) ?: fail("connection shouldn't be null") + val anotherConn = authRule.createConnection(ManagedSsoProfile(ssoRegion = "us-east-1", startUrl = "anotherUrl", scopes = Q_SCOPES)) + val fooProfile = QRegionProfile("foo", "foo-arn") + val barProfile = QRegionProfile("bar", "bar-arn") + val state = QProfileState().apply { + this.connectionIdToActiveProfile[activeConn.id] = fooProfile + this.connectionIdToActiveProfile[anotherConn.id] = barProfile + } + sut.loadState(state) + assertThat(sut.activeProfile(project)).isEqualTo(fooProfile) + + sut.validateProfile(project) + assertThat(sut.activeProfile(project)).isEqualTo(fooProfile) + } + @Test fun `clientSettings should return the region Q profile specify`() { MockClientManager.useRealImplementations(disposableRule.disposable) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt index b3b242c363d..4c4875046ee 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt @@ -19,7 +19,6 @@ import software.amazon.awssdk.core.SdkClient import software.aws.toolkits.core.TokenConnectionSettings import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger -import software.aws.toolkits.core.utils.tryOrNull import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.core.AwsClientManager import software.aws.toolkits.jetbrains.core.AwsResourceCache @@ -59,16 +58,24 @@ class QRegionProfileManager : PersistentStateComponent, Disposabl ) } - // should be call on project startup to validate if profile is still active + /** + * Called on project startup to validate if selected profile is still active + */ + @Deprecated("This is a giant hack and we are not handling all the cases") @RequiresBackgroundThread fun validateProfile(project: Project) { val conn = getIdcConnectionOrNull(project) val selected = activeProfile(project) ?: return - val profiles = tryOrNull { + val profiles = try { listRegionProfiles(project) + } catch (_: Exception) { + // if we can't list profiles assume it is valid + LOG.warn { "Continuing with $selected since listAvailableProfiles failed" } + return } - if (profiles == null || profiles.none { it.arn == selected.arn }) { + // succeeded in listing profiles, but none match selected + if (profiles?.none { it.arn == selected.arn } == true) { invalidateProfile(selected.arn) switchProfile(project, null, intent = QProfileSwitchIntent.Reload) Telemetry.amazonq.profileState.use { span -> @@ -83,6 +90,7 @@ class QRegionProfileManager : PersistentStateComponent, Disposabl fun listRegionProfiles(project: Project): List? { val connection = getIdcConnectionOrNull(project) ?: return null + return try { val connectionSettings = connection.getConnectionSettings() val mappedProfiles = AwsResourceCache.getInstance().getResourceNow( From 2bfa57d17ed963a392906f07871d207cc7aadc0e Mon Sep 17 00:00:00 2001 From: Richard Li Date: Thu, 24 Apr 2025 15:48:22 -0700 Subject: [PATCH 2/2] lint --- .../codewhisperer/QRegionProfileManagerTest.kt | 13 ++++++++----- .../amazonq/profile/QRegionProfileManager.kt | 16 +++++++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt index 02b90162e11..53f5d3170f2 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt @@ -15,7 +15,6 @@ import org.junit.Rule import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.doReturn -import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.stub @@ -201,10 +200,14 @@ class QRegionProfileManagerTest { this.connectionIdToActiveProfile[activeConn.id] = fooProfile this.connectionIdToActiveProfile[anotherConn.id] = barProfile } - resourceCache.addEntry(activeConn.getConnectionSettings(), QProfileResources.LIST_REGION_PROFILES, listOf( - QRegionProfile("foo", "foo-arn-v2"), - QRegionProfile("bar", "bar-arn"), - )) + resourceCache.addEntry( + activeConn.getConnectionSettings(), + QProfileResources.LIST_REGION_PROFILES, + listOf( + QRegionProfile("foo", "foo-arn-v2"), + QRegionProfile("bar", "bar-arn"), + ) + ) sut.loadState(state) assertThat(sut.activeProfile(project)).isEqualTo(fooProfile) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt index 4c4875046ee..5d052877477 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt @@ -16,6 +16,7 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.xmlb.annotations.MapAnnotation import com.intellij.util.xmlb.annotations.Property import software.amazon.awssdk.core.SdkClient +import software.amazon.awssdk.services.codewhispererruntime.model.AccessDeniedException import software.aws.toolkits.core.TokenConnectionSettings import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger @@ -68,14 +69,19 @@ class QRegionProfileManager : PersistentStateComponent, Disposabl val selected = activeProfile(project) ?: return val profiles = try { listRegionProfiles(project) - } catch (_: Exception) { - // if we can't list profiles assume it is valid - LOG.warn { "Continuing with $selected since listAvailableProfiles failed" } - return + } catch (e: Exception) { + if (e is AccessDeniedException) { + null + } else { + // if we can't list profiles assume it is valid + LOG.warn { "Continuing with $selected since listAvailableProfiles failed" } + return + } } // succeeded in listing profiles, but none match selected - if (profiles?.none { it.arn == selected.arn } == true) { + // profiles should be null if access denied or connection is not IdC + if (profiles == null || profiles.none { it.arn == selected.arn }) { invalidateProfile(selected.arn) switchProfile(project, null, intent = QProfileSwitchIntent.Reload) Telemetry.amazonq.profileState.use { span ->