Skip to content

Coverage gate: replace the global MaxSkipped ceiling with per-file skip attribution #132

Description

Problem

The MaxSkipped ceiling in Tests/coverage-baseline.psd1 is a single global number per
edition. Because it is a ceiling with headroom rather than a pin, it silently absorbs skip
movement until some later, unrelated change crosses the line — and then it reds that change
instead of the one that caused the movement.

This is not hypothetical. It just happened, and the arithmetic is exact.

The incident

A local Windows PowerShell 5.1 full-suite run measured 292 skipped against the ceiling of
268, so the gate failed. Attributing that 292 by source file shows the +40 over the 252 that
originally justified 268 splits in two:

Delta File Cause
+34 Update-PfbTestModuleImport.Tests.ps1 An unmerged branch. The tool under test declares #Requires -Version 7.0, so the PS7 gate is correct, and the file runs in full on the pwsh7 leg.
+6 Build-PfbDeadKeyReport.Tests.ps1 Not that branch. Landed in 302f9e4 (#120) on 2026-08-16, two days after 364a0fe set the ceiling to 268 on 2026-08-14.

So #120 added 6 legitimate PS7-gated skips and never needed to touch the ceiling, because at
258 measured against 268 it still had slack. It passed. The ceiling was then stale — carrying
10 headroom, not the 16 its own comment claimed — and the next branch to cross the line was
billed for all 40, including #120's 6.

Why the headroom is the mechanism, not the mitigation

Headroom exists to avoid false reds. What it actually does is convert an attributable red
on the PR that caused the movement into an unattributable red on a later innocent PR. That
is strictly worse for whoever has to diagnose it: they get a number that is wrong by an amount
they did not cause, with no signal telling them which part is theirs.

It also means a real coverage regression of up to MaxSkipped - measured tests is invisible.
That is the issue #63 failure shape — green summary, lost coverage — just bounded in size.

Proposed design: per-file expected skips

Replace (or supplement) the global ceiling with a per-file map of expected skip counts.

ExpectedSkips = @{
    'Build-PfbApiDriftReport.Tests.ps1'  = 65
    'Build-PfbCapabilityMap.Tests.ps1'   = 50
    # ...
}

Assertion becomes: for every file, actual skips equal expected. Then

  • a new PS7-gated file is an explicit new entry in the reviewed diff — the thing the
    maintainer actually wants to eyeball;
  • an existing file that starts skipping reds immediately and names itself, instead of
    being absorbed by slack;
  • a file that stops skipping also reds, which catches an accidentally-ungated test — something
    a ceiling structurally cannot see;
  • blame lands on the causing PR, every time, so ceilings cannot go stale between merges.

This composes with the existing RequiredDescribes allowlist rather than replacing it. That
list catches a Describe vanishing from the result tree entirely (contributing neither a skip
nor a pass), which per-file skip counts still cannot see.

Data already gathered

Full per-file skip attribution from the 5.1 run described above (2983 passed / 0 failed / 292
skipped). The 19 entries sum to exactly 292, so this is complete, not a sample:

  65  Build-PfbApiDriftReport.Tests.ps1
  50  Build-PfbCapabilityMap.Tests.ps1
  34  Update-PfbTestModuleImport.Tests.ps1     <-- unmerged branch, not on main yet
  33  PfbPipelineSelectorTools.Tests.ps1
  16  PfbSelectorProbeHarness.Tests.ps1
  15  Build-PfbFieldCmdletMap.Tests.ps1
  14  Build-PfbValueEnumMap.Tests.ps1
  11  PfbPipelineSelectorRail.Tests.ps1
   9  Build-PfbResponseShapeMap.Tests.ps1
   8  Build-PfbCapabilityMap.ContextScopeDrift.Tests.ps1
   8  PfbApiDriftTools.Tests.ps1
   8  PfbValueEnumTools.Tests.ps1
   6  PfbSpecTools.Tests.ps1
   6  Build-PfbDeadKeyReport.Tests.ps1
   4  PfbSpecTools.ContextScope.Tests.ps1
   2  New-PfbJwtToken.Tests.ps1
   1  Set-PfbTlsProtocol.Tests.ps1
   1  Connect-PfbArray.OAuth2TokenRefresh.Tests.ps1
   1  Issue31.DriftConfidence.Tests.ps1

Corresponding pwsh7 leg for the same tree: 3273 passed / 0 failed / 2 skipped against its
ceiling of 8 — which is why these files being PS7-gated is correct behaviour rather than lost
coverage.

Caveat on provenance: both figures are local runs, not CI run numbers like the ones cited
in coverage-baseline.psd1 today. Expect CI on windows-latest to differ slightly. The
per-file shape is what matters here, not the absolute totals.

Implementation notes

No log parsing needed. Assert-PfbTestCoverage.ps1 already receives the result object, and
that object carries the file per test. Verified directly on Pester 6.0.1, both editions:

  • $Result.Tests[].ScriptBlock.File — full path of the source file for each test.
  • $Result.Containers[].Item — a FileInfo per container, so .Name gives the leaf.

Both are available alongside the existing .Path[0] (top-level Describe) and .Result that
the current assertions already use, so this needs no new plumbing in the gate's signature.

Gotchas worth knowing up front:

  • Key the map on the leaf file name, not a full path. Paths differ between runners and
    between a normal checkout and a worktree.
  • Tests/ currently has no test files in subdirectories, so leaf names are unique today. If
    that changes, a leaf-keyed map silently merges two files — worth a uniqueness assertion.
  • Both baseline blocks (pwsh7 and winps51) need the treatment. They differ by a wide
    margin by design: the tooling Describes are PS7-gated, so the 5.1 leg skips essentially all
    of them.
  • Import-PowerShellDataFile cannot parse a single collection literal with more than ~500
    entries. A ~195-entry hashtable per edition is well clear of that, but the two blocks should
    stay separate hashtables rather than being merged into one flat list.
  • The gate script is constrained to Windows PowerShell 5.1 syntax — no ternaries, no ??, no
    &&/||. It runs under both editions.
  • Deciding what to do about a file that is absent from the run entirely (filtered out, or
    deleted) is the interesting design question — that is the graceful-skip hole
    RequiredDescribes exists for, and a per-file skip map has the same blind spot unless
    absence is treated as a violation.

Reproducing the current numbers

The attribution above was produced by scraping a saved full-suite log, purely because the run
had already finished. This is a one-off diagnostic, not the shape the real implementation
should take — use the result object instead, as above. Included only so the numbers can be
re-derived from any existing log.

param([Parameter(Mandatory)][string]$LogPath)

# Pester writes ANSI colour codes, so lines do not start with the text they appear to start
# with. Strip them before any matching or every pattern silently fails to match.
$esc = [char]27
$lines = ((Get-Content $LogPath -Raw) -replace "$esc\[[0-9;]*m", '') -split "`r?`n"

$current = ''
$tally = @{}
foreach ($line in $lines) {
    $trimmed = $line.Trim()
    if ($trimmed -like "Running tests from '*") {
        $current = ($trimmed.TrimEnd("'") -split '[\\/]')[-1]
    }
    elseif ($trimmed.StartsWith('[!]') -and $current -ne '') {
        if (-not $tally.ContainsKey($current)) { $tally[$current] = 0 }
        $tally[$current] = $tally[$current] + 1
    }
}

"TOTAL = " + ($tally.Values | Measure-Object -Sum).Sum
$tally.GetEnumerator() | Sort-Object Value -Descending |
    ForEach-Object { '{0,4}  {1}' -f $_.Value, $_.Key }

Sanity check: the total must equal the run's reported skipped + not-run count. If it does not,
the boundary detection is wrong and the per-file numbers cannot be trusted.

Open questions for the maintainer

  1. Replace the ceiling, or keep both? A global ceiling is a cheap backstop against a
    file-level map drifting out of date, but two overlapping assertions on the same quantity may
    not be worth the maintenance.
  2. How noisy is acceptable? Exact per-file matching means any legitimate change to a gated
    test reds CI until the map is updated in the same PR. That is the point, but it is a real
    increase in required diff churn.
  3. Absent files — violation, or ignored? See the last implementation note.

Cheap interim alternative

If the per-file map is more churn than wanted, setting MaxSkipped to the measured value with
zero headroom captures most of the benefit for a one-line change: movement then reds the PR
that caused it rather than a later one. It still cannot say which file moved, and it cannot
detect a test that stopped skipping.

Activity

  1. added 2 commits that reference this issue on Aug 21, 2026
  2. added
    status:agent-readyScope and approach are unambiguous enough for an agent to pick up unattended.
    priority:P3Nice to have. Safe to leave indefinitely.
    size:SOne sitting. Single file or a mechanical change.
    area:ciWorkflows, gates, Pester scoping and test economics.
    source:driftOpened from a drift-report finding, by tooling.
    on Sep 21, 2026
  3. juemerson-at-purestorage commented on Sep 21, 2026

    @juemerson-at-purestorage
    CollaboratorAuthor

    Written by Claude during triage, reviewed by @juemerson-at-purestorage before posting.

    Agent Brief

    Category: bug
    Summary: Establish whether PR #135's ExpectedSkips fully satisfies this issue's ask, then either recommend closure with evidence or state precisely what remains. This is a close-out, not an implementation.

    Verification: n/a — cannot reach the wire. This is test-infrastructure work with no request path.

    Read this before starting. flywheel/issue-triage.md records that this issue is "FIXED by PR #135, merged 2026-08-21 — ExpectedSkips is now an exact per-file pin. Close it, or say what remains." That claim has not been checked against the merged code. Do not assume it is true and do not assume it is false — this issue exists because a ceiling with headroom silently drifted from what its own comment claimed, so an unverified belief about a coverage gate is exactly the thing this issue is about.

    Current behavior:
    The issue was filed against a single global MaxSkipped number per edition in the test coverage baseline. Because it was a ceiling with headroom rather than a pin, it absorbed skip movement until an unrelated later change crossed the line, then failed that change instead of the one that caused the drift. The issue documents the incident with exact arithmetic: 292 measured against a ceiling of 268, where 6 of the +40 belonged to a PR two days earlier that had passed on slack and the next branch across the line was billed for all forty.

    The issue proposes a per-file ExpectedSkips map as the fix. PR #135 is reported to have shipped exactly that.

    Desired behavior:
    One of two outcomes, whichever the evidence supports:

    1. The ask is met. A comment states what shipped, cites the merged code, and shows that each of the issue's stated problems is addressed. Recommend closure as completed; do not close it — that is the maintainer's call.
    2. Something remains. A comment names precisely what, in terms of the issue's own three complaints: unattributable reds, invisible coverage regression bounded by the headroom, and a stale ceiling whose comment misstates its own slack.

    Key interfaces:

    • The coverage baseline data file and whatever gate script reads it. Find them by searching for the ExpectedSkips and MaxSkipped keys rather than by any path quoted here or in the issue — the issue predates the fix and its paths may have moved.
    • The question to answer about the shape: is ExpectedSkips an exact per-file pin (a file measuring fewer skips than expected also fails), or a per-file ceiling? Only the exact pin closes the invisible-regression half of the complaint. A per-file ceiling is a smaller version of the same bug.
    • Whether the global MaxSkipped still exists alongside it, and if so whether it can still drift.

    Acceptance criteria:

    • A comment on the issue states, with the merged code as evidence, which of the issue's three complaints are addressed and which are not.
    • The pin-versus-ceiling question above is answered explicitly, not implied.
    • If anything remains, it is stated as a concrete change, not as "further work needed".
    • A recommendation to close or to keep open, with the reason. The issue is not closed by this work — recommend, and leave it.
    • No source change is made. If the investigation finds a real remaining defect, it is reported, not fixed; fixing it is a separate task with its own review.

    Out of scope:

    • Implementing anything. Even if a gap is found. This brief buys an answer, not a patch.
    • Closing the issue. Triage permission allows it; the standing rule is that closure and its reason are the maintainer's call.
    • Re-running the full suite to produce fresh skip numbers. The aggregate run is never a task's own completion check here, and a number measured in a worktree missing the tools/specs/ cache differs from one measured with it — roughly 27 tests are spec-cache-gated and skip without it. If a number is needed, say which environment it assumes.
    • Issue CI silently skips ~23% of the test suite, including the absolute-path regression guards #63 (CI skipping ~23% of tests). Adjacent and a superset of the invisible-coverage concern, but a separate issue with a separate fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:ciWorkflows, gates, Pester scoping and test economics.priority:P3Nice to have. Safe to leave indefinitely.size:SOne sitting. Single file or a mechanical change.source:driftOpened from a drift-report finding, by tooling.status:agent-readyScope and approach are unambiguous enough for an agent to pick up unattended.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions