Files
cleveragents-core/features/steps/structural_component_validation_steps.py
T
freemo 904cf2b3d4 feat: implement invariant enforcement and validation pipeline
Added three new modules for M3 Epic #8137:
- invariant_enforcer.py detects violations during execution with
  actionable error messages; raises InvariantEnforcementError (blocking)
  or InvariantEnforcementWarning (non-blocking).
- structural_component_validator.py validates output via flexible
  substring, wildcard, regex, and field-path matching instead of exact
  character equality, making tests resilient to cosmetic changes.
- phase_transition_gate.py gates plan phase transitions by verifying
  invariant enforcement decisions and required validation results,
  blocking Apply when critical constraints are violated.

All modules persist results as validation_response decisions in the
decision tree for auditability. Includes Behave BDD test scenarios.

ISSUES CLOSED: #8137
2026-06-14 13:17:17 -04:00

137 lines
4.3 KiB
Python

"""Step definitions for Structural Component Validator - Epic #8137."""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.structural_component_validator import (
ComponentCheckResult,
ExpectKind,
StructuralExpectation,
ValidationOutcome,
)
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given("a present structural expectation on keyword {keyword!r}")
def step_present(context: Context, keyword: str) -> None:
"""Create a presence expectation with case-insensitive matching."""
name = f"present-on-{keyword.replace(' ', '-')}"
context.expectations = [
StructuralExpectation(
name=name,
kind=ExpectKind.PRESENT,
value=keyword,
case_sensitive=False,
weight=1.0,
),
]
@given("an absent structural expectation on keyword {keyword!r}")
def step_absent(context: Context, keyword: str) -> None:
"""Create an absence expectation with case-insensitive matching."""
name = f"absent-on-{keyword.replace(' ', '-')}"
context.expectations = [
StructuralExpectation(
name=name,
kind=ExpectKind.ABSENT,
value=keyword,
case_sensitive=False,
weight=1.0,
),
]
@given("a pattern expectation on regex {pattern!r}")
def step_pattern(context: Context, pattern: str) -> None:
"""Create a pattern expectation with case-insensitive matching."""
context.expectations = [
StructuralExpectation(
name="pattern-match",
kind=ExpectKind.PATTERN,
value=pattern,
case_sensitive=False,
weight=1.0,
),
]
@given("a present expectation on field path {field_path!r}")
def step_nested(context: Context, field_path: str) -> None:
"""Create a nested-field-presence expectation."""
context.expectations = [
StructuralExpectation(
name=f"field-present-{field_path}",
kind=ExpectKind.FIELD_PRESENT,
value=field_path,
case_sensitive=False,
weight=1.0,
),
]
@given("the actual output reads: {output_text!r}")
def step_actual_output(context: Context, output_text: str) -> None:
"""Set the actual output string."""
context.actual_outputs = [output_text]
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when("I validate the outputs against the expectations")
def step_validate(context: Context) -> None:
"""Validate structural expectations against outputs."""
from cleveragents.application.services.structural_component_validator import (
validate_structural_components,
)
outputs = getattr(context, "actual_outputs", [""])
context.outcome = validate_structural_components(
expectations=context.expectations,
actual_outputs=outputs,
)
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then("the ValidationOutcome should have passed={value}")
def step_validate_passed(context: Context, value: str) -> None:
"""Assert overall pass/fail."""
outcome: ValidationOutcome = context.outcome
expected = value.lower() == "true"
assert outcome.passed is expected, (
f"Expected {expected} but got {outcome.passed}. "
f"Failed: {outcome.failed_count}, Partial: {outcome.partial_count}"
)
@then("total_checks should equal {count:d}")
def step_check_total(context: Context, count: int) -> None:
"""Assert total component check count."""
outcome: ValidationOutcome = context.outcome
assert outcome.total_checks == count, (
f"Expected {count} checks but got {outcome.total_checks}"
)
@then("all checks should PASS")
def step_all_pass(context: Context) -> None:
"""Assert every individual component passed."""
outcome: ValidationOutcome = context.outcome
for check in outcome.results:
assert check.result == ComponentCheckResult.PASS, (
f"Component '{check.expectation.name}' not PASS."
)