diff --git a/build.gradle.kts b/build.gradle.kts index e69248452e..a663a89845 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -140,6 +140,7 @@ dependencies { testImplementation("net.java.dev.jna:jna:5.10.0") testImplementation("org.awaitility:awaitility-kotlin:4.2.1") testImplementation("org.mockito:mockito-core:5.12.0") + testImplementation("io.mockk:mockk:1.13.13") testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:2.0.0") testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1") diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 65f3e23a5d..5820cc64d4 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -3,7 +3,6 @@ package com.sourcegraph.cody.agent import com.intellij.ide.plugins.PluginManagerCore import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.project.Project import com.intellij.openapi.util.SystemInfoRt import com.intellij.util.net.HttpConfigurable @@ -92,7 +91,6 @@ private constructor( companion object { private val logger = Logger.getInstance(CodyAgent::class.java) - private val PLUGIN_ID = PluginId.getId("com.sourcegraph.jetbrains") private const val DEFAULT_AGENT_DEBUG_PORT = 3113 // Also defined in agent/src/cli/jsonrpc.ts @JvmField val executorService: ExecutorService = Executors.newCachedThreadPool() @@ -345,7 +343,7 @@ private constructor( return if (fromProperty.isNotEmpty()) { Paths.get(fromProperty) } else { - PluginManagerCore.getPlugin(PLUGIN_ID)?.pluginPath + PluginManagerCore.getPlugin(ConfigUtil.getPluginId())?.pluginPath } } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt index f882c485b9..41cc186393 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt @@ -171,4 +171,6 @@ interface _LegacyAgentServer { @JsonRequest("testing/requestErrors") fun testingRequestErrors(): CompletableFuture> + + @JsonRequest("extension/reset") fun extension_reset(params: Null?): CompletableFuture } diff --git a/src/main/kotlin/com/sourcegraph/cody/config/CodyAuthenticationManager.kt b/src/main/kotlin/com/sourcegraph/cody/config/CodyAuthenticationManager.kt index 7b86b87751..bd75be4b83 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/CodyAuthenticationManager.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/CodyAuthenticationManager.kt @@ -273,6 +273,10 @@ class CodyAuthenticationManager : fun showInvalidAccessTokenError() = getIsTokenInvalid().getNow(null) == true + fun removeAll() { + accountManager.accounts.forEach { accountManager.removeAccount(it) } + } + override fun dispose() { scheduler.shutdown() } diff --git a/src/main/kotlin/com/sourcegraph/cody/config/ui/CheckUpdatesTask.kt b/src/main/kotlin/com/sourcegraph/cody/config/ui/CheckUpdatesTask.kt index 8845c39478..fc26273798 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/ui/CheckUpdatesTask.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/ui/CheckUpdatesTask.kt @@ -7,7 +7,6 @@ import com.intellij.notification.NotificationAction import com.intellij.notification.NotificationType import com.intellij.notification.impl.NotificationFullContent import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task @@ -16,13 +15,15 @@ import com.intellij.openapi.updateSettings.impl.PluginDownloader import com.intellij.openapi.updateSettings.impl.UpdateChecker import com.intellij.openapi.util.BuildNumber import com.sourcegraph.common.NotificationGroups +import com.sourcegraph.config.ConfigUtil import java.lang.reflect.InvocationTargetException class CheckUpdatesTask(project: Project) : Task.Backgroundable(project, "Checking for Sourcegraph Cody + Code Search update...", false) { override fun run(indicator: ProgressIndicator) { - val availableUpdate = getAvailablePluginDownloaders(indicator).find { it.id == pluginId } + val availableUpdate = + getAvailablePluginDownloaders(indicator).find { it.id == ConfigUtil.getPluginId() } if (availableUpdate != null) { CustomPluginRepositoryService.getInstance().clearCache() notifyAboutTheUpdate(project) @@ -31,7 +32,6 @@ class CheckUpdatesTask(project: Project) : companion object { private val logger = Logger.getInstance(CheckUpdatesTask::class.java) - private val pluginId = PluginId.getId("com.sourcegraph.jetbrains") fun getAvailablePluginDownloaders(indicator: ProgressIndicator): Collection { try { diff --git a/src/main/kotlin/com/sourcegraph/cody/initialization/UninstallListener.kt b/src/main/kotlin/com/sourcegraph/cody/initialization/UninstallListener.kt new file mode 100644 index 0000000000..1d63357f84 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/initialization/UninstallListener.kt @@ -0,0 +1,45 @@ +package com.sourcegraph.cody.initialization + +import com.intellij.ide.plugins.IdeaPluginDescriptor +import com.intellij.ide.plugins.PluginInstaller +import com.intellij.ide.plugins.PluginStateListener +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.StartupActivity +import com.sourcegraph.cody.agent.CodyAgentService +import com.sourcegraph.cody.agent.protocol.BillingCategory +import com.sourcegraph.cody.agent.protocol.BillingMetadata +import com.sourcegraph.cody.agent.protocol.BillingProduct +import com.sourcegraph.cody.agent.protocol.TelemetryEventParameters +import com.sourcegraph.cody.config.CodyAuthenticationManager +import com.sourcegraph.cody.telemetry.TelemetryV2 +import com.sourcegraph.config.ConfigUtil +import java.util.concurrent.TimeUnit + +class UninstallListener : StartupActivity { + override fun runActivity(project: Project) { + PluginInstaller.addStateListener( + object : PluginStateListener { + override fun uninstall(descriptor: IdeaPluginDescriptor) { + // Only run for this plugin + if (descriptor.pluginId != ConfigUtil.getPluginId()) { + return + } + val authManager = CodyAuthenticationManager.getInstance() + authManager.setActiveAccount(null) + authManager.removeAll() + TelemetryV2.sendTelemetryEvent( + project, + "cody.extension", + "uninstalled", + TelemetryEventParameters( + billingMetadata = + BillingMetadata(BillingProduct.CODY, BillingCategory.BILLABLE))) + CodyAgentService.withAgent(project) { + it.server.extension_reset(null).get(20, TimeUnit.SECONDS) + } + } + + override fun install(descriptor: IdeaPluginDescriptor) {} + }) + } +} diff --git a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt index 93abe6a557..3de60ada2a 100644 --- a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt +++ b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt @@ -29,6 +29,7 @@ object ConfigUtil { const val CODY_DISPLAY_NAME = "Cody" const val CODE_SEARCH_DISPLAY_NAME = "Code Search" const val SOURCEGRAPH_DISPLAY_NAME = "Sourcegraph" + const val PLUGIN_ID = "com.sourcegraph.jetbrains" private const val FEATURE_FLAGS_ENV_VAR = "CODY_JETBRAINS_FEATURES" private val logger = Logger.getInstance(ConfigUtil::class.java) @@ -149,11 +150,13 @@ object ConfigUtil { return settingsProperties + additionalProperties } + @JvmStatic @Contract(pure = true) fun getPluginId(): PluginId = PluginId.getId(PLUGIN_ID) + @JvmStatic @Contract(pure = true) fun getPluginVersion(): String { // Internal version - val plugin = PluginManagerCore.getPlugin(PluginId.getId("com.sourcegraph.jetbrains")) + val plugin = PluginManagerCore.getPlugin(getPluginId()) return if (plugin != null) plugin.version else "unknown" } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index eac6850783..06ae3a4a6c 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -66,6 +66,8 @@ serviceImplementation="com.sourcegraph.find.FindService"/> + (relaxed = true) + + override fun setUp() { + super.setUp() + + // setup mock objects + mockkObject(CodyAuthenticationManager) + every { CodyAuthenticationManager.getInstance() } returns authManager + mockkObject(TelemetryV2) + every { TelemetryV2.sendTelemetryEvent(any(), any(), any()) } returns Unit + } + + private fun getPlugin() = + PluginManagerCore.findPlugin(ConfigUtil.getPluginId()) ?: throw Exception("Plugin not found") + + fun `test plugin uninstall cleans up resources`() { + // Execute uninstall + uninstallListener.runActivity(project) + val plugin = getPlugin() + PluginInstaller.prepareToUninstall(plugin) + verify { + authManager.setActiveAccount(null) + authManager.removeAll() + TelemetryV2.sendTelemetryEvent(any(), "cody.extension", "uninstalled", any()) + } + } + + fun `test plugin uninstall does nothing for unrelated plugins`() { + // Execute uninstall + val plugin = getPlugin() + uninstallListener.runActivity(project) + + // Now mock out config util so that it returns a different plugin id + // so that the UninstallListener thinks it's a different plugin + mockkStatic(ConfigUtil::getPluginId) + every { ConfigUtil.getPluginId() } returns PluginId.getId("com.sourcegraph.cody.test") + + PluginInstaller.prepareToUninstall(plugin) + // Remove the static method mock so that it doesn't interfere with other tests + unmockkStatic(ConfigUtil::getPluginId) + + // Verify that the uninstall listener didn't do anything + verify(exactly = 0) { + authManager.setActiveAccount(null) + authManager.removeAll() + TelemetryV2.sendTelemetryEvent(any(), "cody.extension", "uninstalled", any()) + } + } +}