feat: add per-requirement test attribution to coverage engine - #226
feat: add per-requirement test attribution to coverage engine#226em-redhat wants to merge 1 commit into
Conversation
5569f56 to
a515d98
Compare
yvonnedevlinrh
left a comment
There was a problem hiding this comment.
Solid implementation that correctly solves the aggregate attribution problem described in #203. Architecture follows the thin-transport-layer pattern, test coverage is thorough (37+ subtests), and CI is fully green. See inline comments for specific findings.
Commit trailer (must fix): Commit a515d98 is missing both required trailers per the constitution's Contribution Workflow section:
Signed-off-by: — required for DCO compliance
Assisted-by: () — required if AI-assisted
| } | ||
|
|
||
| // TestDetail contains per-test outcome information. | ||
| type TestDetail struct { |
There was a problem hiding this comment.
This is the second copy of TestDetail — see identical definition at internal/tester/runner.go:15. The field-by-field conversion in opa.go:58-67 is the only bridge between them. Please see the comment on runner.go for suggested resolution paths.
There was a problem hiding this comment.
Resolved — local TestDetail struct removed. evaluator/evaluator.go:61 now uses testresult.Detail from the shared internal/testresult/ package.
| func LoadOverrideMapping(filePath string) (PackageMapping, error) { | ||
| data, err := os.ReadFile(filePath) | ||
| if err != nil { | ||
| if os.IsNotExist(err) { |
There was a problem hiding this comment.
os.IsNotExist() instead of errors.Is() — LoadOverrideMapping uses deprecated os.IsNotExist(err) instead of errors.Is(err, os.ErrNotExist). Not a bug with os.ReadFile, but violates post-Go-1.13 idiom.
There was a problem hiding this comment.
Resolved — coverage.go:486 now uses errors.Is(err, os.ErrNotExist).
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := NormalizePackageToRequirement(tt.input) |
There was a problem hiding this comment.
Test assertion style inconsistency — new attribution tests use raw t.Errorf while existing tests in the same file use testify (assert.Equal, require.NoError). File already imports both assert and require.
There was a problem hiding this comment.
Resolved — all attribution tests now use testify. See coverage_test.go:603 (assert.Equal), :611 (assert.Empty), :630 (require.Len/assert.Equal), etc. No more raw t.Errorf in the attribution test section.
| // requirementPattern matches package segments that follow the | ||
| // {family}_{major}[_{minor}...]_test naming convention. | ||
| // family is alphabetic, followed by underscore-separated numeric/alpha segments. | ||
| var requirementPattern = regexp.MustCompile(`^([a-z]+)_(\d+(?:_[\da-z]+)*)$`) |
There was a problem hiding this comment.
Regex only matches lowercase alpha families — ^([a-z]+)_(\d+...)$ won't match uppercase (AC_2_1) or hyphenated families (cp-abc_1_2). Fine if the convention is documented, but worth noting for users.
There was a problem hiding this comment.
Resolved — coverage.go:410-415 now documents the lowercase-only constraint and notes that a coverage-mapping.yaml override file should be used for non-standard naming (uppercase or hyphenated families).
hbraswelrh
left a comment
There was a problem hiding this comment.
Review Summary
Solid implementation solving the aggregate attribution problem from #203. Architecture follows thin-transport-layer pattern per AGENTS.md. Test coverage is thorough (26+ subtests).
Concurs with @yvonnedevlinrh's existing review. Two HIGH findings block merge:
- Duplicated
TestDetailstruct across package boundary (Constitution Principle I) - Missing commit trailers (
Signed-off-by,Assisted-by) — constitutional MUST
Additional notes not covered by inline comments:
enrichWithTestResultsatcoverage.go:340still emits stale warning "per-requirement attribution is not yet available" — this PR adds exactly that capability. Update warning text or integrateAttributeTests()intoRun().- Test assertion style mixes raw
t.Errorfwith file's existing testify style — pick one per file. - Regex
requirementPatternonly matches lowercase families — document this constraint if intentional.
This review was generated by /uf.review-pr (AI-assisted).
a515d98 to
a4a55c0
Compare
yvonnedevlinrh
left a comment
There was a problem hiding this comment.
PR: #226
Action: Submit review → Approve
Base: main
Head: opsx/per-requirement-test-attribution
Prior Findings Resolution
All findings from previous reviews have been addressed in a4a55c04:
| Finding | Status | Evidence |
|---|---|---|
| TestDetail duplication | ✅ Resolved | Extracted to shared internal/testresult/testresult.go |
os.IsNotExist → errors.Is |
✅ Resolved | coverage.go:486 |
| Dead append pattern | ✅ Resolved | Direct assignment at coverage.go:467 |
| Test assertion style | ✅ Resolved | All tests use testify |
| MCP asymmetric error handling | ✅ Resolved | Graceful degradation with documented rationale (tools.go:336-343) |
| Warning text | ✅ Resolved | Updated in gap-reporting engine |
| Regex documentation | ✅ Resolved | coverage.go:410-415 documents convention and limitations |
| Commit trailers | ✅ Resolved | Signed-off-by and Assisted-by present |
New Findings (Non-blocking)
LOW: Code duplication in internal/tester/runner.go:81-106
The pkg extraction logic is duplicated between the pass and fail branches (lines 81-84 and 96-99). It could be hoisted above the if/else since both branches are identical:
pkg := ""
if result.Location != nil {
pkg = pkgByFile[result.Location.File]
}Not blocking — the current code is readable and functional.
NIT: Discarded error in MCP handler (tools.go:344)
perReq, _ := coverage.AttributeTests("", "", results)The comment block (lines 336-343) thoroughly documents the rationale. For full CLI/MCP parity, the error could be logged (the CLI logs it as a WARNING at pack.go:241). In practice this is cosmetic since the MCP path passes empty strings for both directory parameters, so LoadOverrideMapping is never reached.
Architecture & Constitution
- ✅ Thin transport layers: CLI (
pack.go:238-252) and MCP (tools.go:336-347) are pure wiring overcoverage.AttributeTests() - ✅ CLI/MCP parity maintained
- ✅ Domain logic in
internal/coverage/,internal/tester/,internal/evaluator/ - ✅ Shared type in
internal/testresult/eliminates cross-package duplication - ✅ SPDX headers on all new files
- ✅ Conventional commit with required trailers
- ✅ All constitution principles satisfied (centralized constants, isolation, readability, composability, convention-over-config)
Test Coverage
Comprehensive across all four packages — 26+ subtests in coverage_test.go, 6 in runner_test.go, 2 in opa_test.go, 2 in tools_test.go. Positive, negative, and edge cases covered. All use testify.
Security
No concerns. Override file path is deterministic. MCP handler passes empty paths preventing filesystem interaction from the attribution engine. YAML parsing uses yaml.v3.
LGTM — well-structured implementation that correctly resolves #203.
sonupreetam
left a comment
There was a problem hiding this comment.
Non-duplicate findings from review council (3 items, all LOW/non-blocking)
1. AGENTS.md:14 — internal/testresult/ not listed in domain packages
New shared types package consumed by tester, evaluator, and coverage is not in the domain packages inventory. Since AGENTS.md is the authoritative guide for where code belongs, consider adding:
- `internal/testresult/` — shared per-test outcome types consumed by tester, evaluator, and coverage
2. coverage.go:497-499 — Override mapping version error lacks actionable guidance
The error unsupported coverage mapping version "2" (expected "1") tells the user what failed but not what to do. Consider:
unsupported coverage mapping version "2"; this version of complypack supports version "1" — upgrade complypack or use version "1"
3. coverage-mapping.yaml format undocumented in-repo
The override file schema (version, mappings structure) is only discoverable by reading source. No example file or README section exists. Consider adding a coverage-mapping.example.yaml or a brief format description in the README.
E2E verified: CLI pack with convention naming, mixed pass/fail attribution, MCP perRequirement field, and override file precedence all work correctly.
a4a55c0 to
024bc3e
Compare
Introduce a per-requirement test attribution pipeline that maps OPA/Conftest test packages to requirement IDs using naming conventions and optional YAML override files. New components: - internal/testresult: shared TestDetail struct (eliminates duplication) - coverage.NormalizePackageToRequirement: regex-based package-to-requirement mapping - coverage.BuildConventionMapping: builds mapping from test details - coverage.LoadOverrideMapping: loads coverage-mapping.yaml overrides - coverage.EnrichWithTestResults: per-requirement pass/fail/untested status - coverage.AttributeTests: orchestrates convention + override mapping Transport integration: - CLI (pack command): logs per-requirement status with graceful degradation - MCP (test_policy tool): includes per-requirement data in response Signed-off-by: Em <elyons@redhat.com> Assisted-by: Claude (Anthropic)
024bc3e to
3929052
Compare
em-redhat
left a comment
There was a problem hiding this comment.
Thanks for the thorough review @sonupreetam — all 4 findings addressed in 3929052:
1. AGENTS.md:14 — internal/testresult/ not listed — Fixed. Added to domain packages list at AGENTS.md:14:
- `internal/testresult/` — shared per-test outcome types consumed by tester, evaluator, and coverage
2. coverage.go:498 — Error message lacks actionable guidance — Fixed. Now reads:
unsupported coverage mapping version "2"; this version of complypack supports version "1" — upgrade complypack or use version "1"
3. coverage-mapping.yaml format undocumented — Fixed. Added coverage-mapping.example.yaml with annotated examples covering: single-requirement mapping, multi-requirement mapping, non-convention package names, and field descriptions.
4. [MEDIUM] MCP observability gap — empty vs absent perRequirement — Fixed. buildTestResultsResponse (tools.go:199) now includes perRequirement: {} whenever attribution was attempted (non-nil map), even when no tests matched the convention. This lets MCP consumers distinguish three states:
perRequirementabsent → feature not available (older complypack)perRequirement: {}→ attribution attempted, no tests matched conventionperRequirement: {"ac-2.1": "passing"}→ tests matched and attributed
Tests updated to verify the new semantics at tools_test.go:382-395 and tools_test.go:738-741.
Summary
Resolves #203 — the coverage engine previously used aggregate test results,
causing all implemented requirements to be marked
implemented_failingwhenany single test failed. This change enables per-requirement test attribution
so each requirement gets its own passing/failing/untested status.
Changes
internal/tester/— Capture per-test details (name, package, location,pass/fail, error) during OPA test execution via new
TestDetailstructinternal/evaluator/— Propagate per-test details through the evaluatorabstraction boundary with mirrored
TestDetailstructinternal/coverage/(NEW) — Map Rego test packages to requirement IDsvia naming convention (
ac_2_1_test→ac-2.1) or optionalcoverage-mapping.yamloverride file; classify each requirement independentlyinternal/mcp/— IncludeperRequirementfield intest_policyJSON responsecmd/complypack/cli/— Display per-requirement test status in CLI outputAGENTS.md— Document newinternal/coverage/domain packageTesting
internal/coverage/coverage_test.gocovering normalization,convention mapping, enrichment, override loading, and orchestration
internal/tester/runner_test.gofor detail captureinternal/evaluator/opa_test.gofor propagationinternal/mcp/tools_test.gofor response serialization-race -count=1Backward Compatible
All changes are additive — existing aggregate fields (
total,passed,failed,errors) remain unchanged. TheperRequirementfield is onlyincluded when per-test details are available.