Skip to content

fix(cli): ask only the runs that covered the suite whether a test stopped - #202

Merged
xping-admin merged 2 commits into
mainfrom
fix/140-vanished-filtered-runs
Sep 7, 2026
Merged

fix(cli): ask only the runs that covered the suite whether a test stopped#202
xping-admin merged 2 commits into
mainfrom
fix/140-vanished-filtered-runs

Conversation

@xping-admin

Copy link
Copy Markdown
Collaborator

The issue is still valid

#191 (which closed the sibling issue #165) replaced Vanished's appearance-count gate with a
one-sided Fisher exact test. That narrowed the symptom but did not fix it — it only helps where the
filtered runs also dominate the baseline. The commonest shape still fired at full strength, and
#191's own commit message records that table as one it intends to report, because it cannot tell
it from a deletion.

Measured on a real store, the same shape #191 used to measure itself — seventeen full runs of
SampleApp.MSTest, then three dotnet test --filter runs naming one test:

stopped running findings
before 20 — one per unselected test, each "ran in 17 of 17 earlier runs" at p = 8.8e-4
after 0

8.8e-4 is the best any twenty-run window can do, so no tightening of the gate would have reached it.

What this does

Absence is only evidence in a session that asked. A run under a --filter did not fail to see the
tests it excluded; it never looked for them.

So each run is classified by how much of the suite it covered — SessionView.IsPartial, its distinct
tests against PartialSessionShare = 0.50 of the largest run in the window — and VanishedProvider
reads the window as the sequence of runs that covered the suite, re-split into its own "now" and
"before".

Three deliberate departures from the issue's proposal:

  • SessionView.Tests, not QuickStatistics.distinctTests. SessionAssemblies.Project nulls
    that field for any multi-assembly session, so it is absent exactly on solution-wide runs. The count
    the CLI already derives from executions is the same number and needs no plumbing or null-handling.
  • Anchor on the largest run, not the median. The issue's own store — four full runs and sixteen
    filtered — has a median of one test, against which every run in it measures as typical. A report is
    scoped to exactly one assembly, so the runs being compared are always runs of the same suite.
  • A re-split, not a filter of the window's own slices. The two differ on a store that interleaves
    full and filtered runs: dropping partial sessions out of a current slice of three filtered runs
    leaves nothing to ask about, where taking the three most recent full runs still finds a test that
    genuinely stopped. Re-deriving both sides also fixes the baseline half — a test present in every
    run that asked for it no longer reads as a 5-of-17 occasional visitor.

AnalysisWindow.SliceSizeFor is extracted so the re-split narrows on the same rule the window does
and the two cannot drift.

The trade, recorded rather than left to be discovered

A count cannot separate a filter from a deletion, and no threshold makes it able to: nine tests
missing from seventeen is the same table whether they were excluded or removed. The line is at a
half, biased towards silence on purpose — the kind is capped at Severity.Low because a
disappearance is usually something the developer did a minute ago, whereas the false positive arrives
once per unselected test on every filtered run for as long as it stays in the window.

So a deletion removing more than half a suite is not reported, and a filter selecting more
than half
still produces false absences. Both are stated in the command reference and in
known limitations, and both are pinned by tests.

Not suppressed silently

VanishedEvidence publishes partialSessionsSetAside; the headline says "full runs" and a
set aside metric gives the number, but only where any were — with nothing set aside there is
nothing for the word to distinguish the runs from. The summary carries partialSessions with a
caveat line worded as an observation rather than a discount: only this kind sets these runs aside,
because a filtered run's outcomes are as true as any other run's and it is only its silences that
mean nothing. Schema moves to 1.13.

Verification

  • dotnet build Xping.Sdk.sln — 0 warnings, 0 errors.
  • 926 CLI + 901 Core + 295 adapter tests pass.
  • 12 new tests, including the issue's exact reproduction, the family-size behaviour, the interleaved
    baseline, the evidence wording in both directions, and the documented cost.
  • End to end on a real store, before and after, as tabled above.
  • On a store of full runs only the envelope is identical apart from the schema version and the
    new field — the unfiltered path is unmoved.

