904cf2b3d4
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
137 lines
4.3 KiB
Python
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."
|
|
)
|