From ae0f936e9a9cc18e943ba9297d6734dc0b05809b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 16:19:02 +0900 Subject: [PATCH 1/5] fix: locate the CLion backend and verify stable IDE runtimes --- ...ion-backend-location-and-stable-runtime.md | 66 ++++++++ .github/workflows/ci.yml | 8 + .github/workflows/clion-stable.yml | 90 ++++++++++ .gitignore | 1 + docs/architecture.md | 7 + mide-plugins/clion/README.md | 20 +++ .../clion/integration-tests/.gitignore | 3 + .../clion/integration-tests/build.gradle.kts | 38 +++++ .../integration-tests/settings.gradle.kts | 1 + .../src/test/kotlin/PackagedPluginTest.kt | 157 ++++++++++++++++++ .../mcppcommunity/mcppclion/McppSession.kt | 30 +++- .../mcppclion/backend/Protocol.kt | 16 ++ .../mcppcommunity/mcppclion/ProtocolTest.kt | 22 +++ 13 files changed, 455 insertions(+), 4 deletions(-) create mode 100644 .agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md create mode 100644 .github/workflows/clion-stable.yml create mode 100644 mide-plugins/clion/integration-tests/.gitignore create mode 100644 mide-plugins/clion/integration-tests/build.gradle.kts create mode 100644 mide-plugins/clion/integration-tests/settings.gradle.kts create mode 100644 mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt create mode 100644 mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/backend/Protocol.kt create mode 100644 mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/ProtocolTest.kt diff --git a/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md b/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md new file mode 100644 index 0000000..553888d --- /dev/null +++ b/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md @@ -0,0 +1,66 @@ +# CLion backend location and stable runtime validation + +Status: implemented on the review branch; stable CI validation pending. + +## Observed failure + +The published mcpp-clion 0.2.0 package contains `backend/mcpp-ide.okb` and its runners. Its +`pluginDirectory()` nevertheless derives the installation directory from the class loader's +`CodeSource.location`. A full local CLion 2026.1.5.2 run, build CL-261.27258.141, verified that +the actual `McppSession` class has a null location. The original package consequently fails +to find the bundled backend in an isolated environment. + +A separate user log records downloading 0.2.0, dynamically loading the plugin, and starting a +backend with source `xlings`. It records neither the external backend's version nor the +unknown-method response. The log establishes fallback selection, not the selected backend's +protocol. No user log excerpts, account names, project names or local paths are included here. + +An independent wire experiment with the real released 0.1.0 backend reproduces error -32601 +for `mcpp-ide/i18n/bundle`; that backend reports protocol 1. The 0.2.0 bundled backend reports +protocol 2 and answers the request. This is a sufficient reproduction of the notification, +not proof of the user's external backend version. The shorter reported `mcpp-ide/bundle` +spelling has not been observed on the wire; the current client sends `mcpp-ide/i18n/bundle`. + +## Responsibilities and implementation + +The CLion adapter owns installation lookup and protocol acceptance. It obtains its directory +from the IDE plugin descriptor and retains the existing backend preference: explicit setting, +bundle, xlings, PATH. The initializer rejects incompatible or missing protocol values before +sending any custom requests. No legacy method aliases are introduced. + +Startup errors previously reached an action's notification handler without a corresponding +log entry. Startup failures now write a warning with backend source, negotiated protocol, +and, for RPC failures, wire method and error code. Process launch errors and a missing backend +also produce a warning. The existing startup line continues to identify the selected command +locally; private diagnostics are not published as CI artifacts. + +The mcppls language service is a separate connection. Its successful initialization in the +user log does not validate the mcpp-ide session, and this lookup failure does not require a +clangd rebuild or a runtime dependency release. + +## Controlled local result + +The original input is the Marketplace 0.2.0 ZIP, SHA-256 +`7e3fec81186d897e4185413ffbf3b69b08f9185f0da78842f4826f2408d274ba`. +Only the plugin JAR was changed for a diagnostic comparison. The backend bundle and every +runner remained byte-identical. With the descriptor lookup and handshake diagnostics, the +same CLion build completes protocol 2 startup using backend 0.2.0, loads translations and +commands, opens the mcpp tool window, and creates all three views. + +This patched ZIP is an experiment, not a published release or the canonical Gradle package. +The normal CI build must still compile and package the complete plugin and run that same +candidate ZIP on both stable versions. + +## Runtime coverage and limits + +`clion-stable.yml` pins CLion 2026.1.5.2 and 2026.2.4.1. Each test starts the actual IDE, +installs the same packaged plugin ZIP with mcppls 0.0.19, opens a synthetic mcpp project, +checks the actual IDE version and build, completes the session, and verifies the visible +tool window and three views. The standalone test build does not compile the product plugin +against each tested SDK. A manual release check can reuse the published ZIP without rebuilding +the backend. Only a report of public versions and assertions is uploaded. + +Plugin Verifier remains a separate static compatibility check. These runtime checks cover +Linux startup and views on the two stable IDE families; native Windows/macOS GUI behavior, +dynamic uninstall/reinstall, debugging and every project action require their own evidence. +The null CodeSource reproduction uses a cold IDE start and does not depend on dynamic installation. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12a4706..6531600 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -471,6 +471,14 @@ jobs: name: mcpp-clion-zip path: mide-plugins/clion/build/distributions/*.zip + # Both stable families load the exact same packaged ZIP. These are actual + # IDE startup/session checks, in addition to the static Plugin Verifier. + clion-stable: + needs: clion + uses: ./.github/workflows/clion-stable.yml + with: + plugin_artifact: mcpp-clion-zip + # The Neovim plugin (mide-plugins/nvim) in a headless Neovim, against the backend built here: over # the mock mcpp on the oldest and the newest Neovim it supports, then over the real mcpp, which # builds the test's project. Neovim comes from xlings (`nvim` in xim-pkgindex), each version diff --git a/.github/workflows/clion-stable.yml b/.github/workflows/clion-stable.yml new file mode 100644 index 0000000..ef5fce1 --- /dev/null +++ b/.github/workflows/clion-stable.yml @@ -0,0 +1,90 @@ +name: CLion stable runtime + +on: + workflow_call: + inputs: + plugin_artifact: + type: string + required: true + workflow_dispatch: + inputs: + release_tag: + description: Published plugin release to install without rebuilding the backend + type: string + default: v0.2.0 + +permissions: + contents: read + actions: read + +jobs: + runtime: + name: packaged plugin on CLion ${{ matrix.version }} + runs-on: ubuntu-24.04 + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + version: ['2026.1.5.2', '2026.2.4.1'] + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v5 + - uses: ./.github/actions/setup-mcpp + - name: Gradle and a virtual display + run: | + xlings install gradle -y -g + sudo apt-get update -qq + sudo apt-get install -y xvfb + - uses: actions/cache@v5 + with: + path: ~/.gradle/caches/modules-2 + key: clion-stable-runtime-${{ hashFiles('mide-plugins/clion/integration-tests/*.gradle.kts') }} + - name: The candidate ZIP built once by CI + if: inputs.plugin_artifact != '' + uses: actions/download-artifact@v5 + with: + name: ${{ inputs.plugin_artifact }} + path: stable-plugin + - name: The published ZIP for an isolated release check + if: inputs.plugin_artifact == '' + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ inputs.release_tag || 'v0.2.0' }} + run: | + set -euo pipefail + [[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] + mkdir -p stable-plugin + gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \ + --pattern 'mcpp-clion-*.zip' --dir stable-plugin + - name: The pinned language-service plugin + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + version="$(sed -n 's/^mcpplsPluginVersion *= *//p' mide-plugins/clion/gradle.properties)" + gh release download "v$version" --repo Sunrisepeak/mcpp-language-server \ + --pattern "mcppls-clion-$version-linux-x64.zip" --dir stable-plugin + - name: Start the real IDE and complete the plugin startup chain + env: + IDE_VERSION: ${{ matrix.version }} + run: | + set -euo pipefail + plugin="$(find stable-plugin -maxdepth 1 -name 'mcpp-clion-*.zip' -print)" + language="$(find stable-plugin -maxdepth 1 -name 'mcppls-clion-*.zip' -print)" + test -n "$plugin" && test -n "$language" + xvfb-run -a gradle --no-daemon --max-workers=4 \ + -p mide-plugins/clion/integration-tests test \ + -PpluginZip="$GITHUB_WORKSPACE/$plugin" \ + -PmcpplsZip="$GITHUB_WORKSPACE/$language" \ + -PideVersion="$IDE_VERSION" \ + -Poutput="$RUNNER_TEMP/clion-stable-result" + # The report contains public versions and assertions only. Private user + # logs and arbitrary IDE process output are not uploaded as artifacts. + - uses: actions/upload-artifact@v5 + if: always() + with: + name: clion-stable-${{ matrix.version }} + path: ${{ runner.temp }}/clion-stable-result/result.json + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index 708cb65..26e4a91 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ webui/dist/ compile_commands.json dist-upload/ .xlings/ +out/ide-tests/ diff --git a/docs/architecture.md b/docs/architecture.md index d74ae17..711ffe9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -94,6 +94,13 @@ bundle and the target set's tree (`mcpp-ide--openkal-bundle-all.tar.gz` one pack on Linux, takes okrun from that tree, opens both editor packages and the tree on every system and starts what they carry for it (`.github/scripts/check-package.sh`). +The CLion adapter obtains its installation directory from the IDE's plugin descriptor. Its +class loader's `CodeSource` URL is not required. The session accepts protocol 2 before sending +`mcpp-ide/i18n/bundle` and `mcpp-ide/commands/list`. Backend selection and startup failures are +recorded in `idea.log`; RPC startup failures include the method and error code. The stable +CLion runtime checks install one packaged plugin ZIP in both stable IDE families and exercise +the complete startup chain in each IDE process. + ## Generated declarations `spec/` is the source of everything the editors declare statically. `mcpp-ide-dev gen vscode` writes diff --git a/mide-plugins/clion/README.md b/mide-plugins/clion/README.md index f44542b..e925472 100644 --- a/mide-plugins/clion/README.md +++ b/mide-plugins/clion/README.md @@ -42,3 +42,23 @@ docs/architecture.md, Distribution. Without them the plugin finds the backend th `MCPP_IDE_MOCK_MCPP` name the programs `mcpp build` produced, and the views' models and the settings form headless. `npm run test:webui` in `../vscode` runs the graph page in Chromium over the backend. + +The packaged ZIP is also run in the real stable CLion 2026.1.5.2 and 2026.2.4.1 IDEs by +`clion-stable.yml`. Both versions load the same ZIP and the pinned mcppls plugin. The test opens +an mcpp project, completes protocol 2 initialization, loads runtime translations and commands, +and activates the mcpp tool window. It supplements Plugin Verifier's static compatibility checks. + +An individual packaged plugin can be checked without rebuilding the backend: + +```bash +xvfb-run -a gradle --no-daemon -p integration-tests test \ + -PpluginZip=/absolute/path/mcpp-clion-0.2.0.zip \ + -PmcpplsZip=/absolute/path/mcppls-clion-0.0.19-linux-x64.zip \ + -PideVersion=2026.1.5.2 +``` + +The CLion session writes backend selection and startup failures to `idea.log`, under +`io.github.mcppcommunity.mcppclion.McppSession`. A protocol mismatch stops initialization before +custom requests. For an explicitly configured older backend, update it or clear +`mcpp.ide.backendPath` to select the bundled backend. Error notifications can also appear in +the IDE's notification area. diff --git a/mide-plugins/clion/integration-tests/.gitignore b/mide-plugins/clion/integration-tests/.gitignore new file mode 100644 index 0000000..ac0e82c --- /dev/null +++ b/mide-plugins/clion/integration-tests/.gitignore @@ -0,0 +1,3 @@ +.gradle/ +.kotlin/ +build/ diff --git a/mide-plugins/clion/integration-tests/build.gradle.kts b/mide-plugins/clion/integration-tests/build.gradle.kts new file mode 100644 index 0000000..f4e0243 --- /dev/null +++ b/mide-plugins/clion/integration-tests/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + kotlin("jvm") version "2.4.0" +} + +repositories { + mavenCentral() + maven("https://www.jetbrains.com/intellij-repository/releases") + maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") + maven("https://download.jetbrains.com/teamcity-repository") + maven("https://cache-redirector.jetbrains.com/packages.jetbrains.team/maven/p/grazi/grazie-platform-public") +} + +dependencies { + testImplementation("com.jetbrains.intellij.tools:ide-starter-squashed:262.10968.63") + testImplementation("com.jetbrains.intellij.tools:ide-starter-junit5:262.10968.63") + testImplementation("com.jetbrains.intellij.tools:ide-starter-driver:262.10968.63") + testImplementation("com.jetbrains.intellij.driver:driver-client:262.10968.63") + testImplementation("com.jetbrains.intellij.driver:driver-sdk:262.10968.63") + testImplementation("org.junit.jupiter:junit-jupiter:5.12.2") + testImplementation("org.kodein.di:kodein-di-jvm:7.20.2") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.1") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +kotlin { jvmToolchain(25) } + +// This build has no dependency on the plugin's main output. Both IDE versions +// load the same packaged ZIP; compiling the plugin against each tested SDK +// would hide binary compatibility defects in the actual distributed plugin. +tasks.test { + useJUnitPlatform() + maxHeapSize = "2g" + for (key in listOf("pluginZip", "mcpplsZip", "ideVersion", "ideHome", "output")) { + providers.gradleProperty(key).orNull?.let { systemProperty("mcpp.clion.$key", it) } + } + providers.gradleProperty("testHome").orNull?.let { systemProperty("user.home", it) } + testLogging { events("passed", "failed", "skipped") } +} diff --git a/mide-plugins/clion/integration-tests/settings.gradle.kts b/mide-plugins/clion/integration-tests/settings.gradle.kts new file mode 100644 index 0000000..f361a5f --- /dev/null +++ b/mide-plugins/clion/integration-tests/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "mcpp-clion-integration-tests" diff --git a/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt b/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt new file mode 100644 index 0000000..ffe6803 --- /dev/null +++ b/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt @@ -0,0 +1,157 @@ +import com.intellij.driver.client.Remote +import com.intellij.driver.client.service +import com.intellij.driver.client.utility +import com.intellij.driver.sdk.invokeAction +import com.intellij.driver.sdk.Project +import com.intellij.driver.sdk.singleProject +import com.intellij.driver.sdk.waitForProjectOpen +import com.intellij.ide.starter.ci.CIServer +import com.intellij.ide.starter.ci.NoCIServer +import com.intellij.ide.starter.di.di +import com.intellij.ide.starter.driver.engine.runIdeWithDriver +import com.intellij.ide.starter.ide.IdeDistributionFactory +import com.intellij.ide.starter.ide.IdeInstaller +import com.intellij.ide.starter.models.IdeInfo +import com.intellij.ide.starter.models.TestCase +import com.intellij.ide.starter.plugins.PluginConfigurator +import com.intellij.ide.starter.project.LocalProjectInfo +import com.intellij.ide.starter.runner.Starter +import com.intellij.platform.testFramework.teamCity.TeamCityReporter.SyntheticTestKind +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.kodein.di.DI +import org.kodein.di.bindSingleton +import org.kodein.di.direct +import org.kodein.di.instance +import java.nio.file.Files +import java.nio.file.Path + +private const val PLUGIN_ID = "io.github.mcpp-community.mcpp-clion" + +@Remote("io.github.mcppcommunity.mcppclion.McppSession", plugin = PLUGIN_ID) +interface McppSessionRemote { + fun start(): StartupFuture + fun getCommands(): List + fun getDegraded(): Boolean +} + + +@Remote("java.util.concurrent.CompletableFuture") +interface StartupFuture { + fun isDone(): Boolean + fun get(): RemoteJson +} + +@Remote("com.google.gson.JsonObject") +interface RemoteJson { fun get(name: String): RemoteJsonValue } + +@Remote("com.google.gson.JsonElement") +interface RemoteJsonValue { + fun getAsInt(): Int + fun getAsString(): String + fun getAsJsonObject(): RemoteJson +} + +@Remote("com.intellij.openapi.application.ApplicationInfo") +interface ApplicationInfoFactory { fun getInstance(): ApplicationInfoRemote } + +@Remote("com.intellij.openapi.application.ApplicationInfo") +interface ApplicationInfoRemote { + fun getFullVersion(): String + fun getBuild(): BuildNumberRemote +} + +@Remote("com.intellij.openapi.util.BuildNumber") +interface BuildNumberRemote { fun asString(): String } + +@Remote("com.intellij.openapi.wm.ToolWindowManager") +interface ToolWindowManagerFactory { fun getInstance(project: Project): ToolWindowManagerRemote } +@Remote("com.intellij.openapi.wm.ToolWindowManager") +interface ToolWindowManagerRemote { fun getToolWindow(id: String): ToolWindowRemote? } +@Remote("com.intellij.openapi.wm.ToolWindow") +interface ToolWindowRemote { + fun isVisible(): Boolean + fun getContentManager(): ContentManagerRemote +} +@Remote("com.intellij.ui.content.ContentManager") +interface ContentManagerRemote { fun getContentCount(): Int } + +class PackagedPluginTest { + init { + // A successful process exit alone cannot prove the plugin loaded: + // exceptions reported by the IDE must fail the integration test. + di = DI { + extend(di) + bindSingleton(overrides = true) { + object : CIServer by NoCIServer { + override fun reportTestFailure(testName: String, message: String, details: String, linkToLogs: String?, kind: SyntheticTestKind, generifyTestName: Boolean) { + throw AssertionError("IDE failure: $testName: $message") + } + } + } + } + } + + @Test + fun packagedPluginStartsItsBundledBackend() { + val version = System.getProperty("mcpp.clion.ideVersion") ?: error("ideVersion is required") + val output = Path.of(System.getProperty("mcpp.clion.output") ?: "build/stable-smoke").toAbsolutePath() + Files.createDirectories(output) + val fixture = output.resolve("project") + Files.createDirectories(fixture.resolve("src")) + Files.writeString(fixture.resolve("mcpp.toml"), "[package]\nname = \"clion-smoke\"\nversion = \"0.1.0\"\n") + Files.writeString(fixture.resolve("src/main.cpp"), "int main() { return 0; }\n") + val localHome = System.getProperty("mcpp.clion.ideHome")?.let(Path::of) + var ide = IdeInfo(productCode = "CL", platformPrefix = "CLion", executableFileName = "clion", fullName = "CLion", version = version, buildType = "release") + if (localHome != null) { + // Reuse the verified full local distribution without copying an + // additional SDK into the ordinary disk cache. + ide = ide.copy(getInstaller = { + object : IdeInstaller { + override suspend fun install(ideInfo: IdeInfo) = + Files.readString(localHome.resolve("build.txt")).trim().removePrefix("CL-") to + di.direct.instance().installIDE(localHome, "clion") + } + }) + } + val context = Starter.newContext("mcpp-clion-stable-$version-${System.currentTimeMillis()}", TestCase(ide, LocalProjectInfo(fixture))) + PluginConfigurator(context).installPluginFromPath(Path.of(System.getProperty("mcpp.clion.mcpplsZip") ?: error("mcpplsZip is required"))) + PluginConfigurator(context).installPluginFromPath(Path.of(System.getProperty("mcpp.clion.pluginZip") ?: error("pluginZip is required"))) + context.applyVMOptionsPatch { + withXmx(2048) + addSystemProperty("idea.trust.all.projects", true) + addSystemProperty("ide.show.tips.on.startup.default.value", false) + } + context.runIdeWithDriver().useDriverAndCloseIde { + waitForProjectOpen() + withContext { + val application = utility().getInstance() + assertEquals(version, application.getFullVersion(), "the requested stable IDE actually runs") + val ideBuild = application.getBuild().asString() + val session = service(singleProject()) + val future = session.start() + val deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(60) + while (!future.isDone() && System.nanoTime() < deadline) Thread.sleep(100) + assertTrue(future.isDone(), "the complete plugin startup chain finishes") + val initialized = future.get() + assertEquals(2, initialized.get("protocol").getAsInt()) + val backendVersion = initialized.get("serverInfo").getAsJsonObject().get("version").getAsString() + assertFalse(session.getCommands().isEmpty(), "i18n/bundle and commands/list complete before startup succeeds") + assertFalse(session.getDegraded()) + invokeAction("ActivatemcppToolWindow") + val window = utility().getInstance(singleProject()).getToolWindow("mcpp") + assertNotNull(window, "the mcpp tool window is registered") + val visibleDeadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(30) + while (!window!!.isVisible() && System.nanoTime() < visibleDeadline) Thread.sleep(100) + assertTrue(window!!.isVisible(), "the tool window opens") + assertEquals(3, window.getContentManager().getContentCount(), "Project, Libraries and Cache views load") + // Store only public versions and assertions. Raw IDE logs can + // contain usernames, private paths or workspace details. + Files.writeString(output.resolve("result.json"), """{"ideVersion":"$version","ideBuild":"$ideBuild","backendVersion":"$backendVersion","protocol":2,"startup":true,"commands":true,"toolWindowVisible":true,"toolWindowTabs":3}""" + "\n") + } + } + } +} diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt index e820d75..54ee858 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt @@ -20,6 +20,9 @@ import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import io.github.mcppcommunity.mcppclion.backend.Backend import io.github.mcppcommunity.mcppclion.backend.Locate +import io.github.mcppcommunity.mcppclion.backend.BackendError +import io.github.mcppcommunity.mcppclion.backend.requireProtocol +import io.github.mcppcommunity.mcppclion.backend.wireName import io.github.mcppcommunity.mcppclion.settings.McppSettings import java.io.File import java.util.concurrent.CompletableFuture @@ -146,12 +149,19 @@ class McppSession(private val project: Project) : Disposable { val configured = if (TrustedProjects.isProjectTrusted(project)) McppSettings.get().string("mcpp.ide.backendPath") else "" val located = Locate.backend(configured, pluginRoot()) if (located == null) { + log.warn("mcpp-ide: startup failed (source=none, protocol=unknown): backend not found") notify(t("The mcpp-ide backend was not found. Install it with `xlings install mcpp-ide`, or set mcpp.ide.backendPath."), NotificationType.ERROR) future.completeExceptionally(IllegalStateException("mcpp-ide was not found")) return future } log.info("mcpp-ide: starting ${located.command.joinToString(" ")} (${located.source})") - val b = Backend(located.command + "serve") { line -> log.info("[backend] $line") } + val b = try { + Backend(located.command + "serve") { line -> log.info("[backend] $line") } + } catch (error: Exception) { + log.warn("mcpp-ide: startup failed (source=${located.source}, protocol=unknown)", error) + future.completeExceptionally(error) + return future + } backend = b b.on("*") { method, params -> listeners.filter { it.first == method || it.first == "*" }.forEach { (m, fn) -> runCatching { fn(if (m == "*") JsonObject().apply { addProperty("method", method); add("params", params) } else params) } @@ -174,7 +184,11 @@ class McppSession(private val project: Project) : Disposable { addProperty("mcpplsServes", mcpplsServes()) }) } + var protocol = "unknown" b.request("initialize", params).thenCompose { init -> + protocol = init.asJsonObject["protocol"]?.toString() ?: "missing" + requireProtocol(init.asJsonObject) + log.info("mcpp-ide: initialized (source=${located.source}, protocol=$protocol)") initialized = init.asJsonObject b.notify("initialized", JsonObject()) b.request("i18n/bundle") @@ -182,7 +196,13 @@ class McppSession(private val project: Project) : Disposable { bundle = strings.asJsonObject.entrySet().associate { it.key to it.value.asString } b.request("commands/list", JsonObject().apply { addProperty("editor", "jetbrains") }) }.whenComplete { table, error -> - if (error != null) { future.completeExceptionally(error); return@whenComplete } + if (error != null) { + val cause = (error as? java.util.concurrent.CompletionException)?.cause ?: error + val rpc = (cause as? BackendError)?.let { ", method=${wireName(it.method)}, code=${it.code}" } ?: "" + log.warn("mcpp-ide: startup failed (source=${located.source}, protocol=$protocol$rpc)", cause) + future.completeExceptionally(error) + return@whenComplete + } commands = table.asJsonObject.getAsJsonArray("commands").map { it.asJsonObject } future.complete(initialized!!) startedListeners.forEach { runCatching { it() } } @@ -330,9 +350,11 @@ class McppSession(private val project: Project) : Disposable { } companion object { - /** /lib/ -> , from where this class was loaded. */ + /** The platform owns plugin paths; modern plugin class loaders can omit CodeSource URLs. */ fun pluginDirectory(): File? = runCatching { - File(McppSession::class.java.protectionDomain.codeSource.location.toURI()).parentFile?.parentFile + com.intellij.ide.plugins.PluginManagerCore.getPlugin( + com.intellij.openapi.extensions.PluginId.getId(PLUGIN_ID) + )?.pluginPath?.toFile() }.getOrNull() const val PLUGIN_ID = "io.github.mcpp-community.mcpp-clion" diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/backend/Protocol.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/backend/Protocol.kt new file mode 100644 index 0000000..e26ec49 --- /dev/null +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/backend/Protocol.kt @@ -0,0 +1,16 @@ +package io.github.mcppcommunity.mcppclion.backend + +import com.google.gson.JsonObject +import java.math.BigDecimal + +/** Check the handshake before issuing any namespaced extension requests. */ +fun requireProtocol(initialized: JsonObject) { + val value = initialized["protocol"] + val compatible = runCatching { + value != null && value.isJsonPrimitive && value.asJsonPrimitive.isNumber && + value.asBigDecimal.compareTo(BigDecimal(2)) == 0 + }.getOrDefault(false) + check(compatible) { + "The selected mcpp-ide backend does not support protocol 2. Update the backend or clear mcpp.ide.backendPath to use the bundled backend." + } +} diff --git a/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/ProtocolTest.kt b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/ProtocolTest.kt new file mode 100644 index 0000000..95d39c3 --- /dev/null +++ b/mide-plugins/clion/src/test/kotlin/io/github/mcppcommunity/mcppclion/ProtocolTest.kt @@ -0,0 +1,22 @@ +package io.github.mcppcommunity.mcppclion + +import com.google.gson.JsonObject +import org.junit.Assert.assertThrows +import org.junit.Test + +class ProtocolTest { + @Test + fun theHandshakeRejectsIncompatibleProtocolsBeforeCustomRequests() { + for (protocol in listOf("1", "3", "null", "\"2\"", "{}", "[]", "true", "2.5")) { + val initialized = com.google.gson.JsonParser.parseString("""{"protocol":$protocol}""").asJsonObject + assertThrows(IllegalStateException::class.java) { + io.github.mcppcommunity.mcppclion.backend.requireProtocol(initialized) + } + } + assertThrows(IllegalStateException::class.java) { + io.github.mcppcommunity.mcppclion.backend.requireProtocol(JsonObject()) + } + io.github.mcppcommunity.mcppclion.backend.requireProtocol(JsonObject().apply { addProperty("protocol", 2) }) + } + +} From 6e44d5444cd0e856937105ac442984ba1b679886 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 16:27:43 +0900 Subject: [PATCH 2/5] test: exercise CLion build run and test actions --- ...ion-backend-location-and-stable-runtime.md | 38 +++++++++++ .github/workflows/clion-stable.yml | 5 +- .../src/test/kotlin/PackagedPluginTest.kt | 64 ++++++++++++++++++- 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md b/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md index 553888d..3c53276 100644 --- a/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md +++ b/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md @@ -64,3 +64,41 @@ Plugin Verifier remains a separate static compatibility check. These runtime che Linux startup and views on the two stable IDE families; native Windows/macOS GUI behavior, dynamic uninstall/reinstall, debugging and every project action require their own evidence. The null CodeSource reproduction uses a cold IDE start and does not depend on dynamic installation. + +## Functional and user-experience coverage + +The denominator is the current command registry and the adapter's additional UI surfaces. +`spec/commands.json` declares 59 commands, of which 55 include JetBrains (five are deprecated). +The runtime table can offer fewer entries according to the language service's advertised +capabilities. Every offered command and each forwarded action must resolve to a registered +IDE action; hidden or deprecated commands are not counted as executed functionality. + +The local 2026.1.5.2 experiment also invokes the actual Build, Run and Test actions, waits for +their IDE process handlers, requires exit code zero, and checks a file written by the program. +These results establish real action execution; test-tree presentation and debugging need +additional assertions. Compilation uses the installed GCC 16.1.0 toolchain. CI installs the +same compiler before running this functional scenario. + +| Area | Required scenario | Current local evidence | +|---|---|---| +| Backend/session | Bundle lookup, protocol mismatch, missing backend, restart and recovery | Original lookup failure and corrected startup reproduced; protocol data validation passes; recovery pending | +| Command integration | Registry, context menu, shortcuts, forwarded mcppls commands | All offered actions resolve; individual forwarded operations and interactive discoverability pending | +| Build | Success, compilation failure, incremental build, cancellation, console source links | Successful IDE action; negative, cancellation and links pending | +| Run | Executable selection, arguments, working directory, PTY, stop | Successful IDE action and program-produced file; other interactions pending | +| Test | Discovery, success, failure, filter, test tree and rerun | Successful IDE action; tree details and negative/filter/rerun pending | +| Debug | Native breakpoint, variables, step, continue, stop; runner-backed program boundary | Pending actual debugger session | +| Project | Data rows, targets, result/artifact links, refresh | Three views instantiate; populated rows and links pending | +| Libraries | Search, detail, add dependency, index update, error feedback | Pending functional interaction | +| Cache | Inventory, plan, confirmation/cancel, isolated apply, refused untrusted actions | Pending; no global user cache deletion is part of this test | +| Settings | Registry form, persist/reload, invalid value, locale | Pending interactive validation | +| Graphs | Package/module graph render, source navigation, empty/error view, no-JCEF fallback | Pending actual JCEF rendering | +| Editor | Manifest/buildscript completion, hover, diagnostics, navigation, snippets; C++ module handoff | Pending actual editor interaction | +| Project/toolchain setup | New project, target/profile choice, install/default toolchain | Compiler installation is test setup; plugin flows pending | +| Diagnostics | Self-check, report export/redaction, logs, degraded operation | Startup logging implemented; export and recovery interactions pending | + +Availability requires an observed operation and its expected effect. Completeness additionally +requires the specified negative and recovery paths. User experience requires checking visible +feedback, discoverability, absence of stalled progress, selection persistence and actionable +errors. Static API compatibility and registered actions alone do not satisfy these criteria. +The two stable IDE versions run the same candidate; platform-specific GUI behavior remains +separately bounded. Findings and fixes continue in the same review PR. diff --git a/.github/workflows/clion-stable.yml b/.github/workflows/clion-stable.yml index ef5fce1..41902c9 100644 --- a/.github/workflows/clion-stable.yml +++ b/.github/workflows/clion-stable.yml @@ -15,7 +15,6 @@ on: permissions: contents: read - actions: read jobs: runtime: @@ -37,6 +36,10 @@ jobs: xlings install gradle -y -g sudo apt-get update -qq sudo apt-get install -y xvfb + - name: The compiler for actual build, run and test actions + run: | + mcpp toolchain install gcc 16.1.0 + mcpp toolchain default gcc@16.1.0 - uses: actions/cache@v5 with: path: ~/.gradle/caches/modules-2 diff --git a/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt b/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt index ffe6803..5b44abe 100644 --- a/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt +++ b/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt @@ -79,6 +79,29 @@ interface ToolWindowRemote { @Remote("com.intellij.ui.content.ContentManager") interface ContentManagerRemote { fun getContentCount(): Int } +@Remote("com.intellij.openapi.actionSystem.ActionManager") +interface ActionManagerFactory { fun getInstance(): ActionManagerRemote } +@Remote("com.intellij.openapi.actionSystem.ActionManager") +interface ActionManagerRemote { fun getAction(id: String): ActionRemote? } +@Remote("com.intellij.openapi.actionSystem.AnAction") +interface ActionRemote + +@Remote("com.intellij.execution.ui.RunContentManager") +interface RunContentManagerFactory { fun getInstance(project: Project): RunContentManagerRemote } +@Remote("com.intellij.execution.ui.RunContentManager") +interface RunContentManagerRemote { fun getAllDescriptors(): List } +@Remote("com.intellij.execution.ui.RunContentDescriptor") +interface RunDescriptorRemote { + fun getDisplayName(): String + fun getExecutionId(): Long + fun getProcessHandler(): ProcessHandlerRemote? +} +@Remote("com.intellij.execution.process.ProcessHandler") +interface ProcessHandlerRemote { + fun isProcessTerminated(): Boolean + fun getExitCode(): Int? +} + class PackagedPluginTest { init { // A successful process exit alone cannot prove the plugin loaded: @@ -100,10 +123,13 @@ class PackagedPluginTest { val version = System.getProperty("mcpp.clion.ideVersion") ?: error("ideVersion is required") val output = Path.of(System.getProperty("mcpp.clion.output") ?: "build/stable-smoke").toAbsolutePath() Files.createDirectories(output) - val fixture = output.resolve("project") + Files.deleteIfExists(output.resolve("result.json")) + val fixture = Files.createTempDirectory(output, "project-") Files.createDirectories(fixture.resolve("src")) Files.writeString(fixture.resolve("mcpp.toml"), "[package]\nname = \"clion-smoke\"\nversion = \"0.1.0\"\n") - Files.writeString(fixture.resolve("src/main.cpp"), "int main() { return 0; }\n") + Files.writeString(fixture.resolve("src/main.cpp"), "#include \nint main() { auto* f = std::fopen(\"clion-run-ok.txt\", \"w\"); if (!f) return 1; std::fputs(\"run-ok\\n\", f); std::fclose(f); return 0; }\n") + Files.createDirectories(fixture.resolve("tests")) + Files.writeString(fixture.resolve("tests/check.cpp"), "int main() { return 0; }\n") val localHome = System.getProperty("mcpp.clion.ideHome")?.let(Path::of) var ide = IdeInfo(productCode = "CL", platformPrefix = "CLion", executableFileName = "clion", fullName = "CLion", version = version, buildType = "release") if (localHome != null) { @@ -140,6 +166,16 @@ class PackagedPluginTest { assertEquals(2, initialized.get("protocol").getAsInt()) val backendVersion = initialized.get("serverInfo").getAsJsonObject().get("version").getAsString() assertFalse(session.getCommands().isEmpty(), "i18n/bundle and commands/list complete before startup succeeds") + val actions = utility().getInstance() + for (command in session.getCommands()) { + val id = command.get("id").getAsString() + assertNotNull(actions.getAction(id), "offered command $id is registered in this IDE") + val action = command.get("action").getAsJsonObject() + if (action.get("kind").getAsString() == "forward") { + val target = action.get("command").getAsString() + assertNotNull(actions.getAction(target), "forwarded command $id resolves to $target") + } + } assertFalse(session.getDegraded()) invokeAction("ActivatemcppToolWindow") val window = utility().getInstance(singleProject()).getToolWindow("mcpp") @@ -148,9 +184,31 @@ class PackagedPluginTest { while (!window!!.isVisible() && System.nanoTime() < visibleDeadline) Thread.sleep(100) assertTrue(window!!.isVisible(), "the tool window opens") assertEquals(3, window.getContentManager().getContentCount(), "Project, Libraries and Cache views load") + val runs = utility().getInstance(singleProject()) + val actionResults = mutableMapOf() + for (command in listOf("build", "run", "test")) { + val before = runs.getAllDescriptors().map { it.getExecutionId() }.toSet() + val startedAt = System.nanoTime() + invokeAction("mcpp.$command") + val deadline = startedAt + java.util.concurrent.TimeUnit.SECONDS.toNanos(180) + var completed: RunDescriptorRemote? = null + while (System.nanoTime() < deadline) { + completed = runs.getAllDescriptors().firstOrNull { + it.getDisplayName().startsWith("mcpp $command ") && + it.getExecutionId() !in before && it.getProcessHandler()?.isProcessTerminated() == true + } + if (completed != null) break + Thread.sleep(100) + } + assertNotNull(completed, "the real $command action finishes in the IDE") + assertEquals(0, completed!!.getProcessHandler()!!.getExitCode(), "$command exits successfully") + actionResults[command] = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt) + } + assertEquals("run-ok\n", Files.readString(fixture.resolve("clion-run-ok.txt")), "the program actually ran") // Store only public versions and assertions. Raw IDE logs can // contain usernames, private paths or workspace details. - Files.writeString(output.resolve("result.json"), """{"ideVersion":"$version","ideBuild":"$ideBuild","backendVersion":"$backendVersion","protocol":2,"startup":true,"commands":true,"toolWindowVisible":true,"toolWindowTabs":3}""" + "\n") + val actionJson = actionResults.entries.joinToString(",") { "\"${it.key}\":${it.value}" } + Files.writeString(output.resolve("result.json"), """{"ideVersion":"$version","ideBuild":"$ideBuild","backendVersion":"$backendVersion","protocol":2,"startup":true,"commands":true,"registeredCommands":${session.getCommands().size},"toolWindowVisible":true,"toolWindowTabs":3,"actionDurationMs":{$actionJson},"programExecuted":true}""" + "\n") } } } From 6a726f73562efa39f12bf2fb914a3a5980de103b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 17:46:49 +0900 Subject: [PATCH 3/5] fix: keep CLion completion and native views usable during analysis --- ...ion-backend-location-and-stable-runtime.md | 34 +++- .github/workflows/clion-stable.yml | 4 +- mide-plugins/clion/README.md | 5 +- .../clion/integration-tests/build.gradle.kts | 1 + .../src/test/kotlin/PackagedPluginTest.kt | 176 +++++++++++++++++- .../mcppcommunity/mcppclion/McppSession.kt | 5 +- .../mcppclion/editor/McppEditor.kt | 3 +- .../mcppclion/ui/McppToolWindow.kt | 33 +++- 8 files changed, 231 insertions(+), 30 deletions(-) diff --git a/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md b/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md index 3c53276..17ed4ef 100644 --- a/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md +++ b/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md @@ -73,26 +73,42 @@ The runtime table can offer fewer entries according to the language service's ad capabilities. Every offered command and each forwarded action must resolve to a registered IDE action; hidden or deprecated commands are not counted as executed functionality. -The local 2026.1.5.2 experiment also invokes the actual Build, Run and Test actions, waits for -their IDE process handlers, requires exit code zero, and checks a file written by the program. -These results establish real action execution; test-tree presentation and debugging need -additional assertions. Compilation uses the installed GCC 16.1.0 toolchain. CI installs the -same compiler before running this functional scenario. +The local 2026.1.5.2 and 2026.2.4.1 experiments pass the same 20 checks. They invoke the actual +Build, Run and Test actions, wait for fresh process handlers, require the expected exit code, +and check a file written by the program. A compilation failure must appear in the console; +restoring the source must restore a successful build. Test failure and recovery must reach the +native test tree. The project path contains Unicode and spaces. Compilation uses GCC 16.1.0; +CI installs the same compiler. These are diagnostic adapter packages with unchanged backend +bytes; the canonical package must still pass CI. + +Two additional adapter defects were reproduced and corrected. The completion contributor did +not implement `DumbAware`, so an index-independent backend feature was suppressed during IDE +analysis. Marking that contributor allows a real completion popup and exact replacement of +`[dep` by `[dependencies]` while indexing is active. Opening Cache through its command also +left the tree empty until manual refresh. Content selection now focuses and refreshes the view, +including command selection and tab selection, and the runtime check requires real inventory +rows without an error. The same selection path handles existing graph tabs and refresh requests. + +Verifier 1.411 confirms both stable builds are compatible with zero internal API usages after +using the public plugin-aware class loader descriptor. The original deprecated/experimental API +warnings remain. Tests resolve the test-runner classes through the product plugin's class loader: +the classes are in the platform on 2026.1 and a separate optional plugin on 2026.2. Local GUI +tests serialize use of the Driver's ports; CI matrix entries use independent runners. | Area | Required scenario | Current local evidence | |---|---|---| | Backend/session | Bundle lookup, protocol mismatch, missing backend, restart and recovery | Original lookup failure and corrected startup reproduced; protocol data validation passes; recovery pending | | Command integration | Registry, context menu, shortcuts, forwarded mcppls commands | All offered actions resolve; individual forwarded operations and interactive discoverability pending | -| Build | Success, compilation failure, incremental build, cancellation, console source links | Successful IDE action; negative, cancellation and links pending | +| Build | Success, compilation failure, incremental build, cancellation, console source links | Success, compiler error text, failure and recovery pass; cancellation and links pending | | Run | Executable selection, arguments, working directory, PTY, stop | Successful IDE action and program-produced file; other interactions pending | -| Test | Discovery, success, failure, filter, test tree and rerun | Successful IDE action; tree details and negative/filter/rerun pending | +| Test | Discovery, success, failure, filter, test tree and rerun | Native tree success, failure and corrected rerun pass; filter pending | | Debug | Native breakpoint, variables, step, continue, stop; runner-backed program boundary | Pending actual debugger session | | Project | Data rows, targets, result/artifact links, refresh | Three views instantiate; populated rows and links pending | | Libraries | Search, detail, add dependency, index update, error feedback | Pending functional interaction | -| Cache | Inventory, plan, confirmation/cancel, isolated apply, refused untrusted actions | Pending; no global user cache deletion is part of this test | +| Cache | Inventory, plan, confirmation/cancel, isolated apply, refused untrusted actions | Opening the view loads real inventory without manual refresh; plan/apply/refusal pending; no global user cache deletion | | Settings | Registry form, persist/reload, invalid value, locale | Pending interactive validation | | Graphs | Package/module graph render, source navigation, empty/error view, no-JCEF fallback | Pending actual JCEF rendering | -| Editor | Manifest/buildscript completion, hover, diagnostics, navigation, snippets; C++ module handoff | Pending actual editor interaction | +| Editor | Manifest/buildscript completion, hover, diagnostics, navigation, snippets; C++ module handoff | Manifest popup and exact insertion during indexing pass; other interactions pending | | Project/toolchain setup | New project, target/profile choice, install/default toolchain | Compiler installation is test setup; plugin flows pending | | Diagnostics | Self-check, report export/redaction, logs, degraded operation | Startup logging implemented; export and recovery interactions pending | diff --git a/.github/workflows/clion-stable.yml b/.github/workflows/clion-stable.yml index 41902c9..e27dbb3 100644 --- a/.github/workflows/clion-stable.yml +++ b/.github/workflows/clion-stable.yml @@ -89,5 +89,7 @@ jobs: if: always() with: name: clion-stable-${{ matrix.version }} - path: ${{ runner.temp }}/clion-stable-result/result.json + path: | + ${{ runner.temp }}/clion-stable-result/result.json + ${{ runner.temp }}/clion-stable-result/checks.json if-no-files-found: warn diff --git a/mide-plugins/clion/README.md b/mide-plugins/clion/README.md index e925472..066f106 100644 --- a/mide-plugins/clion/README.md +++ b/mide-plugins/clion/README.md @@ -46,7 +46,10 @@ backend. The packaged ZIP is also run in the real stable CLion 2026.1.5.2 and 2026.2.4.1 IDEs by `clion-stable.yml`. Both versions load the same ZIP and the pinned mcppls plugin. The test opens an mcpp project, completes protocol 2 initialization, loads runtime translations and commands, -and activates the mcpp tool window. It supplements Plugin Verifier's static compatibility checks. +and verifies the visible mcpp tool window and cache inventory. It executes build, run and test +actions in a project whose path contains Unicode and spaces, checks the native test tree, +compiler-error output, build/test failure and recovery, and exact manifest completion insertion +during project analysis. It supplements Plugin Verifier's static compatibility checks. An individual packaged plugin can be checked without rebuilding the backend: diff --git a/mide-plugins/clion/integration-tests/build.gradle.kts b/mide-plugins/clion/integration-tests/build.gradle.kts index f4e0243..8287cb5 100644 --- a/mide-plugins/clion/integration-tests/build.gradle.kts +++ b/mide-plugins/clion/integration-tests/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { testImplementation("com.jetbrains.intellij.tools:ide-starter-driver:262.10968.63") testImplementation("com.jetbrains.intellij.driver:driver-client:262.10968.63") testImplementation("com.jetbrains.intellij.driver:driver-sdk:262.10968.63") + testImplementation("com.jetbrains.intellij.driver:driver-model:262.10968.63") testImplementation("org.junit.jupiter:junit-jupiter:5.12.2") testImplementation("org.kodein.di:kodein-di-jvm:7.20.2") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.1") diff --git a/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt b/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt index 5b44abe..509b2b1 100644 --- a/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt +++ b/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt @@ -1,8 +1,13 @@ import com.intellij.driver.client.Remote import com.intellij.driver.client.service import com.intellij.driver.client.utility +import com.intellij.driver.client.impl.RefWrapper import com.intellij.driver.sdk.invokeAction import com.intellij.driver.sdk.Project +import com.intellij.driver.sdk.FileEditorManager +import com.intellij.driver.sdk.openEditor +import com.intellij.driver.sdk.VirtualFile +import com.intellij.driver.model.OnDispatcher import com.intellij.driver.sdk.singleProject import com.intellij.driver.sdk.waitForProjectOpen import com.intellij.ide.starter.ci.CIServer @@ -77,7 +82,40 @@ interface ToolWindowRemote { fun getContentManager(): ContentManagerRemote } @Remote("com.intellij.ui.content.ContentManager") -interface ContentManagerRemote { fun getContentCount(): Int } +interface ContentManagerRemote { + fun getContentCount(): Int + fun getSelectedContent(): ContentRemote? +} +@Remote("com.intellij.ui.content.Content") +interface ContentRemote { + fun getDisplayName(): String + fun getComponent(): ContainerRemote +} +@Remote("java.awt.Container") +interface ContainerRemote { + fun getClass(): ClassNameRemote + fun getComponents(): Array +} +@Remote("java.lang.Class") +interface ClassNameRemote { fun getName(): String } +@Remote("javax.swing.JTree") +interface TreeRemote { + fun getRowCount(): Int + fun getModel(): TreeModelRemote +} +@Remote("javax.swing.tree.TreeModel") +interface TreeModelRemote { fun getRoot(): TreeNodeRemote } +@Remote("javax.swing.tree.DefaultMutableTreeNode") +interface TreeNodeRemote { + fun getChildAt(index: Int): TreeNodeRemote + fun getUserObject(): ViewRowRemote +} +@Remote("io.github.mcppcommunity.mcppclion.ui.Row", plugin = PLUGIN_ID) +interface ViewRowRemote { fun getError(): Boolean } +@Remote("com.intellij.openapi.project.DumbService") +interface DumbServiceFactory { fun getInstance(project: Project): DumbServiceRemote } +@Remote("com.intellij.openapi.project.DumbService") +interface DumbServiceRemote { fun isDumb(): Boolean } @Remote("com.intellij.openapi.actionSystem.ActionManager") interface ActionManagerFactory { fun getInstance(): ActionManagerRemote } @@ -95,6 +133,7 @@ interface RunDescriptorRemote { fun getDisplayName(): String fun getExecutionId(): Long fun getProcessHandler(): ProcessHandlerRemote? + fun getExecutionConsole(): ExecutionConsoleRemote } @Remote("com.intellij.execution.process.ProcessHandler") interface ProcessHandlerRemote { @@ -102,6 +141,40 @@ interface ProcessHandlerRemote { fun getExitCode(): Int? } +@Remote("com.intellij.execution.ui.ExecutionConsole") +interface ExecutionConsoleRemote +@Remote("com.intellij.execution.impl.ConsoleViewImpl") +interface TextConsoleRemote { fun getText(): String } + +@Remote("com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView", plugin = PLUGIN_ID) +interface TestConsoleRemote { fun getResultsViewer(): TestResultsRemote } +@Remote("com.intellij.execution.testframework.sm.runner.ui.SMTestRunnerResultsForm", plugin = PLUGIN_ID) +interface TestResultsRemote { fun getTestsRootNode(): TestNodeRemote } +@Remote("com.intellij.execution.testframework.sm.runner.SMTestProxy", plugin = PLUGIN_ID) +interface TestNodeRemote { + fun getChildren(): List + fun isPassed(): Boolean + fun isDefect(): Boolean +} + +@Remote("com.intellij.openapi.fileEditor.FileEditorManager") +interface FileEditorManagerFactory { fun getInstance(project: Project): FileEditorManager } +@Remote("com.intellij.openapi.vfs.LocalFileSystem") +interface LocalFileSystemFactory { fun getInstance(): LocalFileSystemRemote } +@Remote("com.intellij.openapi.vfs.LocalFileSystem") +interface LocalFileSystemRemote { fun refreshAndFindFileByPath(path: String): VirtualFile? } +@Remote("com.intellij.codeInsight.lookup.LookupManager") +interface LookupManagerFactory { fun getInstance(project: Project): LookupManagerRemote } +@Remote("com.intellij.codeInsight.lookup.LookupManager") +interface LookupManagerRemote { fun getActiveLookup(): LookupRemote? } +@Remote("com.intellij.codeInsight.lookup.impl.LookupImpl") +interface LookupRemote { + fun getItems(): List + fun setCurrentItem(item: LookupItemRemote) +} +@Remote("com.intellij.codeInsight.lookup.LookupElement") +interface LookupItemRemote { fun getLookupString(): String } + class PackagedPluginTest { init { // A successful process exit alone cannot prove the plugin loaded: @@ -124,7 +197,14 @@ class PackagedPluginTest { val output = Path.of(System.getProperty("mcpp.clion.output") ?: "build/stable-smoke").toAbsolutePath() Files.createDirectories(output) Files.deleteIfExists(output.resolve("result.json")) - val fixture = Files.createTempDirectory(output, "project-") + Files.deleteIfExists(output.resolve("checks.json")) + val checked = linkedSetOf() + fun record(name: String) { + checked += name + val checks = checked.joinToString(",") { "\"$it\"" } + Files.writeString(output.resolve("checks.json"), """{"passed":[$checks]}""" + "\n") + } + val fixture = Files.createTempDirectory(output, "mcpp-用例 space-") Files.createDirectories(fixture.resolve("src")) Files.writeString(fixture.resolve("mcpp.toml"), "[package]\nname = \"clion-smoke\"\nversion = \"0.1.0\"\n") Files.writeString(fixture.resolve("src/main.cpp"), "#include \nint main() { auto* f = std::fopen(\"clion-run-ok.txt\", \"w\"); if (!f) return 1; std::fputs(\"run-ok\\n\", f); std::fclose(f); return 0; }\n") @@ -164,6 +244,7 @@ class PackagedPluginTest { assertTrue(future.isDone(), "the complete plugin startup chain finishes") val initialized = future.get() assertEquals(2, initialized.get("protocol").getAsInt()) + record("protocol2-startup") val backendVersion = initialized.get("serverInfo").getAsJsonObject().get("version").getAsString() assertFalse(session.getCommands().isEmpty(), "i18n/bundle and commands/list complete before startup succeeds") val actions = utility().getInstance() @@ -176,6 +257,7 @@ class PackagedPluginTest { assertNotNull(actions.getAction(target), "forwarded command $id resolves to $target") } } + record("offered-actions-and-forward-targets") assertFalse(session.getDegraded()) invokeAction("ActivatemcppToolWindow") val window = utility().getInstance(singleProject()).getToolWindow("mcpp") @@ -184,10 +266,13 @@ class PackagedPluginTest { while (!window!!.isVisible() && System.nanoTime() < visibleDeadline) Thread.sleep(100) assertTrue(window!!.isVisible(), "the tool window opens") assertEquals(3, window.getContentManager().getContentCount(), "Project, Libraries and Cache views load") + record("visible-three-view-tool-window") val runs = utility().getInstance(singleProject()) val actionResults = mutableMapOf() - for (command in listOf("build", "run", "test")) { + fun execute(command: String, label: String, success: Boolean = true): RunDescriptorRemote { val before = runs.getAllDescriptors().map { it.getExecutionId() }.toSet() + val previousHandlers = runs.getAllDescriptors().mapNotNull { it.getProcessHandler() } + .map { (it as RefWrapper).getRef().identityHashCode() }.toSet() val startedAt = System.nanoTime() invokeAction("mcpp.$command") val deadline = startedAt + java.util.concurrent.TimeUnit.SECONDS.toNanos(180) @@ -195,20 +280,97 @@ class PackagedPluginTest { while (System.nanoTime() < deadline) { completed = runs.getAllDescriptors().firstOrNull { it.getDisplayName().startsWith("mcpp $command ") && - it.getExecutionId() !in before && it.getProcessHandler()?.isProcessTerminated() == true + it.getExecutionId() !in before && it.getProcessHandler()?.let { handler -> + (handler as RefWrapper).getRef().identityHashCode() !in previousHandlers && handler.isProcessTerminated() + } == true } if (completed != null) break Thread.sleep(100) } assertNotNull(completed, "the real $command action finishes in the IDE") - assertEquals(0, completed!!.getProcessHandler()!!.getExitCode(), "$command exits successfully") - actionResults[command] = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt) + assertEquals(success, completed!!.getProcessHandler()!!.getExitCode() == 0, "$label has the expected exit status") + if (command == "test") { + val testRoot = cast(completed.getExecutionConsole(), TestConsoleRemote::class).getResultsViewer().getTestsRootNode() + assertFalse(testRoot.getChildren().isEmpty(), "the native test tree contains discovered tests") + assertEquals(!success, testRoot.isDefect(), "the test tree records the actual outcome") + if (success) assertTrue(testRoot.getChildren().all { it.isPassed() }, "test results reach the native tree") + record("native-test-tree-$label") + } + actionResults[label] = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt) + record("action-$label") + return completed } + for (command in listOf("build", "run", "test")) execute(command, command) assertEquals("run-ok\n", Files.readString(fixture.resolve("clion-run-ok.txt")), "the program actually ran") + record("program-executed") + val source = fixture.resolve("src/main.cpp") + val sourceBefore = Files.readString(source) + Files.writeString(source, "int main() { return missing_clion_smoke_symbol; }\n") + val failedBuild = execute("build", "build-failure", success = false) + val console = cast(failedBuild.getExecutionConsole(), TextConsoleRemote::class) + val consoleDeadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(10) + while (!console.getText().contains("missing_clion_smoke_symbol") && System.nanoTime() < consoleDeadline) Thread.sleep(100) + assertTrue(console.getText().contains("missing_clion_smoke_symbol"), "the compiler error reaches the visible run console") + record("compiler-error-visible") + Files.writeString(source, sourceBefore) + execute("build", "build-recovery") + val testSource = fixture.resolve("tests/check.cpp") + Files.writeString(testSource, "int main() { return 1; }\n") + execute("test", "test-failure", success = false) + Files.writeString(testSource, "int main() { return 0; }\n") + execute("test", "test-recovery") + for ((command, title) in listOf("mcpp.library.search" to "Libraries", "mcpp.showCachePanel" to "Cache")) { + invokeAction(command) + val deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(10) + while (window.getContentManager().getSelectedContent()?.getDisplayName() != title && System.nanoTime() < deadline) Thread.sleep(100) + assertEquals(title, window.getContentManager().getSelectedContent()?.getDisplayName(), "$command selects its native view") + record("view-$title") + } + val manifest = utility().getInstance().refreshAndFindFileByPath(fixture.resolve("mcpp.toml").toString()) + openEditor(requireNotNull(manifest)) + val editor = requireNotNull(utility().getInstance(singleProject()).getSelectedTextEditor()) + val manifestBefore = editor.getDocument().getText() + withWriteAction { editor.getDocument().setText(manifestBefore + "\n[dep") } + withContext(OnDispatcher.EDT) { editor.getCaretModel().moveToOffset(editor.getDocument().getText().length) } + val indexingAtCompletion = utility().getInstance(singleProject()).isDumb() + invokeAction("CodeCompletion") + val lookups = utility().getInstance(singleProject()) + val lookupDeadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(15) + var dependencies: LookupItemRemote? = null + while (System.nanoTime() < lookupDeadline) { + dependencies = lookups.getActiveLookup()?.getItems()?.firstOrNull { it.getLookupString() == "[dependencies]" } + if (dependencies != null) break + Thread.sleep(100) + } + assertNotNull(dependencies, "mcpp.toml completion is visible in the actual editor") + withContext(OnDispatcher.EDT) { lookups.getActiveLookup()!!.setCurrentItem(dependencies!!) } + invokeAction("EditorChooseLookupItem") + assertEquals(manifestBefore + "\n[dependencies]", editor.getDocument().getText(), "completion replaces exactly the requested range") + record("manifest-completion-and-insertion") + if (indexingAtCompletion) record("manifest-completion-during-indexing") + withContext(OnDispatcher.EDT) { + val pending = ArrayDeque() + pending.add(window.getContentManager().getSelectedContent()!!.getComponent()) + var cacheTree: TreeRemote? = null + while (pending.isNotEmpty()) { + val component = pending.removeFirst() + if (component.getClass().getName() == "com.intellij.ui.treeStructure.Tree") { + cacheTree = cast(component, TreeRemote::class) + break + } + pending.addAll(component.getComponents()) + } + assertNotNull(cacheTree, "the native cache tree exists") + val cacheDeadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(15) + while (cacheTree!!.getRowCount() == 0 && System.nanoTime() < cacheDeadline) Thread.sleep(100) + assertTrue(cacheTree!!.getRowCount() > 0, "opening the cache view loads its inventory without manual refresh") + assertFalse(cacheTree.getModel().getRoot().getChildAt(0).getUserObject().getError(), "the inventory query succeeds") + record("cache-inventory-visible") + } // Store only public versions and assertions. Raw IDE logs can // contain usernames, private paths or workspace details. val actionJson = actionResults.entries.joinToString(",") { "\"${it.key}\":${it.value}" } - Files.writeString(output.resolve("result.json"), """{"ideVersion":"$version","ideBuild":"$ideBuild","backendVersion":"$backendVersion","protocol":2,"startup":true,"commands":true,"registeredCommands":${session.getCommands().size},"toolWindowVisible":true,"toolWindowTabs":3,"actionDurationMs":{$actionJson},"programExecuted":true}""" + "\n") + Files.writeString(output.resolve("result.json"), """{"ideVersion":"$version","ideBuild":"$ideBuild","backendVersion":"$backendVersion","protocol":2,"startup":true,"commands":true,"registeredCommands":${session.getCommands().size},"toolWindowVisible":true,"toolWindowTabs":3,"actionDurationMs":{$actionJson},"programExecuted":true,"indexingAtCompletion":$indexingAtCompletion}""" + "\n") } } } diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt index 54ee858..221d91e 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/McppSession.kt @@ -352,9 +352,8 @@ class McppSession(private val project: Project) : Disposable { companion object { /** The platform owns plugin paths; modern plugin class loaders can omit CodeSource URLs. */ fun pluginDirectory(): File? = runCatching { - com.intellij.ide.plugins.PluginManagerCore.getPlugin( - com.intellij.openapi.extensions.PluginId.getId(PLUGIN_ID) - )?.pluginPath?.toFile() + (McppSession::class.java.classLoader as? com.intellij.ide.plugins.cl.PluginAwareClassLoader) + ?.pluginDescriptor?.pluginPath?.toFile() }.getOrNull() const val PLUGIN_ID = "io.github.mcpp-community.mcpp-clion" diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/editor/McppEditor.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/editor/McppEditor.kt index 0207c90..1d69887 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/editor/McppEditor.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/editor/McppEditor.kt @@ -31,6 +31,7 @@ import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.project.Project +import com.intellij.openapi.project.DumbAware import com.intellij.openapi.util.Key import com.intellij.openapi.util.TextRange import com.intellij.openapi.vfs.LocalFileSystem @@ -265,7 +266,7 @@ internal fun expandSnippet(snippet: String): Pair { } /** The backend's completion in mcpp.toml and build.mcpp. */ -class McppCompletion : CompletionContributor() { +class McppCompletion : CompletionContributor(), DumbAware { override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { val vf = parameters.originalFile.virtualFile ?: return if (!served(vf)) return diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/McppToolWindow.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/McppToolWindow.kt index afc1cad..4aca84e 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/McppToolWindow.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/ui/McppToolWindow.kt @@ -15,6 +15,8 @@ import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.openapi.wm.ToolWindowManager import com.intellij.ui.content.Content import com.intellij.ui.content.ContentFactory +import com.intellij.ui.content.ContentManagerListener +import com.intellij.ui.content.ContentManagerEvent import io.github.mcppcommunity.mcppclion.McppSession import io.github.mcppcommunity.mcppclion.actions.Dispatch import io.github.mcppcommunity.mcppclion.settings.McppConfigurable @@ -25,12 +27,19 @@ interface McppView { val id: String fun refresh() /** Shows what `params` names (a package of the libraries); the view itself when nothing. */ - fun focus(params: JsonObject) {} + fun focus(params: JsonObject) { refresh() } } object McppPanels { const val TOOL_WINDOW = "mcpp" private val VIEW = Key.create("mcpp.view") + private val FOCUS_PARAMS = Key.create("mcpp.focus.params") + + private fun focus(content: Content) { + val params = content.getUserData(FOCUS_PARAMS) ?: JsonObject() + content.putUserData(FOCUS_PARAMS, null) + content.getUserData(VIEW)?.focus(params) + } /** The command table's `panel` actions: a view brought on screen, a graph opened, settings shown. */ fun show(project: Project, panel: String, params: JsonObject = JsonObject()) { @@ -52,16 +61,20 @@ object McppPanels { private fun select(window: ToolWindow, id: String, params: JsonObject) { val content = window.contentManager.contents.firstOrNull { it.getUserData(VIEW)?.id == id } ?: return - window.contentManager.setSelectedContent(content, true) - content.getUserData(VIEW)?.focus(params) + select(window, content, params) + } + + private fun select(window: ToolWindow, content: Content, params: JsonObject) { + content.putUserData(FOCUS_PARAMS, params) + if (window.contentManager.selectedContent === content) focus(content) + else window.contentManager.setSelectedContent(content, true) } /** A graph is a tab of its own, closeable; one per kind, refreshed when it is asked again. */ private fun openGraph(project: Project, window: ToolWindow, kind: String) { val id = "graph:$kind" window.contentManager.contents.firstOrNull { it.getUserData(VIEW)?.id == id }?.let { - window.contentManager.setSelectedContent(it, true) - it.getUserData(VIEW)?.refresh() + select(window, it, JsonObject()) return } val session = McppSession.get(project) @@ -72,8 +85,7 @@ object McppPanels { content.putUserData(VIEW, page) Disposer.register(content, page) window.contentManager.addContent(content) - window.contentManager.setSelectedContent(content, true) - page.refresh() + select(window, content, JsonObject()) } class Factory : ToolWindowFactory, DumbAware { @@ -94,13 +106,18 @@ object McppPanels { add(projectView, projectView.component, session.t("Project")) add(libraryView, libraryView.component, session.t("Libraries")) add(cacheView, cacheView.component, session.t("Cache")) + toolWindow.contentManager.addContentManagerListener(object : ContentManagerListener { + override fun selectionChanged(event: ContentManagerEvent) { + if (event.operation == ContentManagerEvent.ContentOperation.add) focus(event.content) + } + }) // The strings and the data arrive with the backend. session.whenStarted { com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater { if (project.isDisposed) return@invokeLater val titles = mapOf("project" to "Project", "libraries" to "Libraries", "cache" to "Cache") for (c in toolWindow.contentManager.contents) c.getUserData(VIEW)?.id?.let { id -> titles[id]?.let { c.displayName = session.t(it) } } - projectView.refresh() + toolWindow.contentManager.selectedContent?.let(::focus) } } projectView.refresh() From c5aed8e3d8e33268c511d1c537ebe09eb24c12ee Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 18:49:34 +0900 Subject: [PATCH 4/5] fix: start native CLion debug sessions and repair stable CI setup --- ...ion-backend-location-and-stable-runtime.md | 23 ++++++-- .github/workflows/clion-stable.yml | 3 + .gitignore | 2 +- docs/architecture.md | 4 +- mide-plugins/clion/README.md | 3 +- .../src/test/kotlin/PackagedPluginTest.kt | 57 +++++++++++++------ .../mcppclion/run/McppDebugRunner.kt | 7 ++- 7 files changed, 72 insertions(+), 27 deletions(-) diff --git a/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md b/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md index 17ed4ef..4a32720 100644 --- a/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md +++ b/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md @@ -61,8 +61,9 @@ against each tested SDK. A manual release check can reuse the published ZIP with the backend. Only a report of public versions and assertions is uploaded. Plugin Verifier remains a separate static compatibility check. These runtime checks cover -Linux startup and views on the two stable IDE families; native Windows/macOS GUI behavior, -dynamic uninstall/reinstall, debugging and every project action require their own evidence. +Linux startup and the functional scenarios below on the two stable IDE families; native +Windows/macOS GUI behavior, dynamic uninstall/reinstall and remaining interactions require +their own evidence. The null CodeSource reproduction uses a cold IDE start and does not depend on dynamic installation. ## Functional and user-experience coverage @@ -73,7 +74,7 @@ The runtime table can offer fewer entries according to the language service's ad capabilities. Every offered command and each forwarded action must resolve to a registered IDE action; hidden or deprecated commands are not counted as executed functionality. -The local 2026.1.5.2 and 2026.2.4.1 experiments pass the same 20 checks. They invoke the actual +The local 2026.1.5.2 and 2026.2.4.1 experiments pass the same 23 checks. They invoke the actual Build, Run and Test actions, wait for fresh process handlers, require the expected exit code, and check a file written by the program. A compilation failure must appear in the console; restoring the source must restore a successful build. Test failure and recovery must reach the @@ -88,6 +89,20 @@ analysis. Marking that contributor allows a real completion popup and exact repl left the tree empty until manual refresh. Content selection now focuses and refreshes the view, including command selection and tab selection, and the runtime check requires real inventory rows without an error. The same selection path handles existing graph tabs and refresh requests. +Both versions also pass physical mouse selection of the Libraries and Cache tabs. + +The native debugger adapter constructed `CidrLocalDebugProcess` without starting it. The +published adapter consequently opened a debug view without loading the target. Starting the +process inside the session starter follows the native launch lifecycle. Both stable versions +now execute the actual Debug action, finish with exit code zero through a native +`CidrDebugProcess` process handler, and expose the Debug tool window. Breakpoints, variables, +stepping and stop remain separate pending scenarios. + +The first canonical CI run passed the plugin build, static verification and other ecosystem +checks. Both stable runtime jobs stopped during xlings installation when the installer tried +to read `/dev/tty`; neither reached IDE startup. The reusable workflow now sets +`XLINGS_NON_INTERACTIVE=1` itself, because the caller's workflow environment is not inherited. +The corrected canonical runtime jobs still require a successful run. Verifier 1.411 confirms both stable builds are compatible with zero internal API usages after using the public plugin-aware class loader descriptor. The original deprecated/experimental API @@ -102,7 +117,7 @@ tests serialize use of the Driver's ports; CI matrix entries use independent run | Build | Success, compilation failure, incremental build, cancellation, console source links | Success, compiler error text, failure and recovery pass; cancellation and links pending | | Run | Executable selection, arguments, working directory, PTY, stop | Successful IDE action and program-produced file; other interactions pending | | Test | Discovery, success, failure, filter, test tree and rerun | Native tree success, failure and corrected rerun pass; filter pending | -| Debug | Native breakpoint, variables, step, continue, stop; runner-backed program boundary | Pending actual debugger session | +| Debug | Native breakpoint, variables, step, continue, stop; runner-backed program boundary | Actual native debug launch, process completion and Debug view pass on both stable versions; breakpoint interactions and runner-backed program pending | | Project | Data rows, targets, result/artifact links, refresh | Three views instantiate; populated rows and links pending | | Libraries | Search, detail, add dependency, index update, error feedback | Pending functional interaction | | Cache | Inventory, plan, confirmation/cancel, isolated apply, refused untrusted actions | Opening the view loads real inventory without manual refresh; plan/apply/refusal pending; no global user cache deletion | diff --git a/.github/workflows/clion-stable.yml b/.github/workflows/clion-stable.yml index e27dbb3..304f5c7 100644 --- a/.github/workflows/clion-stable.yml +++ b/.github/workflows/clion-stable.yml @@ -16,6 +16,9 @@ on: permissions: contents: read +env: + XLINGS_NON_INTERACTIVE: '1' + jobs: runtime: name: packaged plugin on CLion ${{ matrix.version }} diff --git a/.gitignore b/.gitignore index 26e4a91..b6c05ef 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ webui/dist/ compile_commands.json dist-upload/ .xlings/ -out/ide-tests/ +out/ide-tests diff --git a/docs/architecture.md b/docs/architecture.md index 711ffe9..0f48cbb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -99,7 +99,9 @@ class loader's `CodeSource` URL is not required. The session accepts protocol 2 `mcpp-ide/i18n/bundle` and `mcpp-ide/commands/list`. Backend selection and startup failures are recorded in `idea.log`; RPC startup failures include the method and error code. The stable CLion runtime checks install one packaged plugin ZIP in both stable IDE families and exercise -the complete startup chain in each IDE process. +the complete startup chain in each IDE process. Functional checks cover build, run, test, +native debug launch, error recovery, manifest completion and view selection. The debugger +adapter starts the native Cidr process from the session starter before returning it. ## Generated declarations diff --git a/mide-plugins/clion/README.md b/mide-plugins/clion/README.md index 066f106..e5d763b 100644 --- a/mide-plugins/clion/README.md +++ b/mide-plugins/clion/README.md @@ -47,7 +47,8 @@ The packaged ZIP is also run in the real stable CLion 2026.1.5.2 and 2026.2.4.1 `clion-stable.yml`. Both versions load the same ZIP and the pinned mcppls plugin. The test opens an mcpp project, completes protocol 2 initialization, loads runtime translations and commands, and verifies the visible mcpp tool window and cache inventory. It executes build, run and test -actions in a project whose path contains Unicode and spaces, checks the native test tree, +actions in a project whose path contains Unicode and spaces, launches the native debugger, +checks its process completion and visible view, checks the native test tree, compiler-error output, build/test failure and recovery, and exact manifest completion insertion during project analysis. It supplements Plugin Verifier's static compatibility checks. diff --git a/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt b/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt index 509b2b1..0ff43cc 100644 --- a/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt +++ b/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt @@ -7,6 +7,7 @@ import com.intellij.driver.sdk.Project import com.intellij.driver.sdk.FileEditorManager import com.intellij.driver.sdk.openEditor import com.intellij.driver.sdk.VirtualFile +import com.intellij.driver.sdk.ui.ui import com.intellij.driver.model.OnDispatcher import com.intellij.driver.sdk.singleProject import com.intellij.driver.sdk.waitForProjectOpen @@ -33,6 +34,7 @@ import org.kodein.di.direct import org.kodein.di.instance import java.nio.file.Files import java.nio.file.Path +import kotlin.time.Duration.Companion.minutes private const val PLUGIN_ID = "io.github.mcpp-community.mcpp-clion" @@ -139,8 +141,16 @@ interface RunDescriptorRemote { interface ProcessHandlerRemote { fun isProcessTerminated(): Boolean fun getExitCode(): Int? + fun getClass(): ClassNameRemote } +@Remote("com.jetbrains.cidr.cpp.toolchains.CPPToolchains", plugin = PLUGIN_ID) +interface CppToolchainsFactory { fun getInstance(): CppToolchainsRemote } +@Remote("com.jetbrains.cidr.cpp.toolchains.CPPToolchains", plugin = PLUGIN_ID) +interface CppToolchainsRemote { fun getDefaultToolchain(): CppToolchainRemote? } +@Remote("com.jetbrains.cidr.cpp.toolchains.CPPToolchains\$Toolchain", plugin = PLUGIN_ID) +interface CppToolchainRemote + @Remote("com.intellij.execution.ui.ExecutionConsole") interface ExecutionConsoleRemote @Remote("com.intellij.execution.impl.ConsoleViewImpl") @@ -231,7 +241,7 @@ class PackagedPluginTest { addSystemProperty("idea.trust.all.projects", true) addSystemProperty("ide.show.tips.on.startup.default.value", false) } - context.runIdeWithDriver().useDriverAndCloseIde { + context.runIdeWithDriver(runTimeout = 3.minutes).useDriverAndCloseIde { waitForProjectOpen() withContext { val application = utility().getInstance() @@ -275,7 +285,7 @@ class PackagedPluginTest { .map { (it as RefWrapper).getRef().identityHashCode() }.toSet() val startedAt = System.nanoTime() invokeAction("mcpp.$command") - val deadline = startedAt + java.util.concurrent.TimeUnit.SECONDS.toNanos(180) + val deadline = startedAt + java.util.concurrent.TimeUnit.SECONDS.toNanos(if (command == "debug") 60 else 180) var completed: RunDescriptorRemote? = null while (System.nanoTime() < deadline) { completed = runs.getAllDescriptors().firstOrNull { @@ -319,6 +329,12 @@ class PackagedPluginTest { execute("test", "test-failure", success = false) Files.writeString(testSource, "int main() { return 0; }\n") execute("test", "test-recovery") + assertNotNull(utility().getInstance().getDefaultToolchain(), "CLion has a debugger toolchain") + val debug = execute("debug", "debug-native-launch") + assertTrue(debug.getProcessHandler()!!.getClass().getName().contains("CidrDebugProcess"), "CLion's native debugger owns the process") + val debugWindow = utility().getInstance(singleProject()).getToolWindow("Debug") + assertTrue(debugWindow?.isVisible() == true, "the native debugger view is visible") + record("native-debugger-process-and-view") for ((command, title) in listOf("mcpp.library.search" to "Libraries", "mcpp.showCachePanel" to "Cache")) { invokeAction(command) val deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(10) @@ -326,6 +342,11 @@ class PackagedPluginTest { assertEquals(title, window.getContentManager().getSelectedContent()?.getDisplayName(), "$command selects its native view") record("view-$title") } + for (title in listOf("Libraries", "Cache")) { + ui.x("//div[@visible_text='$title' and contains(@javaclass,'ContentTabLabel')]").click() + assertEquals(title, window.getContentManager().getSelectedContent()?.getDisplayName(), "a mouse click selects $title") + record("mouse-tab-$title") + } val manifest = utility().getInstance().refreshAndFindFileByPath(fixture.resolve("mcpp.toml").toString()) openEditor(requireNotNull(manifest)) val editor = requireNotNull(utility().getInstance(singleProject()).getSelectedTextEditor()) @@ -349,23 +370,23 @@ class PackagedPluginTest { record("manifest-completion-and-insertion") if (indexingAtCompletion) record("manifest-completion-during-indexing") withContext(OnDispatcher.EDT) { - val pending = ArrayDeque() - pending.add(window.getContentManager().getSelectedContent()!!.getComponent()) - var cacheTree: TreeRemote? = null - while (pending.isNotEmpty()) { - val component = pending.removeFirst() - if (component.getClass().getName() == "com.intellij.ui.treeStructure.Tree") { - cacheTree = cast(component, TreeRemote::class) - break - } - pending.addAll(component.getComponents()) + val pending = ArrayDeque() + pending.add(window.getContentManager().getSelectedContent()!!.getComponent()) + var cacheTree: TreeRemote? = null + while (pending.isNotEmpty()) { + val component = pending.removeFirst() + if (component.getClass().getName() == "com.intellij.ui.treeStructure.Tree") { + cacheTree = cast(component, TreeRemote::class) + break } - assertNotNull(cacheTree, "the native cache tree exists") - val cacheDeadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(15) - while (cacheTree!!.getRowCount() == 0 && System.nanoTime() < cacheDeadline) Thread.sleep(100) - assertTrue(cacheTree!!.getRowCount() > 0, "opening the cache view loads its inventory without manual refresh") - assertFalse(cacheTree.getModel().getRoot().getChildAt(0).getUserObject().getError(), "the inventory query succeeds") - record("cache-inventory-visible") + pending.addAll(component.getComponents()) + } + assertNotNull(cacheTree, "the native cache tree exists") + val cacheDeadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(15) + while (cacheTree!!.getRowCount() == 0 && System.nanoTime() < cacheDeadline) Thread.sleep(100) + assertTrue(cacheTree!!.getRowCount() > 0, "opening the cache view loads its inventory without manual refresh") + assertFalse(cacheTree.getModel().getRoot().getChildAt(0).getUserObject().getError(), "the inventory query succeeds") + record("cache-inventory-visible") } // Store only public versions and assertions. Raw IDE logs can // contain usernames, private paths or workspace details. diff --git a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppDebugRunner.kt b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppDebugRunner.kt index f82dcf3..890213b 100644 --- a/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppDebugRunner.kt +++ b/mide-plugins/clion/src/main/kotlin/io/github/mcppcommunity/mcppclion/run/McppDebugRunner.kt @@ -48,8 +48,11 @@ class McppDebugRunner : GenericProgramRunner() { session.notify(session.t("This program runs through {0}. LLDB reads its symbols once `settings set plugin.jit-loader.gdb.enable on` is in ~/.lldbinit; GDB reads them as it is.", launch["via"].asString), NotificationType.INFORMATION) val parameters = TrivialRunParameters(driver, commandLine(launch, pty = false)) return XDebuggerManager.getInstance(project).newSessionBuilder(object : XDebugProcessStarter() { - override fun start(xs: XDebugSession): XDebugProcess = - CidrLocalDebugProcess(parameters, xs, TextConsoleBuilderFactory.getInstance().createBuilder(project)) + override fun start(xs: XDebugSession): XDebugProcess { + val process = CidrLocalDebugProcess(parameters, xs, TextConsoleBuilderFactory.getInstance().createBuilder(project)) + process.start() + return process + } }).environment(env).startSession().runContentDescriptor } } From 4bb18689600f5c5ff43d0ba5390d38b3cde1bde2 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 5 Nov 2026 19:14:16 +0900 Subject: [PATCH 5/5] test: verify native CLion breakpoints stepping and stop --- ...ion-backend-location-and-stable-runtime.md | 18 +++-- mide-plugins/clion/README.md | 2 +- .../src/test/kotlin/PackagedPluginTest.kt | 75 +++++++++++++++++-- 3 files changed, 81 insertions(+), 14 deletions(-) diff --git a/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md b/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md index 4a32720..d8bd0bf 100644 --- a/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md +++ b/.agents/docs/2026-11-05-clion-backend-location-and-stable-runtime.md @@ -1,6 +1,6 @@ # CLion backend location and stable runtime validation -Status: implemented on the review branch; stable CI validation pending. +Status: fixes and the initial stable runtime suite pass canonical CI; expanded debugger CI pending. ## Observed failure @@ -74,7 +74,7 @@ The runtime table can offer fewer entries according to the language service's ad capabilities. Every offered command and each forwarded action must resolve to a registered IDE action; hidden or deprecated commands are not counted as executed functionality. -The local 2026.1.5.2 and 2026.2.4.1 experiments pass the same 23 checks. They invoke the actual +The local 2026.1.5.2 and 2026.2.4.1 experiments pass the same 29 checks. They invoke the actual Build, Run and Test actions, wait for fresh process handlers, require the expected exit code, and check a file written by the program. A compilation failure must appear in the console; restoring the source must restore a successful build. Test failure and recovery must reach the @@ -95,14 +95,20 @@ The native debugger adapter constructed `CidrLocalDebugProcess` without starting published adapter consequently opened a debug view without loading the target. Starting the process inside the session starter follows the native launch lifecycle. Both stable versions now execute the actual Debug action, finish with exit code zero through a native -`CidrDebugProcess` process handler, and expose the Debug tool window. Breakpoints, variables, -stepping and stop remain separate pending scenarios. +`CidrDebugProcess` process handler, and expose the Debug tool window. Both versions then stop +at a real source breakpoint, step to the next source line, resume to normal completion and +check program output. A separate debug run stops at the breakpoint and must terminate before +the program writes its output. Variable inspection and runner-backed debugging remain pending. The first canonical CI run passed the plugin build, static verification and other ecosystem checks. Both stable runtime jobs stopped during xlings installation when the installer tried to read `/dev/tty`; neither reached IDE startup. The reusable workflow now sets `XLINGS_NON_INTERACTIVE=1` itself, because the caller's workflow environment is not inherited. -The corrected canonical runtime jobs still require a successful run. +Canonical run 42005596492 at commit `c5aed8e` subsequently passed all 25 jobs, including +the actual packaged plugin on both stable IDE builds. The stable runtime reports contain +23 successful checks on 2026.1 and 24 on 2026.2, including completion while indexing on the +latter. The added breakpoint, step, resume and stop cases have passed locally on both IDEs; +the expanded canonical suite still requires its own successful run. Verifier 1.411 confirms both stable builds are compatible with zero internal API usages after using the public plugin-aware class loader descriptor. The original deprecated/experimental API @@ -117,7 +123,7 @@ tests serialize use of the Driver's ports; CI matrix entries use independent run | Build | Success, compilation failure, incremental build, cancellation, console source links | Success, compiler error text, failure and recovery pass; cancellation and links pending | | Run | Executable selection, arguments, working directory, PTY, stop | Successful IDE action and program-produced file; other interactions pending | | Test | Discovery, success, failure, filter, test tree and rerun | Native tree success, failure and corrected rerun pass; filter pending | -| Debug | Native breakpoint, variables, step, continue, stop; runner-backed program boundary | Actual native debug launch, process completion and Debug view pass on both stable versions; breakpoint interactions and runner-backed program pending | +| Debug | Native breakpoint, variables, step, continue, stop; runner-backed program boundary | Native debug launch, breakpoint, step over, resume, normal completion and stop pass on both stable versions; variables and runner-backed program pending | | Project | Data rows, targets, result/artifact links, refresh | Three views instantiate; populated rows and links pending | | Libraries | Search, detail, add dependency, index update, error feedback | Pending functional interaction | | Cache | Inventory, plan, confirmation/cancel, isolated apply, refused untrusted actions | Opening the view loads real inventory without manual refresh; plan/apply/refusal pending; no global user cache deletion | diff --git a/mide-plugins/clion/README.md b/mide-plugins/clion/README.md index e5d763b..129bbf6 100644 --- a/mide-plugins/clion/README.md +++ b/mide-plugins/clion/README.md @@ -48,7 +48,7 @@ The packaged ZIP is also run in the real stable CLion 2026.1.5.2 and 2026.2.4.1 an mcpp project, completes protocol 2 initialization, loads runtime translations and commands, and verifies the visible mcpp tool window and cache inventory. It executes build, run and test actions in a project whose path contains Unicode and spaces, launches the native debugger, -checks its process completion and visible view, checks the native test tree, +checks its visible view, source breakpoint, step over, resume and stop, checks the native test tree, compiler-error output, build/test failure and recovery, and exact manifest completion insertion during project analysis. It supplements Plugin Verifier's static compatibility checks. diff --git a/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt b/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt index 0ff43cc..d3742d3 100644 --- a/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt +++ b/mide-plugins/clion/integration-tests/src/test/kotlin/PackagedPluginTest.kt @@ -6,6 +6,7 @@ import com.intellij.driver.sdk.invokeAction import com.intellij.driver.sdk.Project import com.intellij.driver.sdk.FileEditorManager import com.intellij.driver.sdk.openEditor +import com.intellij.driver.sdk.XDebuggerUtil import com.intellij.driver.sdk.VirtualFile import com.intellij.driver.sdk.ui.ui import com.intellij.driver.model.OnDispatcher @@ -151,6 +152,21 @@ interface CppToolchainsRemote { fun getDefaultToolchain(): CppToolchainRemote? } @Remote("com.jetbrains.cidr.cpp.toolchains.CPPToolchains\$Toolchain", plugin = PLUGIN_ID) interface CppToolchainRemote +@Remote("com.intellij.xdebugger.XDebuggerManager") +interface DebuggerManagerFactory { fun getInstance(project: Project): DebuggerManagerRemote } +@Remote("com.intellij.xdebugger.XDebuggerManager") +interface DebuggerManagerRemote { fun getCurrentSession(): DebugSessionRemote? } +@Remote("com.intellij.xdebugger.XDebugSession") +interface DebugSessionRemote { + fun isSuspended(): Boolean + fun getCurrentPosition(): DebugPositionRemote? + fun resume() + fun stepOver(ignoreBreakpoints: Boolean) + fun stop() +} +@Remote("com.intellij.xdebugger.XSourcePosition") +interface DebugPositionRemote { fun getLine(): Int } + @Remote("com.intellij.execution.ui.ExecutionConsole") interface ExecutionConsoleRemote @Remote("com.intellij.execution.impl.ConsoleViewImpl") @@ -217,7 +233,18 @@ class PackagedPluginTest { val fixture = Files.createTempDirectory(output, "mcpp-用例 space-") Files.createDirectories(fixture.resolve("src")) Files.writeString(fixture.resolve("mcpp.toml"), "[package]\nname = \"clion-smoke\"\nversion = \"0.1.0\"\n") - Files.writeString(fixture.resolve("src/main.cpp"), "#include \nint main() { auto* f = std::fopen(\"clion-run-ok.txt\", \"w\"); if (!f) return 1; std::fputs(\"run-ok\\n\", f); std::fclose(f); return 0; }\n") + Files.writeString(fixture.resolve("src/main.cpp"), """ + #include + int main() { + int answer = 40; + answer += 2; + auto* f = std::fopen("clion-run-ok.txt", "w"); + if (!f) return 1; + std::fputs("run-ok\n", f); + std::fclose(f); + return answer == 42 ? 0 : 2; + } + """.trimIndent() + "\n") Files.createDirectories(fixture.resolve("tests")) Files.writeString(fixture.resolve("tests/check.cpp"), "int main() { return 0; }\n") val localHome = System.getProperty("mcpp.clion.ideHome")?.let(Path::of) @@ -279,12 +306,13 @@ class PackagedPluginTest { record("visible-three-view-tool-window") val runs = utility().getInstance(singleProject()) val actionResults = mutableMapOf() - fun execute(command: String, label: String, success: Boolean = true): RunDescriptorRemote { + fun execute(command: String, label: String, success: Boolean? = true, whileRunning: (() -> Unit)? = null): RunDescriptorRemote { val before = runs.getAllDescriptors().map { it.getExecutionId() }.toSet() val previousHandlers = runs.getAllDescriptors().mapNotNull { it.getProcessHandler() } .map { (it as RefWrapper).getRef().identityHashCode() }.toSet() val startedAt = System.nanoTime() invokeAction("mcpp.$command") + whileRunning?.invoke() val deadline = startedAt + java.util.concurrent.TimeUnit.SECONDS.toNanos(if (command == "debug") 60 else 180) var completed: RunDescriptorRemote? = null while (System.nanoTime() < deadline) { @@ -298,17 +326,18 @@ class PackagedPluginTest { Thread.sleep(100) } assertNotNull(completed, "the real $command action finishes in the IDE") - assertEquals(success, completed!!.getProcessHandler()!!.getExitCode() == 0, "$label has the expected exit status") + val finished = requireNotNull(completed) + if (success != null) assertEquals(success, finished.getProcessHandler()!!.getExitCode() == 0, "$label has the expected exit status") if (command == "test") { - val testRoot = cast(completed.getExecutionConsole(), TestConsoleRemote::class).getResultsViewer().getTestsRootNode() + val testRoot = cast(finished.getExecutionConsole(), TestConsoleRemote::class).getResultsViewer().getTestsRootNode() assertFalse(testRoot.getChildren().isEmpty(), "the native test tree contains discovered tests") - assertEquals(!success, testRoot.isDefect(), "the test tree records the actual outcome") - if (success) assertTrue(testRoot.getChildren().all { it.isPassed() }, "test results reach the native tree") + assertEquals(success == false, testRoot.isDefect(), "the test tree records the actual outcome") + if (success == true) assertTrue(testRoot.getChildren().all { it.isPassed() }, "test results reach the native tree") record("native-test-tree-$label") } actionResults[label] = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt) record("action-$label") - return completed + return finished } for (command in listOf("build", "run", "test")) execute(command, command) assertEquals("run-ok\n", Files.readString(fixture.resolve("clion-run-ok.txt")), "the program actually ran") @@ -335,6 +364,38 @@ class PackagedPluginTest { val debugWindow = utility().getInstance(singleProject()).getToolWindow("Debug") assertTrue(debugWindow?.isVisible() == true, "the native debugger view is visible") record("native-debugger-process-and-view") + val debugger = utility().getInstance(singleProject()) + val debugFile = requireNotNull(utility().getInstance().refreshAndFindFileByPath(source.toString())) + openEditor(debugFile) + withContext(OnDispatcher.EDT) { service().toggleLineBreakpoint(singleProject(), debugFile, 3, false) } + fun suspendedAt(line: Int): DebugSessionRemote { + val deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(30) + while (System.nanoTime() < deadline) { + val current = debugger.getCurrentSession() + if (current?.isSuspended() == true && current.getCurrentPosition()?.getLine() == line) return current + Thread.sleep(100) + } + error("the native debugger did not suspend at fixture line $line") + } + Files.delete(fixture.resolve("clion-run-ok.txt")) + execute("debug", "debug-breakpoint-step-resume", whileRunning = { + val paused = suspendedAt(3) + record("debug-breakpoint") + withContext(OnDispatcher.EDT) { paused.stepOver(false) } + val stepped = suspendedAt(4) + record("debug-step-over") + withContext(OnDispatcher.EDT) { stepped.resume() } + }) + assertEquals("run-ok\n", Files.readString(fixture.resolve("clion-run-ok.txt")), "resume completes the debugged program") + record("debug-resume-program-output") + Files.delete(fixture.resolve("clion-run-ok.txt")) + execute("debug", "debug-stop", success = null, whileRunning = { + val paused = suspendedAt(3) + withContext(OnDispatcher.EDT) { paused.stop() } + }) + assertFalse(Files.exists(fixture.resolve("clion-run-ok.txt")), "stop at the breakpoint prevents subsequent program work") + record("debug-stop-prevents-program-work") + withContext(OnDispatcher.EDT) { service().toggleLineBreakpoint(singleProject(), debugFile, 3, false) } for ((command, title) in listOf("mcpp.library.search" to "Libraries", "mcpp.showCachePanel" to "Cache")) { invokeAction(command) val deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(10)