Two things worth a reviewer's eye

Item 3 of the issue is deliberately not done, and cannot be done as written. Investigated per
framework:

  • MSTest is impossible — no test count is exposed to any hook the adapter occupies, pre- or
    post-filter. This is the framework the issue's reproduction was recorded on.
  • xUnit would need a CreateDiscoverer override counting Find callbacks, degrading to null on
    the run-selected-tests path.
  • NUnit can read ITest.TestCaseCount, but NUnit3TestAdapter's opt-in PreFilter silently turns
    it into a post-filter number — a fact-shaped number that can quietly be the wrong fact is worse
    than a null for a statistical gate.
  • And the shape is wrong regardlessTotalTestsExpected is modelled host-wide, so
    SessionAssemblies.Project nulls it before the CLI sees a session. Populating
    GetTotalTestsExpected() alone would change nothing observable.

A follow-up should file it as a per-assembly discovered count, not a host-wide field.

One existing fixture was adjusted. ReportEnvelopeTests.SeedVanishing seeded a three-test suite
and deleted two of them — over the half line, so those tests broke. They use Vanished only as a
convenient way to produce several findings, so the stable set is now sized relative to the vanishing
set. Worth confirming you agree that is the right reading rather than a signal about the threshold.
The helper's remarks record why the suite is squeezed from both sides: too few stable tests and the
later runs read as partial, too many and the Benjamini-Hochberg bar tightens past what eight sessions
can produce.

Refs #140

🤖 Generated with Claude Code

https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc

…pped

`VanishedProvider` decided absence with one membership test — is this fingerprint
in the current slice? — and had no notion of whether the sessions in that slice
tried to run it. A `dotnet test --filter` run in the inner loop therefore produced
one false `stopped running` per unselected test, and #191's Fisher gate did not
fix that: it only helps where the filtered runs dominate the *baseline* too. The
commonest shape still fired at full strength. Measured on a real store — seventeen
full runs of `SampleApp.MSTest` then three `--filter` runs of one test — the report
produced **twenty** `stopped running` findings, each reading "ran in 17 of 17
earlier runs" at p = 8.8e-4, which is the best any twenty-run window can do. It now
produces none.

Absence is only evidence in a session that asked. So each run is classified by how
much of the suite it covered — its distinct tests against
`PartialSessionShare = 0.50` of the largest run in the window — and the kinds that
read absence set the small ones aside. The anchor is the largest run and
deliberately not the median: four full runs and sixteen filtered ones, which is what
ten minutes of an inner loop produces, has a median of one test and every run in it
measures as typical against that. A report is scoped to exactly one assembly, so the
runs being compared are always runs of the same suite.

`SessionView.Tests`, not `QuickStatistics.distinctTests` as the issue proposes.
`SessionAssemblies.Project` nulls that field for any multi-assembly session, so it
is absent exactly on solution-wide runs; the count the CLI already derives from
executions is the same number and needs no plumbing.

A re-split rather than a filter of the window's own slices. The two differ on a
store that interleaves full and filtered runs: dropping partial sessions out of a
current slice of three filtered runs leaves nothing to ask about, where taking the
three most recent full runs still finds a test that genuinely stopped. Both the
"now" and the "before" are re-derived over the covering runs, which also fixes the
baseline half — a test present in every run that asked for it no longer reads as a
5-of-17 occasional visitor. `AnalysisWindow.SliceSizeFor` is extracted so the
re-split narrows on the same rule the window does and the two cannot drift.

The trade, recorded deliberately rather than left to be discovered. A count cannot
separate a filter from a deletion and no threshold makes it able to: nine tests
missing from seventeen is the same table whether they were excluded or removed. The
line is at a half, and it is biased towards silence — the kind is capped at
`Severity.Low` because a disappearance is usually something the developer did on
purpose a minute ago, whereas the false positive arrives once per unselected test on
every filtered run for as long as it stays in the window. So a deletion removing
more than half a suite is not reported, and a filter selecting more than half still
produces false absences. Both are in the command reference and in known limitations.

