Skip to content

fix(coverage): scope the .claude discovery exclusion to paths relative to the search root (#752) - #776

Merged
drmoisan merged 1 commit into
mainfrom
bug/coverage-assembly-discovery-excludes-own-worktree-root-752
Sep 4, 2026
Merged

fix(coverage): scope the .claude discovery exclusion to paths relative to the search root (#752)#776
drmoisan merged 1 commit into
mainfrom
bug/coverage-assembly-discovery-excludes-own-worktree-root-752

Conversation

@drmoisan

@drmoisan drmoisan commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Suggested title

fix(coverage): scope the .claude discovery exclusion to paths relative to the search root

Summary

  • Fixes scripts/vscode/Invoke-MSTestWithCoverage.ps1's test-assembly discovery predicate, which excluded every candidate assembly (not just sibling agent worktrees) whenever the checkout running the script was itself located under .claude/worktrees/ — the normal case for every parallel-run and epic-run child worktree.
  • Root cause: the predicate matched the candidate's absolute FullName for a \.claude\ segment. Get-ChildItem -Recurse always prefixes candidates with the search root, so when the search root itself contains .claude\worktrees\agent-<id>\, that segment appears in every candidate and the discovered assembly set is always empty, producing a misleading No test assemblies found ... Build first. error.
  • Fix: match against the candidate path computed relative to the search root ([System.IO.Path]::GetRelativePath) using the anchored pattern (^|\\)\.claude\\, so only a .claude segment that appears after the search root (a nested sibling worktree) is excluded — self-exclusion of the running checkout's own root no longer occurs.
  • Adds a new Pester regression file, tests/scripts/vscode/Invoke-MSTestWithCoverage.AssemblyDiscovery.Tests.ps1, covering both the self-exclusion fix and the continued nested-sibling-worktree exclusion.
  • Preserves the pre-existing regression test tests/scripts/vscode/Invoke-MSTest.RunSettings.Tests.ps1:416-442 byte-for-byte unmodified; it continues to pass.
  • All 6 acceptance criteria in spec.md are met; full PowerShell toolchain (PoshQC format → PoshQC analyze → Pester test) passes clean.
  • One post-implementation remediation cycle sanitized an absolute host path that had leaked into committed feature-folder markdown during preparation (not into any source or test file); see Risks and Mitigations.

Why

Item #733 (PR #748) added the .claude exclusion clause specifically to keep a sibling agent worktree nested beneath the search root out of test-assembly discovery when the wrapper script runs from the main checkout. That filter did not anticipate the wrapper script itself running from a checkout under .claude/worktrees/agent-<id>/, which is the standard topology for every parallel-orchestration and epic-orchestration child. In that topology the exclusion clause fired on every candidate unconditionally, breaking coverage collection for every C# item executed through the parallel/epic surfaces. CI is unaffected because CI runner checkouts never contain a .claude path segment.

What Changed

Core fix

  • scripts/vscode/Invoke-MSTestWithCoverage.ps1 (Invoke-MSTestWithCoverageMain, 1 line): the .claude exclusion clause now matches [System.IO.Path]::GetRelativePath($resolvedSearchRoot, $_.FullName) against (^|\\)\.claude\\ instead of matching $_.FullName against \\\.claude\\. The \bin\<Configuration>\, \obj\, and \ref\ clauses are unchanged.

Tests

  • tests/scripts/vscode/Invoke-MSTestWithCoverage.AssemblyDiscovery.Tests.ps1 (new, 99 lines): exercises Invoke-MSTestWithCoverageMain directly via AST-parsed ScriptBlock with in-memory [pscustomobject] fixtures, covering (1) an assembly discovered directly beneath a .claude\worktrees\agent-<id>\ search root — must be included, and (2) a sibling agent worktree nested beneath the search root — must remain excluded, and (3) a double-nested case (search root itself under .claude\worktrees\agent-N\, containing a further-nested sibling worktree) exercising the ^ branch of the anchor.
  • tests/scripts/vscode/Invoke-MSTest.RunSettings.Tests.ps1 is unmodified (verified byte-identical by blob-hash comparison before and after).

Docs / tooling

  • Feature-folder documentation (docs/features/active/2026-09-03-coverage-assembly-discovery-excludes-own-worktree-root-752/): issue, spec, research, atomic plan, evidence artifacts, and remediation plan/audit artifacts for this item. No production or test code is affected by these files.

Architecture / How It Fits Together

Invoke-MSTestWithCoverageMain resolves $resolvedSearchRoot (already in scope before the discovery block) and then runs Get-ChildItem -Path $resolvedSearchRoot -Recurse to enumerate candidate assemblies. The discovery Where-Object predicate filters that candidate set against four clauses (\bin\<Configuration>\, \obj\, \ref\, and .claude); only the .claude clause's match target changed. The corrected predicate now measures "does a .claude segment appear after the search root in this candidate's path" rather than "does the candidate's absolute path contain .claude anywhere," which is the distinction that separates a legitimate nested-sibling exclusion from an unintended self-exclusion. No change to Invoke-MSTestWithCoverageMain's parameters, return shape, or downstream coverage-collection invocation.

Verification

Completed (evidence committed under the feature folder's evidence/ tree):

  • Full PowerShell toolchain (PoshQC format → PoshQC analyze → Pester test), single clean pass, no unrelated file modified.
  • Pester: 95 passed / 0 failed / 0 skipped in the full tests/scripts/vscode suite post-fix (baseline 92 + 3 new cases); fail-before capture confirmed the new suite's self-exclusion case failed pre-fix (Passed=1 Failed=2 with the exact No test assemblies found ... Build first. message) and passed post-fix.
  • The preserved original nested-sibling-worktree test (Invoke-MSTest.RunSettings.Tests.ps1) passes unchanged; its blob hash matches the pre-change baseline exactly.
  • PowerShell line coverage: baseline 78.3042% → post-change 78.3313% (delta +0.0270; PowerShell has no branch-coverage gate per repo policy). The 85% line floor is not met, but this is a pre-existing repository-wide shortfall predating this item; the applied gate was post-change ≥ baseline, which passed.
  • .claude-clause sibling-defect sweep (git grep -e ".claude" -- scripts/) found no other file in the repository carrying the same absolute-path-vs-.claude discovery defect.

Recommended

  • pwsh -NoProfile -File scripts/vscode/Invoke-MSTest.ps1 from a checkout under .claude/worktrees/agent-<id>/ with a prior build, to confirm end-to-end that the wrapper no longer throws the misleading build-first error in that topology.

Backward Compatibility / Migration Notes

None. The fix strictly widens the discovered-assembly set to include the previously-misclassified self-root case while preserving the previously-excluded nested-sibling-worktree case; no consumer of the discovered assembly list observes a shape change. No public API, CLI flag, or config schema is affected.

Risks and Mitigations

  • Absolute host path leaked into a committed markdown research artifact during preparation. A feature-review pass found that research/research-findings.2026-09-03T00-00.md line 5 (and, on closer branch-diff enumeration, three further lines across spec.md, issue.md, and the promoted potential-entry copy) recorded an operator account name and full worktree directory layout. Mitigation: a remediation plan (independently preflight-cleared across 4 rounds) sanitized all identified lines with class-based placeholders, and a reaudit confirmed zero remaining matches across the full branch diff, plus caught and sanitized one additional leak introduced by the audit process itself (an example verification command that had spelled the same identifiers). No production or test file was touched by this remediation. This PR must be merged with a squash merge, not a merge commit — a sanitizing commit removes the value from the branch tip but leaves the pre-sanitization blob reachable through an earlier commit in the branch's history; squashing is what keeps the identifier out of main.
  • Single-line predicate change carries residual risk of an anchor mismatch. Mitigated by the double-nested regression case and by the preserved, unmodified original test continuing to pass, which together prove both directions of the anchored alternation.

Review Guide

  1. scripts/vscode/Invoke-MSTestWithCoverage.ps1 — the one-line predicate change; this is the entire behavioral fix.
  2. tests/scripts/vscode/Invoke-MSTestWithCoverage.AssemblyDiscovery.Tests.ps1 — new regression coverage; review the three It cases and mock setup.
  3. docs/features/active/2026-09-03-coverage-assembly-discovery-excludes-own-worktree-root-752/spec.md and plan.2026-09-03T07-23.md — root-cause analysis and design rationale, if deeper context is wanted.
  4. The remaining ~55 files are feature-folder documentation and evidence artifacts (plan checklists, baseline/QA-gate/regression evidence, and the remediation-cycle audit trail); mechanical and not expected to need line-by-line review.

Follow-ups

  • Not verified in this PR: discovery-filter parity for scripts/vscode/Invoke-MSTest.ps1's Get-MSTestAssemblyPathList pipeline, which carries no .claude clause today. Research confirmed this is a pre-existing, deliberately out-of-scope gap distinct from this defect.
  • Not verified in this PR: Invoke-MSTestWithCoverage.Helpers.ps1's Get-KoverageProjectAllowlist project-file filter carries no .claude clause either; flagged as a distinct, unreported concern outside this issue's scope.
  • Optional: a permanent test pinning the No test assemblies found ... Build first. throw message does not yet exist (pre-existing gap, not introduced here).
  • Optional: roughly 40 lines of setup duplication between the new test file and Invoke-MSTest.RunSettings.Tests.ps1, forced by the latter's 488/500-line cap; a shared setup helper under tests/scripts/vscode/ could remove it.

GitHub Auto-close

None

🤖 Generated with Claude Code

https://claude.ai/code/session_01TzGiZSnVySFZcoC1BHN5Vv

…e to the search root (#752)

Invoke-MSTestWithCoverage.ps1's test-assembly discovery predicate excluded every
candidate assembly whenever the checkout running the script was itself located
under .claude/worktrees/ (the normal case for every parallel-run and epic-run
child worktree), because the predicate matched the candidate's absolute FullName
for a \.claude\ segment. The fix matches the candidate path computed relative to
the search root instead, so only a nested sibling worktree is excluded and the
running checkout's own root is no longer self-excluded.

Adds a new 3-case Pester regression suite covering the self-exclusion fix, the
continued nested-sibling exclusion, and a double-nested edge case. Preserves the
pre-existing regression test byte-for-byte unmodified. All 6 acceptance criteria
in spec.md are met; full PowerShell toolchain (format, analyze, test) passes
clean, independently reaudited with 0 blocking findings.

This commit intentionally squashes the branch's prior commit history: an earlier
commit in that history carried an absolute host path that leaked into committed
research markdown during preparation and was later sanitized, but the
pre-sanitization blob remained reachable through that earlier commit. Squashing
to one commit is what keeps that content out of main, since this repository's
branch protection on main permits only the ordinary merge method.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TzGiZSnVySFZcoC1BHN5Vv
@drmoisan
drmoisan force-pushed the bug/coverage-assembly-discovery-excludes-own-worktree-root-752 branch from e1b4856 to 68e9022 Compare September 4, 2026 04:23
@drmoisan
drmoisan merged commit c12c4c3 into main Sep 4, 2026
5 checks passed
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.

1 participant