diff --git a/platform/platform-api/resources/messages/IdeBundle.properties b/platform/platform-api/resources/messages/IdeBundle.properties index ab35cf1d43d4..48917154cfd4 100644 --- a/platform/platform-api/resources/messages/IdeBundle.properties +++ b/platform/platform-api/resources/messages/IdeBundle.properties @@ -2738,6 +2738,7 @@ gdpr.ai.data.collection.consent.additional.notice.2=\ Even if this setting is disabled, the AI Assistant plugin will send the data essential for this feature to large language model providers \ and models hosted on JetBrains servers. If you work on a project where you don't want to share your data, you can disable the plugin. gdpr.consent.externally.disabled.warning=This option is disabled externally +gdpr.consent.trace.requires.ai.assistant=This option requires the AI Assistant plugin gdpr.data.sharing.title.comment.text=Help shape the future of {0} products. By sharing your data and usage statistics, you allow us to better understand how you use our tools and how we can improve them. Learn more here. gdpr.data.collection.consent.setting.enabled.warning.text=Data collection has been enabled by your organization administrator. gdpr.data.collection.consent.setting.disabled.warning.text=Data collection has been disabled by your organization administrator. diff --git a/platform/platform-impl/src/com/intellij/ide/gdpr/ConsentOptionsProviderImpl.java b/platform/platform-impl/src/com/intellij/ide/gdpr/ConsentOptionsProviderImpl.java index 96700f5d3153..a7c16e4d75a5 100644 --- a/platform/platform-impl/src/com/intellij/ide/gdpr/ConsentOptionsProviderImpl.java +++ b/platform/platform-impl/src/com/intellij/ide/gdpr/ConsentOptionsProviderImpl.java @@ -63,8 +63,11 @@ final class ConsentOptionsProviderImpl implements ConsentOptionsProvider { return false; } AiDataCollectionExternalSettings settings = AiDataCollectionExternalSettings.findSettingsImplementedByAiAssistant(); + if (settings == null) { + return false; // AIA plugin is required for TRACE data collection + } boolean isAllowed = isTraceDataCollectionAllowedByMetadata(metadata); - boolean isDisabled = settings != null && settings.isForciblyDisabled(); + boolean isDisabled = settings.isForciblyDisabled(); return isAllowed && !isDisabled; } diff --git a/platform/platform-impl/src/com/intellij/ide/gdpr/ui/consents/AiDataCollectionExternalSettings.kt b/platform/platform-impl/src/com/intellij/ide/gdpr/ui/consents/AiDataCollectionExternalSettings.kt index 256e59c05d4a..3d43343ff098 100644 --- a/platform/platform-impl/src/com/intellij/ide/gdpr/ui/consents/AiDataCollectionExternalSettings.kt +++ b/platform/platform-impl/src/com/intellij/ide/gdpr/ui/consents/AiDataCollectionExternalSettings.kt @@ -2,8 +2,11 @@ package com.intellij.ide.gdpr.ui.consents import com.intellij.internal.statistic.utils.getPluginInfo +import com.intellij.openapi.Disposable import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.util.Disposer import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.TestOnly @ApiStatus.Internal interface AiDataCollectionExternalSettings { @@ -13,8 +16,18 @@ interface AiDataCollectionExternalSettings { private const val AI_ASSISTANT_PLUGIN_ID = "com.intellij.ml.llm" + private var testOverride: AiDataCollectionExternalSettings? = null + + @TestOnly + @JvmStatic + fun overrideForTest(settings: AiDataCollectionExternalSettings, parentDisposable: Disposable) { + testOverride = settings + Disposer.register(parentDisposable) { testOverride = null } + } + @JvmStatic fun findSettingsImplementedByAiAssistant(): AiDataCollectionExternalSettings? { + testOverride?.let { return it } return EP_NAME.findFirstSafe { val pluginInfo = getPluginInfo(it.javaClass) pluginInfo.isDevelopedByJetBrains() && pluginInfo.id == AI_ASSISTANT_PLUGIN_ID diff --git a/platform/platform-impl/src/com/intellij/ide/gdpr/ui/consents/TraceDataCollectionConsentUI.kt b/platform/platform-impl/src/com/intellij/ide/gdpr/ui/consents/TraceDataCollectionConsentUI.kt index f6f0530b1e17..4cbf42c137c0 100644 --- a/platform/platform-impl/src/com/intellij/ide/gdpr/ui/consents/TraceDataCollectionConsentUI.kt +++ b/platform/platform-impl/src/com/intellij/ide/gdpr/ui/consents/TraceDataCollectionConsentUI.kt @@ -22,15 +22,17 @@ internal class TraceDataCollectionConsentUI( if (LOG.isDebugEnabled) { LOG.debug("AiDataCollectionExternalSettings: $externalSettings") } - if (externalSettings != null) { - val isForciblyDisabled = externalSettings.isForciblyDisabled() - if (LOG.isDebugEnabled) { - LOG.debug("AiDataCollectionExternalSettings: isForciblyDisabled: ${isForciblyDisabled}") - } - if (isForciblyDisabled) { - return ConsentForcedState.ExternallyDisabled(externalSettings.getForciblyDisabledDescription() - ?: IdeBundle.message("gdpr.consent.externally.disabled.warning")) - } + if (externalSettings == null) { + // AIA plugin is required for TRACE data collection + return ConsentForcedState.ExternallyDisabled(IdeBundle.message("gdpr.consent.trace.requires.ai.assistant")) + } + val isForciblyDisabled = externalSettings.isForciblyDisabled() + if (LOG.isDebugEnabled) { + LOG.debug("AiDataCollectionExternalSettings: isForciblyDisabled: $isForciblyDisabled") + } + if (isForciblyDisabled) { + return ConsentForcedState.ExternallyDisabled(externalSettings.getForciblyDisabledDescription() + ?: IdeBundle.message("gdpr.consent.externally.disabled.warning")) } val dataCollectionAgreement = DataCollectionAgreement.getInstance() return when (dataCollectionAgreement) { diff --git a/platform/platform-tests/BUILD.bazel b/platform/platform-tests/BUILD.bazel index 27d86f3c27ae..ab0eb4099c19 100644 --- a/platform/platform-tests/BUILD.bazel +++ b/platform/platform-tests/BUILD.bazel @@ -189,6 +189,7 @@ jvm_library( "//platform/platform-tests/eel-helper", "//platform/platform-impl/ui", "//platform/eel-provider", + "//libraries/mockito", "@lib//:io-mockk", "@lib//:io-mockk-jvm", "//platform/bookmarks", diff --git a/platform/platform-tests/intellij.platform.tests.iml b/platform/platform-tests/intellij.platform.tests.iml index 906fff80ed9f..d1b67f225161 100644 --- a/platform/platform-tests/intellij.platform.tests.iml +++ b/platform/platform-tests/intellij.platform.tests.iml @@ -138,6 +138,7 @@ + diff --git a/platform/platform-tests/testSrc/com/intellij/ide/gdpr/DataCollectionConsentUiTest.java b/platform/platform-tests/testSrc/com/intellij/ide/gdpr/DataCollectionConsentUiTest.java index 48eeae2bd3c3..ab9a5d4cb0ce 100644 --- a/platform/platform-tests/testSrc/com/intellij/ide/gdpr/DataCollectionConsentUiTest.java +++ b/platform/platform-tests/testSrc/com/intellij/ide/gdpr/DataCollectionConsentUiTest.java @@ -1,10 +1,13 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.ide.gdpr; +import com.intellij.ide.IdeBundle; +import com.intellij.ide.gdpr.ui.consents.AiDataCollectionExternalSettings; import com.intellij.ide.gdpr.ui.consents.ConsentForcedState; import com.intellij.ide.gdpr.ui.consents.ConsentUi; import com.intellij.testFramework.fixtures.BasePlatformTestCase; import com.intellij.ui.LicensingFacade; +import org.mockito.Mockito; public class DataCollectionConsentUiTest extends BasePlatformTestCase { private static final String CONSENT_ID_USAGE_STATS = "rsch.send.usage.stat"; @@ -66,6 +69,10 @@ public class DataCollectionConsentUiTest extends BasePlatformTestCase { false, "en"); ConsentUi ui = ConsentSettingsUi.getConsentUi(trace); + AiDataCollectionExternalSettings mockSettings = Mockito.mock(AiDataCollectionExternalSettings.class); + Mockito.doReturn(false).when(mockSettings).isForciblyDisabled(); + AiDataCollectionExternalSettings.overrideForTest(mockSettings, getTestRootDisposable()); + setupLicensingFacade('X'); assertNull(ui.getForcedState()); @@ -81,4 +88,35 @@ public class DataCollectionConsentUiTest extends BasePlatformTestCase { assertInstanceOf(state, ConsentForcedState.AlwaysEnabled.class); assertEquals("Data collection has been enabled by your organization administrator.", state.getDescription()); } -} \ No newline at end of file + + public void testTraceConsentForcedDisabledWhenAiaPluginAbsent() { + Consent trace = new Consent(CONSENT_ID_TRACE_DATA_COM_COLLECTION, Version.fromString("1.0"), "Send detailed code-related data", "text", false, + false, "en"); + ConsentUi ui = ConsentSettingsUi.getConsentUi(trace); + + for (char agreementChar : new char[]{'X', 'N', 'Y'}) { + setupLicensingFacade(agreementChar); + var state = ui.getForcedState(); + assertNotNull("TRACE consent must be force-disabled when AIA plugin is absent (agreement='" + agreementChar + "')", state); + assertInstanceOf(state, ConsentForcedState.ExternallyDisabled.class); + assertEquals(IdeBundle.message("gdpr.consent.trace.requires.ai.assistant"), state.getDescription()); + } + } + + public void testTraceConsentForcedDisabledWhenAiaForciblyDisabled() { + Consent trace = new Consent(CONSENT_ID_TRACE_DATA_COM_COLLECTION, Version.fromString("1.0"), "Send detailed code-related data", "text", false, + false, "en"); + ConsentUi ui = ConsentSettingsUi.getConsentUi(trace); + + AiDataCollectionExternalSettings mockSettings = Mockito.mock(AiDataCollectionExternalSettings.class); + Mockito.doReturn(true).when(mockSettings).isForciblyDisabled(); + Mockito.doReturn("Disabled by organization").when(mockSettings).getForciblyDisabledDescription(); + AiDataCollectionExternalSettings.overrideForTest(mockSettings, getTestRootDisposable()); + + setupLicensingFacade('X'); + var state = ui.getForcedState(); + assertNotNull(state); + assertInstanceOf(state, ConsentForcedState.ExternallyDisabled.class); + assertEquals("Disabled by organization", state.getDescription()); + } +}