Not suppressed silently. `VanishedEvidence` publishes `partialSessionsSetAside`, the
headline says "full runs" and a `set aside` metric gives the number where any were,
and the summary carries `partialSessions` with a caveat line worded as an
observation rather than a discount — only this kind sets them aside, because a
filtered run's *outcomes* are as true as any other run's and it is only its silences
that mean nothing. The schema moves to 1.13.

Verified end to end: on a store of full runs only the envelope is identical apart
from the schema version and the new field.

Item 3 of the issue — populating `TestSession.TotalTestsExpected` — is deliberately
not done here, and cannot be done as written. MSTest exposes no test count to any
hook the adapter occupies; xUnit needs a `CreateDiscoverer` override and NUnit's
`ITest.TestCaseCount` silently becomes post-filter under the adapter's `PreFilter`
setting. And the field is modelled host-wide, so `SessionAssemblies.Project` nulls
it before the CLI sees a session. A follow-up will file it as a per-assembly
discovered count.

Refs #140

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are small but user-visible/documentation issues in the updated text outputs/comments (notably the Vanished headline string missing “runs/full runs” in one clause) that should be corrected before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR refines the CLI’s Vanished finding so “stopped running” is evaluated only against sessions that plausibly covered the full suite, preventing filtered runs (e.g., dotnet test --filter) from generating false disappearances. It introduces a window-relative “partial session” classification, re-slices Vanished over only full-suite runs, and updates the JSON contract and docs accordingly.

Changes:

  • Classify sessions as “partial” based on distinct test count vs the window’s largest run, and surface partial-session counts in the report summary.
  • Update VanishedProvider to re-slice over full-suite runs only (setting partial sessions aside) and publish partialSessionsSetAside in VanishedEvidence.
  • Bump report schema to 1.13 and update tests + documentation to reflect the new output fields and wording.
File summaries
File Description
tests/Xping.Cli.Tests/Report/VanishedProviderTests.cs Adds coverage for filtered runs, re-splitting behavior, and set-aside evidence/headline wording.
tests/Xping.Cli.Tests/Report/ShareableOutputTests.cs Updates expected VanishedEvidence and SummaryDto shapes for the new fields.
tests/Xping.Cli.Tests/Report/SessionViewTests.cs Adds tests pinning window-relative partial-session classification and boundary behavior.
tests/Xping.Cli.Tests/Report/ReportEnvelopeTests.cs Adjusts seeded data and schema assertions for the new summary field/version.
tests/Xping.Cli.Tests/Commands/CliSurfaceTests.cs Updates CLI schema version expectations to 1.13.
src/Xping.Cli/Report/Windowing/AnalysisWindow.cs Extracts SliceSizeFor to share split sizing logic with re-slicing providers.
src/Xping.Cli/Report/Rendering/TextReportRenderer.cs Emits a caveat line when partial sessions are present in the window.
src/Xping.Cli/Report/Providers/VanishedProvider.cs Re-slices over full-suite sessions only; publishes set-aside count in evidence.
src/Xping.Cli/Report/LocalAnalysisConstants.cs Introduces PartialSessionShare (0.50) and documents the tradeoff.
src/Xping.Cli/Report/Indexes/SessionView.cs Adds IsPartial flag (window-derived) with documentation.
src/Xping.Cli/Report/Contract/ReportEnvelope.cs Bumps schema to 1.13 and documents the contract change.
src/Xping.Cli/Report/Contract/EvidenceHeadline.cs Updates Vanished headline/metrics to reflect “full runs” + set-aside metric.
src/Xping.Cli/Report/Contract/EnvelopeBuilder.cs Adds partialSessions to the summary DTO construction.
src/Xping.Cli/Report/AnalysisContext.cs Computes per-window partial-session classification and count.
docs/known-limitations.md Documents the limitation/tradeoff around filters vs deletions and the 50% threshold.
docs/cli/command-reference.md Updates Vanished documentation and JSON example for schema 1.13 + partialSessions.
Review details
  • Files reviewed: 16/16 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Xping.Cli/Report/Contract/EvidenceHeadline.cs
