forked from HAL9000/cleveragents-core
3cf3f1f69e
Implemented FixThenRevalidateOrchestrator with full diagnosis → self-fix → re-validation → retry limit → strategy revision → user escalation → terminal failure flow. Added retry counting per validation per plan, configurable retry limit (default 3), auto_strategy_revision flag support, and validation_fix_history recording in plan execution metadata. Review fixes applied (round 1): - Added try/except around fix_callback and revalidate_callback (spec: validation errors treated as required failure regardless of mode) - Wired optional EventBus for VALIDATION_FIX_ATTEMPTED/SUCCEEDED/EXHAUSTED events - Fixed total_attempts derivation from fix_history length instead of cumulative retry counts - Updated module docstring to reflect implemented vs caller-responsible steps - Added exhausted-retry logging on re-invocation - Replaced bare assert in Robot helper with explicit _check() function - Added 10 new Behave scenarios covering exception paths, multi-failure, reset, field validation, boundary values, event_bus property, and exhausted re-invocation Review fixes applied (round 2 — PR #711): - B1+B7: Fixed TOCTOU race in _fix_single_validation; atomic claim-per-iteration under RLock; eliminated defaultdict auto-vivification with .get() reads - B2: Added validation_name mismatch check on revalidate_callback return - B3: Added fix_description truncation to 2000 chars before FixAttemptRecord - T1: Switched threading.Lock to threading.RLock for defensive reentrancy - A1: Added model_validator preventing escalated+terminal_failure both True - R1: Added max_length=255 to FixAttemptRecord.validation_name - S7: Fixed misleading docstring about automation_profile integration - D4: Fixed timestamp field description to specify UTC - D1: Renamed misleading benchmark time_fix_after_two_retries - Added 5 new Behave scenarios for truncation, model constraints, name mismatch ISSUES CLOSED: #583
963 lines
33 KiB
Python
963 lines
33 KiB
Python
"""Step definitions for Fix-then-Revalidate coverage boost scenarios.
|
|
|
|
Covers validation edge cases, property accessors, error paths,
|
|
callback exception handling, multi-failure scenarios, and boundary
|
|
tests to ensure >=97% coverage on fix_then_revalidate.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from pydantic import ValidationError as PydanticValidationError
|
|
|
|
from cleveragents.application.services.fix_then_revalidate import (
|
|
FixAttemptRecord,
|
|
FixThenRevalidateOrchestrator,
|
|
FixThenRevalidateResult,
|
|
)
|
|
from cleveragents.application.services.validation_pipeline import (
|
|
ValidationPipeline,
|
|
ValidationResult,
|
|
)
|
|
from cleveragents.core.exceptions import ValidationError
|
|
from cleveragents.domain.models.core.tool import ValidationMode
|
|
from cleveragents.infrastructure.events.models import DomainEvent
|
|
from cleveragents.infrastructure.events.types import EventType
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _mock_executor(validation_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
return {"passed": True, "message": f"{validation_name} ok"}
|
|
|
|
|
|
def _make_pipeline() -> ValidationPipeline:
|
|
return ValidationPipeline(commands=[], executor=_mock_executor)
|
|
|
|
|
|
def _always_fix(result: ValidationResult) -> str:
|
|
return f"Fixed {result.validation_name}"
|
|
|
|
|
|
def _always_pass(result: ValidationResult) -> ValidationResult:
|
|
return ValidationResult(
|
|
validation_name=result.validation_name,
|
|
resource_id=result.resource_id,
|
|
resource_name=result.resource_name,
|
|
mode=result.mode,
|
|
passed=True,
|
|
message="passed",
|
|
data=result.data,
|
|
duration_ms=1.0,
|
|
)
|
|
|
|
|
|
def _make_failed_result(
|
|
name: str,
|
|
mode: ValidationMode = ValidationMode.REQUIRED,
|
|
) -> ValidationResult:
|
|
return ValidationResult(
|
|
validation_name=name,
|
|
resource_id="RES0001",
|
|
resource_name="test-resource",
|
|
mode=mode,
|
|
passed=False,
|
|
message=f"{name} failed: expected condition not met",
|
|
data={"hint": "check configuration"},
|
|
duration_ms=10.0,
|
|
)
|
|
|
|
|
|
def _raising_fix(result: ValidationResult) -> str:
|
|
raise RuntimeError(f"Fix callback crash for {result.validation_name}")
|
|
|
|
|
|
class _RaisingRevalidateCallback:
|
|
"""Revalidate callback that always raises."""
|
|
|
|
def __call__(self, result: ValidationResult) -> ValidationResult:
|
|
raise RuntimeError(f"Revalidate callback crash for {result.validation_name}")
|
|
|
|
|
|
class _NeverPassRevalidateCallback:
|
|
"""Revalidate callback that never passes."""
|
|
|
|
def __call__(self, result: ValidationResult) -> ValidationResult:
|
|
return ValidationResult(
|
|
validation_name=result.validation_name,
|
|
resource_id=result.resource_id,
|
|
resource_name=result.resource_name,
|
|
mode=result.mode,
|
|
passed=False,
|
|
message="still failing",
|
|
data=result.data,
|
|
duration_ms=5.0,
|
|
)
|
|
|
|
|
|
class _CountingRevalidateCallback:
|
|
"""Revalidate callback that passes after N calls."""
|
|
|
|
def __init__(self, pass_after: int) -> None:
|
|
self._pass_after = pass_after
|
|
self._count = 0
|
|
|
|
def __call__(self, result: ValidationResult) -> ValidationResult:
|
|
self._count += 1
|
|
passed = self._count >= self._pass_after
|
|
return ValidationResult(
|
|
validation_name=result.validation_name,
|
|
resource_id=result.resource_id,
|
|
resource_name=result.resource_name,
|
|
mode=result.mode,
|
|
passed=passed,
|
|
message=f"{'passed' if passed else 'still failing'} "
|
|
f"(attempt {self._count})",
|
|
data=result.data,
|
|
duration_ms=5.0,
|
|
)
|
|
|
|
|
|
class _PerValidationRevalidateCallback:
|
|
"""Revalidate callback with per-validation pass-after config."""
|
|
|
|
def __init__(self, config: dict[str, int | None]) -> None:
|
|
self._config = config
|
|
self._counts: dict[str, int] = {}
|
|
|
|
def __call__(self, result: ValidationResult) -> ValidationResult:
|
|
vname = result.validation_name
|
|
self._counts[vname] = self._counts.get(vname, 0) + 1
|
|
pass_after = self._config.get(vname)
|
|
passed = pass_after is not None and self._counts[vname] >= pass_after
|
|
return ValidationResult(
|
|
validation_name=result.validation_name,
|
|
resource_id=result.resource_id,
|
|
resource_name=result.resource_name,
|
|
mode=result.mode,
|
|
passed=passed,
|
|
message=f"{'passed' if passed else 'still failing'} "
|
|
f"(attempt {self._counts[vname]})",
|
|
data=result.data,
|
|
duration_ms=5.0,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a fix-revalidate coverage orchestrator with max_retries {n:d}")
|
|
def step_given_coverage_orchestrator(context: Context, n: int) -> None:
|
|
context.frc_orchestrator = FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=n,
|
|
auto_strategy_revision=1.0,
|
|
)
|
|
context.frc_error = None
|
|
context.frc_failed_results = []
|
|
context.frc_fix_callback = _always_fix
|
|
context.frc_revalidate_callback = _CountingRevalidateCallback(pass_after=1)
|
|
|
|
|
|
@given("a fix-revalidate coverage orchestrator with auto_strategy_revision enabled")
|
|
def step_given_coverage_orchestrator_asr(context: Context) -> None:
|
|
context.frc_orchestrator = FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=3,
|
|
auto_strategy_revision=0.0,
|
|
)
|
|
context.frc_error = None
|
|
context.frc_failed_results = []
|
|
context.frc_fix_callback = _always_fix
|
|
context.frc_revalidate_callback = _CountingRevalidateCallback(pass_after=1)
|
|
|
|
|
|
@given('a required coverage validation "{name}" that fails')
|
|
def step_given_required_coverage_validation(context: Context, name: str) -> None:
|
|
context.frc_failed_results.append(_make_failed_result(name))
|
|
|
|
|
|
@given("a fix-revalidate coverage fix callback that raises an exception")
|
|
def step_given_raising_fix_callback(context: Context) -> None:
|
|
context.frc_fix_callback = _raising_fix
|
|
|
|
|
|
@given("a fix-revalidate coverage fix callback that always succeeds")
|
|
def step_given_always_fix_callback(context: Context) -> None:
|
|
context.frc_fix_callback = _always_fix
|
|
|
|
|
|
@given("a fix-revalidate coverage revalidate callback that never passes")
|
|
def step_given_never_pass_revalidate(context: Context) -> None:
|
|
context.frc_revalidate_callback = _NeverPassRevalidateCallback()
|
|
|
|
|
|
@given("a fix-revalidate coverage revalidate callback that raises an exception")
|
|
def step_given_raising_revalidate(context: Context) -> None:
|
|
context.frc_revalidate_callback = _RaisingRevalidateCallback()
|
|
|
|
|
|
@given(
|
|
"a fix-revalidate coverage revalidate callback where "
|
|
"check-a passes after 1 and check-b never passes"
|
|
)
|
|
def step_given_per_validation_revalidate(context: Context) -> None:
|
|
context.frc_revalidate_callback = _PerValidationRevalidateCallback(
|
|
config={"check-a": 1, "check-b": None}
|
|
)
|
|
|
|
|
|
@given("a fix-revalidate coverage revalidate callback that passes after {n:d}")
|
|
def step_given_counting_revalidate(context: Context, n: int) -> None:
|
|
context.frc_revalidate_callback = _CountingRevalidateCallback(pass_after=n)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps (constructor validation)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("the fix-revalidate orchestrator is created with None pipeline")
|
|
def step_when_create_none_pipeline(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_pipeline: Any = None
|
|
FixThenRevalidateOrchestrator(
|
|
validation_pipeline=bad_pipeline,
|
|
max_retries=3,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate orchestrator is created with non-integer max_retries")
|
|
def step_when_create_non_int_max_retries(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_max_retries: Any = "three"
|
|
FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=bad_max_retries,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate orchestrator is created with max_retries {n:d}")
|
|
def step_when_create_bad_max_retries(context: Context, n: int) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=n,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps (method validation)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("the fix-revalidate get_retry_count is called with empty plan_id")
|
|
def step_when_get_retry_count_empty_plan(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
context.frc_orchestrator.get_retry_count("", "check-a")
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate get_retry_count is called with empty validation_name")
|
|
def step_when_get_retry_count_empty_vname(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
context.frc_orchestrator.get_retry_count("P1", "")
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate reset_retry_counts is called with empty plan_id")
|
|
def step_when_reset_empty_plan(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
context.frc_orchestrator.reset_retry_counts("")
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate run_fix_loop is called with empty plan_id")
|
|
def step_when_run_loop_empty_plan(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
context.frc_orchestrator.run_fix_loop(
|
|
plan_id="",
|
|
failed_results=[],
|
|
fix_callback=_always_fix,
|
|
revalidate_callback=_always_pass,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate run_fix_loop is called with None failed_results")
|
|
def step_when_run_loop_none_results(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_results: Any = None
|
|
context.frc_orchestrator.run_fix_loop(
|
|
plan_id="P1",
|
|
failed_results=bad_results,
|
|
fix_callback=_always_fix,
|
|
revalidate_callback=_always_pass,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate run_fix_loop is called with None fix_callback")
|
|
def step_when_run_loop_none_fix(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_callback: Any = None
|
|
context.frc_orchestrator.run_fix_loop(
|
|
plan_id="P1",
|
|
failed_results=[],
|
|
fix_callback=bad_callback,
|
|
revalidate_callback=_always_pass,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate run_fix_loop is called with None revalidate_callback")
|
|
def step_when_run_loop_none_revalidate(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_callback: Any = None
|
|
context.frc_orchestrator.run_fix_loop(
|
|
plan_id="P1",
|
|
failed_results=[],
|
|
fix_callback=_always_fix,
|
|
revalidate_callback=bad_callback,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when('the fix-revalidate coverage loop runs for plan "{plan_id}"')
|
|
def step_when_run_coverage_loop(context: Context, plan_id: str) -> None:
|
|
result: FixThenRevalidateResult = context.frc_orchestrator.run_fix_loop(
|
|
plan_id=plan_id,
|
|
failed_results=context.frc_failed_results,
|
|
fix_callback=context.frc_fix_callback,
|
|
revalidate_callback=context.frc_revalidate_callback,
|
|
)
|
|
context.frc_result = result
|
|
|
|
|
|
@when('the fix-revalidate coverage retry counts are reset for plan "{plan_id}"')
|
|
def step_when_reset_coverage_counts(context: Context, plan_id: str) -> None:
|
|
context.frc_orchestrator.reset_retry_counts(plan_id)
|
|
|
|
|
|
@when("a fix-revalidate coverage revalidate callback that passes after {n:d}")
|
|
def step_when_set_counting_revalidate(context: Context, n: int) -> None:
|
|
context.frc_revalidate_callback = _CountingRevalidateCallback(pass_after=n)
|
|
|
|
|
|
@when(
|
|
'the fix-revalidate coverage loop runs again for plan "{plan_id}" '
|
|
'with validation "{vname}"'
|
|
)
|
|
def step_when_run_coverage_loop_again(
|
|
context: Context, plan_id: str, vname: str
|
|
) -> None:
|
|
failed_results = [_make_failed_result(vname)]
|
|
result: FixThenRevalidateResult = context.frc_orchestrator.run_fix_loop(
|
|
plan_id=plan_id,
|
|
failed_results=failed_results,
|
|
fix_callback=context.frc_fix_callback,
|
|
revalidate_callback=context.frc_revalidate_callback,
|
|
)
|
|
context.frc_result = result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then('a fix-revalidate validation error should be raised with "{text}"')
|
|
def step_then_validation_error(context: Context, text: str) -> None:
|
|
assert context.frc_error is not None, (
|
|
"Expected a ValidationError but none was raised"
|
|
)
|
|
assert text in context.frc_error, (
|
|
f"Expected error containing '{text}', got '{context.frc_error}'"
|
|
)
|
|
|
|
|
|
@then("the fix-revalidate max_retries property should return {n:d}")
|
|
def step_then_max_retries_property(context: Context, n: int) -> None:
|
|
assert context.frc_orchestrator.max_retries == n, (
|
|
f"Expected max_retries={n}, got {context.frc_orchestrator.max_retries}"
|
|
)
|
|
|
|
|
|
@then("the fix-revalidate auto_strategy_revision property should be 0.0")
|
|
def step_then_asr_property(context: Context) -> None:
|
|
assert context.frc_orchestrator.auto_strategy_revision == 0.0, (
|
|
"Expected auto_strategy_revision=0.0"
|
|
)
|
|
|
|
|
|
@then(
|
|
'the fix-revalidate get_retry_count for plan "{plan_id}" '
|
|
'validation "{vname}" should be {n:d}'
|
|
)
|
|
def step_then_retry_count(context: Context, plan_id: str, vname: str, n: int) -> None:
|
|
count = context.frc_orchestrator.get_retry_count(plan_id, vname)
|
|
assert count == n, f"Expected retry count {n}, got {count}"
|
|
|
|
|
|
@then("the fix-revalidate coverage result should have final_passed {expected}")
|
|
def step_then_coverage_final_passed(context: Context, expected: str) -> None:
|
|
result: FixThenRevalidateResult = context.frc_result
|
|
expected_bool = expected.strip().lower() == "true"
|
|
assert result.final_passed is expected_bool, (
|
|
f"Expected final_passed={expected_bool}, got {result.final_passed}"
|
|
)
|
|
|
|
|
|
@then("the fix-revalidate coverage result should be terminal failure")
|
|
def step_then_coverage_terminal(context: Context) -> None:
|
|
result: FixThenRevalidateResult = context.frc_result
|
|
assert result.terminal_failure is True, "Expected terminal_failure=True"
|
|
|
|
|
|
@then("the fix-revalidate coverage result should not be terminal failure")
|
|
def step_then_coverage_not_terminal(context: Context) -> None:
|
|
result: FixThenRevalidateResult = context.frc_result
|
|
assert result.terminal_failure is False, "Expected terminal_failure=False"
|
|
|
|
|
|
@then("the fix-revalidate coverage result should need user escalation")
|
|
def step_then_coverage_needs_user_escalation(context: Context) -> None:
|
|
result: FixThenRevalidateResult = context.frc_result
|
|
assert result.needs_user_escalation is True, "Expected needs_user_escalation=True"
|
|
|
|
|
|
@then("the fix-revalidate coverage validation fix history should have {n:d} records")
|
|
def step_then_coverage_fix_history_count(context: Context, n: int) -> None:
|
|
result: FixThenRevalidateResult = context.frc_result
|
|
assert len(result.validation_fix_history) == n, (
|
|
f"Expected {n} fix history records, got {len(result.validation_fix_history)}"
|
|
)
|
|
|
|
|
|
@then(
|
|
"each fix-revalidate coverage validation fix history record "
|
|
"should have success False"
|
|
)
|
|
def step_then_all_records_failed(context: Context) -> None:
|
|
result: FixThenRevalidateResult = context.frc_result
|
|
for i, rec in enumerate(result.validation_fix_history):
|
|
assert rec.success is False, (
|
|
f"Record {i + 1} has success={rec.success}, expected False"
|
|
)
|
|
|
|
|
|
@then("the fix-revalidate coverage result should have validation_attempts {n:d}")
|
|
def step_then_coverage_total_attempts(context: Context, n: int) -> None:
|
|
result: FixThenRevalidateResult = context.frc_result
|
|
assert result.validation_attempts == n, (
|
|
f"Expected validation_attempts={n}, got {result.validation_attempts}"
|
|
)
|
|
|
|
|
|
@then(
|
|
"fix-revalidate coverage validation fix history record {idx:d} "
|
|
'should have validation_name "{name}"'
|
|
)
|
|
def step_then_record_vname(context: Context, idx: int, name: str) -> None:
|
|
result: FixThenRevalidateResult = context.frc_result
|
|
record = result.validation_fix_history[idx - 1]
|
|
assert record.validation_name == name, (
|
|
f"Record {idx} validation_name={record.validation_name}, expected {name}"
|
|
)
|
|
|
|
|
|
@then(
|
|
"fix-revalidate coverage validation fix history record {idx:d} "
|
|
"should have attempt_number {n:d}"
|
|
)
|
|
def step_then_record_attempt_number(context: Context, idx: int, n: int) -> None:
|
|
result: FixThenRevalidateResult = context.frc_result
|
|
record = result.validation_fix_history[idx - 1]
|
|
assert record.attempt_number == n, (
|
|
f"Record {idx} attempt_number={record.attempt_number}, expected {n}"
|
|
)
|
|
|
|
|
|
@then(
|
|
"fix-revalidate coverage validation fix history record {idx:d} "
|
|
"should have success {expected}"
|
|
)
|
|
def step_then_record_success(context: Context, idx: int, expected: str) -> None:
|
|
result: FixThenRevalidateResult = context.frc_result
|
|
record = result.validation_fix_history[idx - 1]
|
|
expected_bool = expected.strip().lower() == "true"
|
|
assert record.success is expected_bool, (
|
|
f"Record {idx} success={record.success}, expected {expected_bool}"
|
|
)
|
|
|
|
|
|
@then("the fix-revalidate event_bus property should be None")
|
|
def step_then_event_bus_none(context: Context) -> None:
|
|
assert context.frc_orchestrator.event_bus is None, "Expected event_bus=None"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mock EventBus for M4 review fix
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _MockEventBus:
|
|
"""Mock EventBus that records emitted events."""
|
|
|
|
def __init__(self) -> None:
|
|
self.events: list[DomainEvent] = []
|
|
|
|
def emit(self, event: DomainEvent) -> None:
|
|
self.events.append(event)
|
|
|
|
def subscribe(
|
|
self,
|
|
event_type: EventType,
|
|
handler: Any,
|
|
) -> None:
|
|
pass # Not needed for this test
|
|
|
|
|
|
class _FailingEventBus:
|
|
"""Mock EventBus whose emit always raises."""
|
|
|
|
def emit(self, event: DomainEvent) -> None:
|
|
raise RuntimeError("EventBus emit failure")
|
|
|
|
def subscribe(
|
|
self,
|
|
event_type: EventType,
|
|
handler: Any,
|
|
) -> None:
|
|
pass
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps for new review-fix scenarios
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given(
|
|
"a fix-revalidate coverage orchestrator with max_retries {n:d} and a mock event bus"
|
|
)
|
|
def step_given_coverage_orchestrator_with_mock_bus(context: Context, n: int) -> None:
|
|
context.frc_mock_event_bus = _MockEventBus()
|
|
context.frc_orchestrator = FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=n,
|
|
auto_strategy_revision=1.0,
|
|
event_bus=context.frc_mock_event_bus,
|
|
)
|
|
context.frc_error = None
|
|
context.frc_failed_results = []
|
|
context.frc_fix_callback = _always_fix
|
|
context.frc_revalidate_callback = _CountingRevalidateCallback(pass_after=1)
|
|
|
|
|
|
@given(
|
|
"a fix-revalidate coverage orchestrator with max_retries {n:d} "
|
|
"and a failing event bus"
|
|
)
|
|
def step_given_coverage_orchestrator_with_failing_bus(context: Context, n: int) -> None:
|
|
context.frc_mock_event_bus = _FailingEventBus()
|
|
context.frc_orchestrator = FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
auto_strategy_revision=1.0,
|
|
max_retries=n,
|
|
event_bus=context.frc_mock_event_bus,
|
|
)
|
|
context.frc_error = None
|
|
context.frc_failed_results = []
|
|
context.frc_fix_callback = _always_fix
|
|
context.frc_revalidate_callback = _CountingRevalidateCallback(pass_after=1)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps for new review-fix scenarios
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("the fix-revalidate orchestrator is created with boolean max_retries True")
|
|
def step_when_create_bool_true_max_retries(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_retries: Any = True
|
|
FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=bad_retries,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate orchestrator is created with boolean max_retries False")
|
|
def step_when_create_bool_false_max_retries(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_retries: Any = False
|
|
FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=bad_retries,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("a fix-revalidate FixAttemptRecord is created with attempt_number 0")
|
|
def step_when_create_record_bad_attempt(context: Context) -> None:
|
|
context.frc_pydantic_error = None
|
|
try:
|
|
FixAttemptRecord(
|
|
attempt_number=0,
|
|
validation_name="check-a",
|
|
fix_description="test",
|
|
success=False,
|
|
)
|
|
except PydanticValidationError as exc:
|
|
context.frc_pydantic_error = str(exc)
|
|
|
|
|
|
@when("a fix-revalidate FixAttemptRecord is created with whitespace validation_name")
|
|
def step_when_create_record_whitespace_vname(context: Context) -> None:
|
|
context.frc_pydantic_error = None
|
|
try:
|
|
FixAttemptRecord(
|
|
attempt_number=1,
|
|
validation_name=" ",
|
|
fix_description="test",
|
|
success=False,
|
|
)
|
|
except PydanticValidationError as exc:
|
|
context.frc_pydantic_error = str(exc)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps for new review-fix scenarios
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the mock event bus should have received {n:d} events")
|
|
def step_then_mock_bus_event_count(context: Context, n: int) -> None:
|
|
bus: _MockEventBus = context.frc_mock_event_bus
|
|
assert len(bus.events) == n, (
|
|
f"Expected {n} events, got {len(bus.events)}: "
|
|
f"{[e.event_type for e in bus.events]}"
|
|
)
|
|
|
|
|
|
@then('mock event bus event {idx:d} should have event_type "{etype}"')
|
|
def step_then_mock_bus_event_type(context: Context, idx: int, etype: str) -> None:
|
|
bus: _MockEventBus = context.frc_mock_event_bus
|
|
event = bus.events[idx - 1]
|
|
assert event.event_type.value == etype, (
|
|
f"Event {idx} type={event.event_type.value}, expected {etype}"
|
|
)
|
|
|
|
|
|
@then("a fix-revalidate pydantic validation error should be raised")
|
|
def step_then_pydantic_error(context: Context) -> None:
|
|
assert context.frc_pydantic_error is not None, (
|
|
"Expected a Pydantic ValidationError but none was raised"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Steps for B3, A1, R1, B2 review fixes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _long_description_fix(result: ValidationResult) -> str:
|
|
"""Fix callback that returns a description exceeding 2000 chars."""
|
|
return "x" * 3000
|
|
|
|
|
|
class _MismatchedRevalidateCallback:
|
|
"""Revalidate callback that returns a result with the wrong name."""
|
|
|
|
def __call__(self, result: ValidationResult) -> ValidationResult:
|
|
return ValidationResult(
|
|
validation_name="wrong-validation-name",
|
|
resource_id=result.resource_id,
|
|
resource_name=result.resource_name,
|
|
mode=result.mode,
|
|
passed=True,
|
|
message="passed but with wrong name",
|
|
data=result.data,
|
|
duration_ms=1.0,
|
|
)
|
|
|
|
|
|
@given("a fix-revalidate coverage fix callback that returns a 3000 char description")
|
|
def step_given_long_fix_callback(context: Context) -> None:
|
|
context.frc_fix_callback = _long_description_fix
|
|
|
|
|
|
@given(
|
|
"a fix-revalidate coverage revalidate callback "
|
|
"that returns a different validation_name"
|
|
)
|
|
def step_given_mismatched_revalidate(context: Context) -> None:
|
|
context.frc_revalidate_callback = _MismatchedRevalidateCallback()
|
|
|
|
|
|
@when(
|
|
"a fix-revalidate FixThenRevalidateResult is created with "
|
|
"escalated True and terminal_failure True"
|
|
)
|
|
def step_when_create_result_both_flags(context: Context) -> None:
|
|
context.frc_pydantic_error = None
|
|
try:
|
|
FixThenRevalidateResult(
|
|
validation_attempts=3,
|
|
final_passed=False,
|
|
escalated=True,
|
|
terminal_failure=True,
|
|
)
|
|
except PydanticValidationError as exc:
|
|
context.frc_pydantic_error = str(exc)
|
|
|
|
|
|
@when(
|
|
"a fix-revalidate FixAttemptRecord is created "
|
|
"with validation_name exceeding 255 chars"
|
|
)
|
|
def step_when_create_record_long_vname(context: Context) -> None:
|
|
context.frc_pydantic_error = None
|
|
try:
|
|
FixAttemptRecord(
|
|
attempt_number=1,
|
|
validation_name="v" * 256,
|
|
fix_description="test",
|
|
success=False,
|
|
)
|
|
except PydanticValidationError as exc:
|
|
context.frc_pydantic_error = str(exc)
|
|
|
|
|
|
@then(
|
|
"fix-revalidate coverage validation fix history record {idx:d} "
|
|
"fix_description should be at most {n:d} chars"
|
|
)
|
|
def step_then_record_fix_description_length(context: Context, idx: int, n: int) -> None:
|
|
result: FixThenRevalidateResult = context.frc_result
|
|
record = result.validation_fix_history[idx - 1]
|
|
assert len(record.fix_description) <= n, (
|
|
f"Record {idx} fix_description has {len(record.fix_description)} chars, "
|
|
f"expected at most {n}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# New steps for review-fix round 2/3 coverage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("the fix-revalidate orchestrator is created with boolean auto_strategy_revision")
|
|
def step_when_create_bool_asr(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_asr: Any = True
|
|
FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=3,
|
|
auto_strategy_revision=bad_asr,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate orchestrator is created with auto_strategy_revision {val}")
|
|
def step_when_create_bad_asr_range(context: Context, val: str) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=3,
|
|
auto_strategy_revision=float(val),
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate orchestrator is created with boolean auto_validation_fix")
|
|
def step_when_create_bool_avf(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_avf: Any = True
|
|
FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=3,
|
|
auto_validation_fix=bad_avf,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate orchestrator is created with auto_validation_fix {val}")
|
|
def step_when_create_bad_avf_range(context: Context, val: str) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=3,
|
|
auto_validation_fix=float(val),
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate orchestrator is created with invalid event_bus type")
|
|
def step_when_create_bad_event_bus(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_bus: Any = 42
|
|
FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=3,
|
|
event_bus=bad_bus,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate run_fix_loop is called with non-callable fix_callback")
|
|
def step_when_run_loop_non_callable_fix(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_fix: Any = "not_callable"
|
|
context.frc_orchestrator.run_fix_loop(
|
|
plan_id="P1",
|
|
failed_results=[],
|
|
fix_callback=bad_fix,
|
|
revalidate_callback=_always_pass,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
@when("the fix-revalidate run_fix_loop is called with non-callable revalidate_callback")
|
|
def step_when_run_loop_non_callable_revalidate(context: Context) -> None:
|
|
context.frc_error = None
|
|
try:
|
|
bad_revalidate: Any = "not_callable"
|
|
context.frc_orchestrator.run_fix_loop(
|
|
plan_id="P1",
|
|
failed_results=[],
|
|
fix_callback=_always_fix,
|
|
revalidate_callback=bad_revalidate,
|
|
)
|
|
except ValidationError as exc:
|
|
context.frc_error = str(exc)
|
|
|
|
|
|
def _none_fix(result: ValidationResult) -> None:
|
|
"""Fix callback that returns None to signal unfixable."""
|
|
return None
|
|
|
|
|
|
@given("a fix-revalidate coverage fix callback that returns None")
|
|
def step_given_none_fix_callback(context: Context) -> None:
|
|
context.frc_fix_callback = _none_fix
|
|
|
|
|
|
class _PassedWithErrorRevalidateCallback:
|
|
"""Revalidate callback that returns passed=True with an error set."""
|
|
|
|
def __call__(self, result: ValidationResult) -> ValidationResult:
|
|
return ValidationResult(
|
|
validation_name=result.validation_name,
|
|
resource_id=result.resource_id,
|
|
resource_name=result.resource_name,
|
|
mode=result.mode,
|
|
passed=True,
|
|
message="passed but has error",
|
|
data=result.data,
|
|
duration_ms=1.0,
|
|
error="runtime error in validation",
|
|
)
|
|
|
|
|
|
@given("a fix-revalidate coverage revalidate callback that returns passed with error")
|
|
def step_given_passed_with_error_revalidate(context: Context) -> None:
|
|
context.frc_revalidate_callback = _PassedWithErrorRevalidateCallback()
|
|
|
|
|
|
@when(
|
|
"a fix-revalidate FixThenRevalidateResult is created "
|
|
"with final_passed True and escalated True"
|
|
)
|
|
def step_when_create_result_final_passed_escalated(context: Context) -> None:
|
|
context.frc_pydantic_error = None
|
|
try:
|
|
FixThenRevalidateResult(
|
|
validation_attempts=1,
|
|
final_passed=True,
|
|
escalated=True,
|
|
)
|
|
except PydanticValidationError as exc:
|
|
context.frc_pydantic_error = str(exc)
|
|
|
|
|
|
@then("the fix-revalidate auto_validation_fix property should be {val}")
|
|
def step_then_avf_property(context: Context, val: str) -> None:
|
|
expected = float(val)
|
|
actual = context.frc_orchestrator.auto_validation_fix
|
|
assert actual == expected, f"Expected auto_validation_fix={expected}, got {actual}"
|
|
|
|
|
|
@given(
|
|
"a fix-revalidate coverage orchestrator with max_retries {n:d} "
|
|
"and mock event bus and escalation"
|
|
)
|
|
def step_given_coverage_orchestrator_mock_bus_escalation(
|
|
context: Context, n: int
|
|
) -> None:
|
|
context.frc_mock_event_bus = _MockEventBus()
|
|
context.frc_orchestrator = FixThenRevalidateOrchestrator(
|
|
validation_pipeline=_make_pipeline(),
|
|
max_retries=n,
|
|
auto_strategy_revision=0.0,
|
|
event_bus=context.frc_mock_event_bus,
|
|
)
|
|
context.frc_error = None
|
|
context.frc_failed_results = []
|
|
context.frc_fix_callback = _always_fix
|
|
context.frc_revalidate_callback = _CountingRevalidateCallback(pass_after=1)
|
|
|
|
|
|
@given('an informational coverage validation "{name}" that fails')
|
|
def step_given_informational_coverage_validation(context: Context, name: str) -> None:
|
|
context.frc_failed_results.append(
|
|
_make_failed_result(name, ValidationMode.INFORMATIONAL)
|
|
)
|