Skip to content

feat: add per-requirement test attribution to coverage engine - #226

Open
em-redhat wants to merge 1 commit into
complytime:mainfrom
em-redhat:opsx/per-requirement-test-attribution
Open

feat: add per-requirement test attribution to coverage engine#226
em-redhat wants to merge 1 commit into
complytime:mainfrom
em-redhat:opsx/per-requirement-test-attribution

Conversation

@em-redhat

Copy link
Copy Markdown
Contributor

Summary

Resolves #203 — the coverage engine previously used aggregate test results,
causing all implemented requirements to be marked implemented_failing when
any 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 TestDetail struct
  • internal/evaluator/ — Propagate per-test details through the evaluator
    abstraction boundary with mirrored TestDetail struct
  • internal/coverage/ (NEW) — Map Rego test packages to requirement IDs
    via naming convention (ac_2_1_testac-2.1) or optional
    coverage-mapping.yaml override file; classify each requirement independently
  • internal/mcp/ — Include perRequirement field in test_policy JSON response
  • cmd/complypack/cli/ — Display per-requirement test status in CLI output
  • AGENTS.md — Document new internal/coverage/ domain package

Testing

  • 26+ subtests in internal/coverage/coverage_test.go covering normalization,
    convention mapping, enrichment, override loading, and orchestration
  • 6 subtests in internal/tester/runner_test.go for detail capture
  • 2 subtests in internal/evaluator/opa_test.go for propagation
  • 3 tests in internal/mcp/tools_test.go for response serialization
  • All 16 packages pass with -race -count=1

Backward Compatible

All changes are additive — existing aggregate fields (total, passed,
failed, errors) remain unchanged. The perRequirement field is only
included when per-test details are available.

@em-redhat
em-redhat requested a review from a team as a code owner August 5, 2026 11:40
@em-redhat
em-redhat force-pushed the opsx/per-requirement-test-attribution branch 2 times, most recently from 5569f56 to a515d98 Compare August 5, 2026 12:33
@em-redhat
em-redhat requested review from yvonnedevlinrh and removed request for cdaniels255 August 5, 2026 13:06

@yvonnedevlinrh yvonnedevlinrh 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.

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

Comment thread internal/tester/runner.go Outdated
Comment thread internal/evaluator/evaluator.go Outdated
}

// TestDetail contains per-test outcome information.
type TestDetail struct {

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved — local TestDetail struct removed. evaluator/evaluator.go:61 now uses testresult.Detail from the shared internal/testresult/ package.

Comment thread internal/coverage/coverage.go
Comment thread internal/coverage/coverage.go Outdated
func LoadOverrideMapping(filePath string) (PackageMapping, error) {
data, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved — coverage.go:486 now uses errors.Is(err, os.ErrNotExist).

Comment thread internal/coverage/coverage.go Outdated

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NormalizePackageToRequirement(tt.input)

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread internal/coverage/coverage_test.go Outdated
Comment thread internal/mcp/tools.go Outdated
// 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]+)*)$`)

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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).

Comment thread internal/coverage/coverage_test.go

@hbraswelrh hbraswelrh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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:

  1. Duplicated TestDetail struct across package boundary (Constitution Principle I)
  2. Missing commit trailers (Signed-off-by, Assisted-by) — constitutional MUST

Additional notes not covered by inline comments:

  • enrichWithTestResults at coverage.go:340 still emits stale warning "per-requirement attribution is not yet available" — this PR adds exactly that capability. Update warning text or integrate AttributeTests() into Run().
  • Test assertion style mixes raw t.Errorf with file's existing testify style — pick one per file.
  • Regex requirementPattern only matches lowercase families — document this constraint if intentional.

This review was generated by /uf.review-pr (AI-assisted).

Comment thread internal/evaluator/evaluator.go Outdated
Comment thread internal/evaluator/opa.go Outdated
Comment thread internal/coverage/coverage.go
Comment thread internal/mcp/tools.go Outdated
Comment thread cmd/complypack/cli/pack.go
@em-redhat
em-redhat force-pushed the opsx/per-requirement-test-attribution branch from a515d98 to a4a55c0 Compare August 6, 2026 14:38
yvonnedevlinrh
yvonnedevlinrh previously approved these changes Aug 11, 2026

@yvonnedevlinrh yvonnedevlinrh 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.

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.IsNotExisterrors.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 over coverage.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 sonupreetam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-duplicate findings from review council (3 items, all LOW/non-blocking)

1. AGENTS.md:14internal/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.

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)
@em-redhat
em-redhat force-pushed the opsx/per-requirement-test-attribution branch from 024bc3e to 3929052 Compare August 12, 2026 14:00

@em-redhat em-redhat left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the thorough review @sonupreetam — all 4 findings addressed in 3929052:

1. AGENTS.md:14internal/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:

  • perRequirement absent → feature not available (older complypack)
  • perRequirement: {} → attribution attempted, no tests matched convention
  • perRequirement: {"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.

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.

feat: per-requirement test attribution in coverage reports

4 participants