Comment thread tests/Xping.Cli.Tests/Report/ReportEnvelopeTests.cs Outdated
@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...c/Xping.Cli/Report/Rendering/TextReportRenderer.cs 0.00% 3 Missing and 1 partial ⚠️
Files with missing lines Coverage Δ
src/Xping.Cli/Report/AnalysisContext.cs 90.00% <100.00%> (+5.00%) ⬆️
src/Xping.Cli/Report/Contract/EnvelopeBuilder.cs 95.34% <100.00%> (+0.05%) ⬆️
src/Xping.Cli/Report/Contract/EvidenceHeadline.cs 93.85% <100.00%> (+0.49%) ⬆️
src/Xping.Cli/Report/Contract/ReportEnvelope.cs 100.00% <100.00%> (ø)
src/Xping.Cli/Report/Indexes/SessionView.cs 92.85% <100.00%> (+0.54%) ⬆️
src/Xping.Cli/Report/Providers/VanishedProvider.cs 100.00% <100.00%> (ø)
src/Xping.Cli/Report/Windowing/AnalysisWindow.cs 100.00% <100.00%> (ø)
...c/Xping.Cli/Report/Rendering/TextReportRenderer.cs 81.52% <0.00%> (-2.14%) ⬇️

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@xping-admin

Copy link
Copy Markdown
Collaborator Author

Follow-ups filed, so the two things this PR deliberately leaves undone are tracked rather than lost:

With both tracked, Closes #140 is right — the reported bug is fixed and measured here.

Two findings from review.

"absent from the last 3" was unambiguous only while every run in the window was
one this kind counted. Once runs are set aside it is not: the last three runs and
the last three *full* runs are different runs, and the first clause saying "earlier
full runs" does not repair the second. A headline is read, and pasted, a clause at
a time, so a clause that is only true given the one before it is a clause that will
be quoted false. Both clauses carry the noun where anything was set aside, and the
sentence stays byte for byte what it was where nothing was — on an ordinary store
"the last 3" can mean nothing but the last three runs, and the shorter sentence is
the true one there.

The two headline tests now pin the whole sentence rather than asserting a substring
of it, which is what let the second clause drift in the first place.

`SeedVanishing` had two consecutive `<summary>` blocks: the edit that added the
sizing rationale kept the original summary and added a second rather than merging
them. One summary, and the sizing argument stays in the remarks where it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc
@xping-admin

Copy link
Copy Markdown
Collaborator Author

Both Copilot findings addressed in 33ab0a2 — both valid, and one of them was mine rather than a style preference.

The headline clause was the substantive one, and worse than the "grammatically incomplete" framing suggests. absent from the last 3 was unambiguous only while every run in the window was one this kind counted. Once runs are set aside it is not — the last three runs and the last three full runs are different runs — and the first clause saying earlier full runs does not repair the second. Both clauses carry the noun now where anything was set aside, and the sentence is unchanged where nothing was:

ran in 17 of 17 earlier full runs, absent from the last 3 full runs   # runs set aside
ran in 17 of 17 earlier runs, absent from the last 3                  # ordinary store

The two headline tests now assert the whole sentence rather than a substring of it, which is what let the second clause drift in the first place.

The duplicated <summary> in SeedVanishing was an editing slip on my part — the change that added the sizing rationale kept the original summary and appended a second. Merged.

926 CLI tests still pass. The doc examples in command-reference.md and local-first.md show stores with no partial runs, so their unqualified headlines remain correct and are unchanged.

@xping-admin
xping-admin merged commit c6d073e into main Sep 7, 2026
2 of 3 checks passed
@xping-admin
xping-admin deleted the fix/140-vanished-filtered-runs branch September 7, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(cli): Vanished fires on filtered runs — --filter makes every unselected test look deleted

2 participants