diff --git a/.agents/docs/2026-11-10-tests-from-mcpp.md b/.agents/docs/2026-11-10-tests-from-mcpp.md
index f4eaadc..3782f61 100644
--- a/.agents/docs/2026-11-10-tests-from-mcpp.md
+++ b/.agents/docs/2026-11-10-tests-from-mcpp.md
@@ -92,7 +92,9 @@ A result names the program it belongs to:
A whole-program run therefore marks every case.
- `test/caseResult`: gains the fields of a case above. It is sent for every record of the job, not
- only the last one; a run of a directory program writes one record per part.
+ only the last one; a run of a directory program writes one record per part. It names the program
+ and the part as the record does, by the rule of a result above, whatever the job's filter; mcpp
+ 2026.11.9.1 names a run that selects cases (`--case`) after the program, and its result has no part.
- `test/caseEvent` (new): `{jobId, root, program, part, case, event: "begin" | "end", tMs, ...}` with
the fields of a case on `end`. It is sent as mcpp writes a `case` record (`test-case-stream`).
- `test/history {root, program?, case?, runs?}` (new): the store's last runs (20 by default) and each
@@ -151,7 +153,7 @@ insights are empty and say so.
- A failure's `expected` and `actual` appear as two lines under it, and in its quickfix text.
- A case's line carries its sign (✓, ✗) and, as virtual text, its duration and its metrics against
their bounds.
-- `:Mcpp testInsights` opens the insights as text in a floating window. A metric's trend is drawn with
+- `:Mcpp test.insights` opens the insights as text in a floating window. A metric's trend is drawn with
block characters (▁▂▃▅▇), and the schedule as bars.
## Performance
@@ -178,6 +180,6 @@ insights are empty and say so.
directory's `.mock-mcpp-tests` selects them, as `.mock-mcpp-features` selects the features.
- VS Code: the tree of a directory program; part results attributed to it; the diff message; the
decorations' text.
-- Neovim: the smoke test covers live results, the virtual text and `:Mcpp testInsights`.
+- Neovim: the smoke test covers live results, the virtual text and `:Mcpp test.insights`.
- CLion: the comparison failure and the nested suites, in the plugin's unit tests.
- The insights page is rendered by a Chromium test, as the graph page is.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8761bba..2c2b194 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,19 @@
The changelog is kept by series (`X.Y`); each section lists its components' releases.
+## 1.4
+
+### 1.4.0
+
+Every component is 1.4.0: the backend, mcpp-vscode, mcpp-clion and mcpp-nvim (from 1.3.0).
+
+- **A run of cases through a part names its program.** `test/caseResult` names the program and the part
+ as the record states them, as `test/result` does. 1.3.0 named the job's filter as the program: cases
+ run through a part (`unit/a`) were reported as the program `unit/a`, and CLion and Neovim placed them
+ under a suite of that name. mcpp names a run that selects cases (`--case`) after the program; such a
+ result carries no part.
+- **Docs.** The Neovim command of the test insights is `:Mcpp test.insights`.
+
## 1.3
### 1.3.0
diff --git a/mcpp.toml b/mcpp.toml
index 5c31729..e2a6337 100644
--- a/mcpp.toml
+++ b/mcpp.toml
@@ -9,7 +9,7 @@
# M2/M3), where the system it runs upon is answered when the image is loaded.
[package]
name = "mcpp-ide"
-version = "1.3.0"
+version = "1.4.0"
description = "The IDE backend for mcpp: sessions, jobs, records and view models over mcpp's control contract"
license = "Apache-2.0"
authors = ["mcpp-community"]
diff --git a/mide-plugins/clion/README.md b/mide-plugins/clion/README.md
index 79dfc74..a021d5c 100644
--- a/mide-plugins/clion/README.md
+++ b/mide-plugins/clion/README.md
@@ -99,7 +99,7 @@ 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-1.3.0.zip \
+ -PpluginZip=/absolute/path/mcpp-clion-1.4.0.zip \
-PmcpplsZip=/absolute/path/mcppls-clion-0.21.0.zip \
-PideVersion=2026.1.5.2
```
diff --git a/mide-plugins/clion/gradle.properties b/mide-plugins/clion/gradle.properties
index d53b182..2bdbf07 100644
--- a/mide-plugins/clion/gradle.properties
+++ b/mide-plugins/clion/gradle.properties
@@ -2,7 +2,7 @@
# where it installs (2026.1 and later); the verifier checks both ends.
platformType = CL
platformVersion = 2026.2.3
-pluginVersion = 1.3.0
+pluginVersion = 1.4.0
# The mcppls CLion plugin this one depends on (io.github.sunrisepeak.mcppls):
# its release's one package for every system, fetched for the sandbox, the tests and the verifier.
mcpplsPluginVersion = 0.21.0
diff --git a/mide-plugins/nvim/lua/mcpp/session.lua b/mide-plugins/nvim/lua/mcpp/session.lua
index 86d4b05..b3be537 100644
--- a/mide-plugins/nvim/lua/mcpp/session.lua
+++ b/mide-plugins/nvim/lua/mcpp/session.lua
@@ -13,7 +13,7 @@ local uv = vim.uv or vim.loop
local is_windows = vim.fn.has('win32') == 1
-- mcpp-nvim's version: mcpp-ide's, whose release carries it.
-M.version = '1.3.0'
+M.version = '1.4.0'
-- Neovim's version, as its release names it.
function M.nvim_version()
diff --git a/mide-plugins/nvim/lua/mcpp/tests.lua b/mide-plugins/nvim/lua/mcpp/tests.lua
index d899745..d2df27c 100644
--- a/mide-plugins/nvim/lua/mcpp/tests.lua
+++ b/mide-plugins/nvim/lua/mcpp/tests.lua
@@ -323,7 +323,7 @@ function M.said_of(root, test, case)
return said[(root or '') .. '\0' .. test .. '\0' .. case]
end
--- ── the insights as text (`:Mcpp testInsights`) ──────────────────────────────────────────────
+-- ── the insights as text (`:Mcpp test.insights`) ──────────────────────────────────────────────
local BLOCKS = { '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█' }
diff --git a/mide-plugins/vscode/package-lock.json b/mide-plugins/vscode/package-lock.json
index 1d3c683..13da408 100644
--- a/mide-plugins/vscode/package-lock.json
+++ b/mide-plugins/vscode/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "mcpp-vscode",
- "version": "1.3.0",
+ "version": "1.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mcpp-vscode",
- "version": "1.3.0",
+ "version": "1.4.0",
"license": "Apache-2.0",
"devDependencies": {
"@mermaid-js/tiny": "12.2.0",
diff --git a/mide-plugins/vscode/package.json b/mide-plugins/vscode/package.json
index c4c9524..dbe6474 100644
--- a/mide-plugins/vscode/package.json
+++ b/mide-plugins/vscode/package.json
@@ -2,7 +2,7 @@
"name": "mcpp-vscode",
"displayName": "%displayName%",
"description": "%description%",
- "version": "1.3.0",
+ "version": "1.4.0",
"publisher": "mcpp-community",
"license": "Apache-2.0",
"icon": "media/logo.png",
diff --git a/src/server.cppm b/src/server.cppm
index 0e7f14b..6673ac5 100644
--- a/src/server.cppm
+++ b/src/server.cppm
@@ -35,13 +35,13 @@ export namespace mcppide::server {
using Json = nlohmann::json;
-inline constexpr std::string_view kVersion = "1.3.0";
+inline constexpr std::string_view kVersion = "1.4.0";
// THE SERIES (mcpp design 2026-11-07, D15): `X.Y` of `X.Y.Z`, shared by the backend and every editor
// plugin; components of one series work together. A plugin refuses a backend of another series,
// naming both versions. The backend is always `X.Y.0`: the VS Code and CLion packages carry it, so a
// change of the backend is a change of every package, and it begins a series (1.3.0). `Z` is an
// editor plugin's own patch.
-inline constexpr std::string_view kSeries = "1.3";
+inline constexpr std::string_view kSeries = "1.4";
static_assert(kVersion.starts_with(kSeries) && kVersion.size() == kSeries.size() + 2 && kVersion.ends_with(".0"));
inline constexpr int kProtocol = 2;
@@ -1848,10 +1848,17 @@ Json Session::test_result_of(const std::filesystem::path& root, const Json& reco
}
// The cases of one record of a test/runCases job, each sent as `test/caseResult` with its program
-// and part, a failure's message as its output.
+// and part, a failure's message as its output. The record names them, as a test/result's does: the
+// job's filter may be a part, and mcpp names a run that selects cases after the program.
void Session::protocol_case_results(std::string_view jobId, const std::filesystem::path& root, const std::string& test,
const Json& record) {
+ std::vector programs;
+ {
+ std::lock_guard lock(mutex_);
+ if (auto it = programs_.find(root); it != programs_.end()) programs = it->second;
+ }
const auto part = record.value("test", "");
+ const auto program = testing::owner_of(part, record.value("part_of", ""), programs);
for (const auto& o : testing::protocol_outcomes(record)) {
bool truncated = false;
auto output = views::bounded_output(o.outcome.status == "failed" ? std::string_view(o.outcome.message) : std::string_view{}, truncated);
@@ -1859,8 +1866,8 @@ void Session::protocol_case_results(std::string_view jobId, const std::filesyste
n["jobId"] = jobId;
n["root"] = root.generic_string();
n["test"] = test;
- n["program"] = test;
- if (part != test) n["part"] = part;
+ n["program"] = program;
+ if (part != program) n["part"] = part;
n["output"] = std::move(output);
n["outputTruncated"] = truncated;
rpc_.notify("mcpp-ide/test/caseResult", n);
diff --git a/tests/e2e/protocol.cpp b/tests/e2e/protocol.cpp
index d2b3009..a65e62a 100644
--- a/tests/e2e/protocol.cpp
+++ b/tests/e2e/protocol.cpp
@@ -425,7 +425,23 @@ E2E protocol{"protocol", [] {
&& events[eventsBefore + 1]["metrics"][0]["name"] == "ready");
check(events[eventsBefore + 3]["status"] == "failed" && events[eventsBefore + 3]["part"] == "unit/b");
}
- for (const auto* name : {"mcpp-ide/test/result", "mcpp-ide/test/caseEvent"})
+ // Cases asked for through a part: each result names its program and its own part, as the
+ // record states them, not the job's filter.
+ const auto caseResultsBefore = wire.notifications("mcpp-ide/test/caseResult").size();
+ const auto finishedCases = wire.notifications("mcpp-ide/job/finished").size();
+ result_of("mcpp-ide/test/runCases", Json{{"root", dir.generic_string()}, {"test", "unit/a"}, {"framework", "mtest"},
+ {"cases", Json::array({"alpha"})}});
+ for (int i = 0; i < 2000 && wire.notifications("mcpp-ide/job/finished").size() <= finishedCases; ++i)
+ std::this_thread::sleep_for(std::chrono::milliseconds(5));
+ const auto caseResults = wire.notifications("mcpp-ide/test/caseResult");
+ check(caseResults.size() == caseResultsBefore + 2);
+ if (caseResults.size() == caseResultsBefore + 2) {
+ const auto& alpha = caseResults[caseResultsBefore];
+ const auto& beta = caseResults[caseResultsBefore + 1];
+ check(alpha["test"] == "unit/a" && alpha["program"] == "unit" && alpha["part"] == "unit/a" && alpha["case"] == "alpha");
+ check(beta["program"] == "unit" && beta["part"] == "unit/b" && beta["status"] == "failed");
+ }
+ for (const auto* name : {"mcpp-ide/test/result", "mcpp-ide/test/caseEvent", "mcpp-ide/test/caseResult"})
for (const auto& n : wire.notifications(name)) check(conforms(n, protocol["notifications"][name]["params"], protocol, name));
// The store mcpp keeps (target/mtest): three runs, the last with mcpp#917's fields.
const auto noRuns = result_of("mcpp-ide/test/insights", Json{{"root", dir.generic_string()}});