Skip to content

Commit 6a84110

Browse files
committed
Merge branch 'refactor-telemetry' of github.com:chungjac/aws-toolkit-jetbrains into refactor-telemetry
2 parents edf4331 + 2ca2cab commit 6a84110

File tree

5 files changed

+137
-16
lines changed

5 files changed

+137
-16
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "bugfix",
3+
"description" : "Amazon Q /test: Test generation fails for files outside the project"
4+
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererUTGChatManager.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ class CodeWhispererUTGChatManager(val project: Project, private val cs: Coroutin
512512
AmazonqTelemetry.utgGenerateTests(
513513
cwsprChatProgrammingLanguage = session.programmingLanguage.languageId,
514514
hasUserPromptSupplied = session.hasUserPromptSupplied,
515+
isFileInWorkspace = true,
515516
isSupportedLanguage = true,
516517
credentialStartUrl = getStartUrl(project),
517518
jobGroup = session.testGenerationJobGroupName,

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.intellij.openapi.application.ApplicationManager
1212
import com.intellij.openapi.fileEditor.FileDocumentManager
1313
import com.intellij.openapi.fileEditor.FileEditorManager
1414
import com.intellij.openapi.project.Project
15+
import com.intellij.openapi.project.guessProjectDir
1516
import com.intellij.openapi.vfs.LocalFileSystem
1617
import com.intellij.openapi.vfs.VirtualFile
1718
import com.intellij.openapi.vfs.VirtualFileManager
@@ -158,6 +159,10 @@ class CodeTestChatController(
158159
// check if IDE has active file open, yes return (fileName and filePath) else return null
159160
val project = context.project
160161
val fileInfo = checkActiveFileInIDE(project, message) ?: return
162+
val projectRoot = Path.of(
163+
project.basePath ?: project.guessProjectDir()?.path
164+
?: error("Cannot guess base directory for project ${project.name}")
165+
)
161166
session.programmingLanguage = fileInfo.fileLanguage
162167
if (session.isGeneratingTests === true) {
163168
return
@@ -185,7 +190,8 @@ class CodeTestChatController(
185190
message.tabId,
186191
false
187192
)
188-
if (isLanguageSupported(fileInfo.fileLanguage.languageId)) {
193+
val supported = fileInfo.filePath.startsWith(projectRoot.toString()) && isLanguageSupported(fileInfo.fileLanguage.languageId)
194+
if (supported) {
189195
// Send Capability card to chat
190196
codeTestChatHelper.addNewMessage(
191197
CodeTestChatMessageContent(informationCard = true, message = null, type = ChatMessageType.Answer, canBeVoted = false),
@@ -231,9 +237,15 @@ class CodeTestChatController(
231237
}
232238
.build()
233239

234-
val messageContent = "<span style=\"color: #EE9D28;\">&#9888;<b> ${fileInfo.fileLanguage.languageId} is not a " +
235-
"language I support specialized unit test generation for at the moment.</b><br></span>The languages " +
236-
"I support now are Python and Java. I can still provide examples, instructions and code suggestions."
240+
val messageContent = if (fileInfo.filePath.startsWith(projectRoot.toString())) {
241+
"<span style=\"color: #EE9D28;\">&#9888;<b> ${fileInfo.fileLanguage.languageId} is not a " +
242+
"language I support specialized unit test generation for at the moment.</b><br></span>The languages " +
243+
"I support now are Python and Java. I can still provide examples, instructions and code suggestions."
244+
} else {
245+
"<span style=\"color: #EE9D28;\">&#9888;<b> I can't generate tests for ${fileInfo.fileName}" +
246+
" because it's outside the project directory.</b><br></span> " +
247+
"I can still provide examples, instructions and code suggestions."
248+
}
237249

238250
codeTestChatHelper.addNewMessage(
239251
CodeTestChatMessageContent(
@@ -288,7 +300,8 @@ class CodeTestChatController(
288300
AmazonqTelemetry.utgGenerateTests(
289301
cwsprChatProgrammingLanguage = session.programmingLanguage.languageId,
290302
hasUserPromptSupplied = session.hasUserPromptSupplied,
291-
isSupportedLanguage = false,
303+
isFileInWorkspace = fileInfo.filePath.startsWith(projectRoot.toString()),
304+
isSupportedLanguage = isLanguageSupported(fileInfo.fileLanguage.languageId),
292305
credentialStartUrl = getStartUrl(project),
293306
result = MetricResult.Succeeded,
294307
perfClientLatency = (Instant.now().toEpochMilli() - session.startTimeOfTestGeneration),
@@ -590,6 +603,7 @@ class CodeTestChatController(
590603
AmazonqTelemetry.utgGenerateTests(
591604
cwsprChatProgrammingLanguage = session.programmingLanguage.languageId,
592605
hasUserPromptSupplied = session.hasUserPromptSupplied,
606+
isFileInWorkspace = true,
593607
isSupportedLanguage = true,
594608
credentialStartUrl = getStartUrl(project = context.project),
595609
jobGroup = session.testGenerationJobGroupName,
@@ -785,6 +799,7 @@ class CodeTestChatController(
785799
AmazonqTelemetry.utgGenerateTests(
786800
cwsprChatProgrammingLanguage = session.programmingLanguage.languageId,
787801
hasUserPromptSupplied = session.hasUserPromptSupplied,
802+
isFileInWorkspace = true,
788803
isSupportedLanguage = true,
789804
credentialStartUrl = getStartUrl(project = context.project),
790805
jobGroup = session.testGenerationJobGroupName,

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationStateUtils.kt

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,54 @@ import com.intellij.openapi.components.State
99
import com.intellij.openapi.components.Storage
1010
import com.intellij.openapi.components.service
1111
import software.aws.toolkits.core.utils.ETagProvider
12+
import java.time.Duration
13+
import java.time.Instant
14+
15+
data class DismissedNotification(
16+
var id: String = "",
17+
var dismissedAt: String = Instant.now().toEpochMilli().toString(),
18+
)
19+
20+
data class NotificationDismissalConfiguration(
21+
var dismissedNotifications: MutableSet<DismissedNotification> = mutableSetOf(),
22+
)
1223

1324
@Service
1425
@State(name = "notificationDismissals", storages = [Storage("aws.xml")])
1526
class NotificationDismissalState : PersistentStateComponent<NotificationDismissalConfiguration> {
16-
private val state = NotificationDismissalConfiguration()
27+
private var state = NotificationDismissalConfiguration()
28+
private val retentionPeriod = Duration.ofDays(60) // 2 months
1729

1830
override fun getState(): NotificationDismissalConfiguration = state
1931

2032
override fun loadState(state: NotificationDismissalConfiguration) {
21-
this.state.dismissedNotificationIds.clear()
22-
this.state.dismissedNotificationIds.addAll(state.dismissedNotificationIds)
33+
this.state = state
34+
cleanExpiredNotifications()
2335
}
2436

2537
fun isDismissed(notificationId: String): Boolean =
26-
state.dismissedNotificationIds.contains(notificationId)
38+
state.dismissedNotifications.any { it.id == notificationId }
2739

2840
fun dismissNotification(notificationId: String) {
29-
state.dismissedNotificationIds.add(notificationId)
41+
state.dismissedNotifications.add(
42+
DismissedNotification(
43+
id = notificationId
44+
)
45+
)
46+
}
47+
48+
private fun cleanExpiredNotifications() {
49+
val now = Instant.now()
50+
state.dismissedNotifications.removeAll { notification ->
51+
Duration.between(Instant.ofEpochMilli(notification.dismissedAt.toLong()), now) > retentionPeriod
52+
}
3053
}
3154

3255
companion object {
33-
fun getInstance(): NotificationDismissalState =
34-
service()
56+
fun getInstance(): NotificationDismissalState = service()
3557
}
3658
}
3759

38-
data class NotificationDismissalConfiguration(
39-
var dismissedNotificationIds: MutableSet<String> = mutableSetOf(),
40-
)
41-
4260
@Service
4361
@State(name = "notificationEtag", storages = [Storage("aws.xml")])
4462
class NotificationEtagState : PersistentStateComponent<NotificationEtagConfiguration>, ETagProvider {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.core.notifications
5+
6+
import org.junit.jupiter.api.Assertions.assertEquals
7+
import org.junit.jupiter.api.Assertions.assertFalse
8+
import org.junit.jupiter.api.Assertions.assertTrue
9+
import org.junit.jupiter.api.BeforeEach
10+
import org.junit.jupiter.api.Test
11+
import java.time.Instant
12+
import java.time.temporal.ChronoUnit
13+
14+
class NotificationDismissalStateTest {
15+
private lateinit var state: NotificationDismissalState
16+
17+
@BeforeEach
18+
fun setUp() {
19+
state = NotificationDismissalState()
20+
}
21+
22+
@Test
23+
fun `notifications less than 2 months old are not removed`() {
24+
val recentNotification = DismissedNotification(
25+
id = "recent-notification",
26+
dismissedAt = Instant.now().minus(30, ChronoUnit.DAYS).toEpochMilli().toString()
27+
)
28+
29+
state.loadState(NotificationDismissalConfiguration(mutableSetOf(recentNotification)))
30+
31+
val persistedState = state.getState()
32+
33+
assertEquals(1, persistedState.dismissedNotifications.size)
34+
assertTrue(persistedState.dismissedNotifications.any { it.id == "recent-notification" })
35+
assertTrue(state.isDismissed("recent-notification"))
36+
}
37+
38+
@Test
39+
fun `notifications older than 2 months are removed`() {
40+
val oldNotification = DismissedNotification(
41+
id = "old-notification",
42+
dismissedAt = Instant.now().minus(61, ChronoUnit.DAYS).toEpochMilli().toString()
43+
)
44+
45+
state.loadState(NotificationDismissalConfiguration(mutableSetOf(oldNotification)))
46+
47+
val persistedState = state.getState()
48+
49+
assertEquals(0, persistedState.dismissedNotifications.size)
50+
assertFalse(state.isDismissed("old-notification"))
51+
}
52+
53+
@Test
54+
fun `mixed age notifications are handled correctly`() {
55+
val recentNotification = DismissedNotification(
56+
id = "recent-notification",
57+
dismissedAt = Instant.now().minus(30, ChronoUnit.DAYS).toEpochMilli().toString()
58+
)
59+
val oldNotification = DismissedNotification(
60+
id = "old-notification",
61+
dismissedAt = Instant.now().minus(61, ChronoUnit.DAYS).toEpochMilli().toString()
62+
)
63+
64+
state.loadState(
65+
NotificationDismissalConfiguration(
66+
mutableSetOf(recentNotification, oldNotification)
67+
)
68+
)
69+
70+
val persistedState = state.getState()
71+
72+
assertEquals(1, persistedState.dismissedNotifications.size)
73+
assertTrue(state.isDismissed("recent-notification"))
74+
assertFalse(state.isDismissed("old-notification"))
75+
}
76+
77+
@Test
78+
fun `dismissing new notification retains it`() {
79+
state.dismissNotification("new-notification")
80+
81+
assertTrue(state.isDismissed("new-notification"))
82+
}
83+
}

0 commit comments

Comments
 (0)