Files
cleveragents-core/features/steps/validation_pipeline_steps.py
T
HAL9000 bd955696f9 fix(pr-7811 review): resolve remaining CI and code style blockers
Address all 4 remaining blocking issues from PR #7811 review:

- Delete duplicate _validation_pipeline_mock.py in features/steps/
  (the inline MockValidationExecutor is kept where it belongs)
- Replace # type: ignore[return-value] with cast(dict[str, Any], ...)
  to satisfy the zero-type-suppressions policy for test additions
- Add @tdd_issue_7623 regression tag on concurrency scenario
- Remove unreachable dead-code RuntimeError guard in
  _install_thread_local_streams() (constructors never return None)

All CI lint checks now pass. Source files remain within line limits.
2026-06-02 04:29:20 -04:00

426 lines
15 KiB
Python

"""Step definitions for validation pipeline scenarios.
All step names are prefixed with 'validation pipeline' context to avoid
AmbiguousStep collisions with existing step definitions.
Uses the ``re`` step matcher for patterns that have optional trailing parts.
"""
from __future__ import annotations
from typing import Any
from behave import given, then, use_step_matcher, when
from cleveragents.application.services.validation_pipeline import (
ValidationCommand,
ValidationPipeline,
ValidationResult,
ValidationSummary,
)
from cleveragents.domain.models.core.tool import ValidationMode
from features.mocks.validation_pipeline_mock import MockValidationExecutor
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_RESOURCE_ID_COUNTER: int = 0
def _next_resource_id() -> str:
global _RESOURCE_ID_COUNTER
_RESOURCE_ID_COUNTER += 1
return f"R{_RESOURCE_ID_COUNTER:05d}"
# ---------------------------------------------------------------------------
# Given steps — use "re" matcher for the required/informational patterns
# ---------------------------------------------------------------------------
use_step_matcher("re")
@given(r"a validation pipeline test environment")
def step_given_pipeline_env(context: Any) -> None:
context.vp_commands: list[ValidationCommand] = []
context.vp_executor = MockValidationExecutor()
context.vp_max_workers: int = 4
context.vp_read_only_resources: set[str] = set()
context.vp_summary: ValidationSummary | None = None
context.vp_plan_metadata: dict[str, Any] | None = None
context.vp_resource_id_map: dict[str, str] = {}
global _RESOURCE_ID_COUNTER
_RESOURCE_ID_COUNTER = 0
@given(r"an empty list of validation commands")
def step_given_empty_commands(context: Any) -> None:
context.vp_commands = []
@given(
r'a required vp command "(?P<name>[^"]+)" on resource "(?P<resource>[^"]+)"'
r" with timeout (?P<timeout>[\d.]+)"
)
def step_given_required_command_timeout(
context: Any, name: str, resource: str, timeout: str
) -> None:
rid = context.vp_resource_id_map.get(resource, _next_resource_id())
context.vp_resource_id_map[resource] = rid
context.vp_commands.append(
ValidationCommand(
validation_name=name,
resource_id=rid,
resource_name=resource,
mode=ValidationMode.REQUIRED,
arguments={},
timeout_seconds=float(timeout),
)
)
@given(
r'a required vp command "(?P<name>[^"]+)" on resource "(?P<resource>[^"]+)"'
r' having resource_id "(?P<rid>[^"]+)"'
)
def step_given_required_command_with_rid(
context: Any, name: str, resource: str, rid: str
) -> None:
context.vp_resource_id_map[resource] = rid
context.vp_commands.append(
ValidationCommand(
validation_name=name,
resource_id=rid,
resource_name=resource,
mode=ValidationMode.REQUIRED,
arguments={},
)
)
@given(r'a required vp command "(?P<name>[^"]+)" on resource "(?P<resource>[^"]+)"')
def step_given_required_command(context: Any, name: str, resource: str) -> None:
rid = context.vp_resource_id_map.get(resource, _next_resource_id())
context.vp_resource_id_map[resource] = rid
context.vp_commands.append(
ValidationCommand(
validation_name=name,
resource_id=rid,
resource_name=resource,
mode=ValidationMode.REQUIRED,
arguments={},
)
)
@given(
r'an informational vp command "(?P<name>[^"]+)"'
r' on resource "(?P<resource>[^"]+)"'
)
def step_given_informational_command(context: Any, name: str, resource: str) -> None:
rid = context.vp_resource_id_map.get(resource, _next_resource_id())
context.vp_resource_id_map[resource] = rid
context.vp_commands.append(
ValidationCommand(
validation_name=name,
resource_id=rid,
resource_name=resource,
mode=ValidationMode.INFORMATIONAL,
arguments={},
)
)
@given(r'the vp executor returns passed for "(?P<name>[^"]+)"')
def step_given_executor_passed(context: Any, name: str) -> None:
context.vp_executor.set_passed(name)
@given(
r'the vp executor returns failed for "(?P<name>[^"]+)"'
r' with message "(?P<message>[^"]+)"'
)
def step_given_executor_failed(context: Any, name: str, message: str) -> None:
context.vp_executor.set_failed(name, message)
@given(r"the vp executor returns passed for all validations")
def step_given_executor_all_passed(context: Any) -> None:
context.vp_executor.set_default_passed()
@given(r'the vp executor will timeout for "(?P<name>[^"]+)"')
def step_given_executor_timeout(context: Any, name: str) -> None:
context.vp_executor.set_timeout(name)
@given(r'the vp executor returns non-dict output for "(?P<name>[^"]+)"')
def step_given_executor_non_dict(context: Any, name: str) -> None:
context.vp_executor.set_non_dict(name)
@given(r'the vp executor returns dict without passed key for "(?P<name>[^"]+)"')
def step_given_executor_missing_passed(context: Any, name: str) -> None:
context.vp_executor.set_missing_passed(name)
@given(r'the vp executor raises an exception for "(?P<name>[^"]+)"')
def step_given_executor_exception(context: Any, name: str) -> None:
context.vp_executor.set_exception(name, RuntimeError("Test exception"))
@given(r'the vp executor returns passed with data for "(?P<name>[^"]+)"')
def step_given_executor_passed_with_data(context: Any, name: str) -> None:
context.vp_executor.set_passed_with_data(name)
@given(r'the vp resource "(?P<rid>[^"]+)" is marked as read-only')
def step_given_read_only_resource(context: Any, rid: str) -> None:
context.vp_read_only_resources.add(rid)
@given(r"a validation summary with (?P<count>\d+) required failures")
def step_given_summary_with_failures(context: Any, count: str) -> None:
cnt = int(count)
context.vp_summary = ValidationSummary(
total=cnt + 1,
required_passed=1,
required_failed=cnt,
informational_passed=0,
informational_failed=0,
results=[],
)
@given(r"the pipeline max_workers is set to (?P<workers>\d+)")
def step_given_max_workers(context: Any, workers: str) -> None:
context.vp_max_workers = int(workers)
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when(r"the validation pipeline runs for plan with metadata")
def step_when_pipeline_runs_for_plan(context: Any) -> None:
context.vp_plan_metadata = {}
pipeline = ValidationPipeline(
commands=context.vp_commands,
executor=context.vp_executor,
max_workers=context.vp_max_workers,
read_only_resources=context.vp_read_only_resources,
)
context.vp_summary = pipeline.run_for_plan(plan_metadata=context.vp_plan_metadata)
@when(r"the validation pipeline runs")
def step_when_pipeline_runs(context: Any) -> None:
pipeline = ValidationPipeline(
commands=context.vp_commands,
executor=context.vp_executor,
max_workers=context.vp_max_workers,
read_only_resources=context.vp_read_only_resources,
)
context.vp_summary = pipeline.run()
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then(r"the validation summary total is (?P<count>\d+)")
def step_then_summary_total(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None, "No summary available"
assert context.vp_summary.total == cnt, (
f"Expected total={cnt}, got {context.vp_summary.total}"
)
@then(r"the validation summary required_passed is (?P<count>\d+)")
def step_then_summary_required_passed(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
assert context.vp_summary.required_passed == cnt, (
f"Expected required_passed={cnt}, got {context.vp_summary.required_passed}"
)
@then(r"the validation summary required_failed is (?P<count>\d+)")
def step_then_summary_required_failed(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
assert context.vp_summary.required_failed == cnt, (
f"Expected required_failed={cnt}, got {context.vp_summary.required_failed}"
)
@then(r"the validation summary informational_passed is (?P<count>\d+)")
def step_then_summary_informational_passed(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
assert context.vp_summary.informational_passed == cnt, (
f"Expected informational_passed={cnt}, "
f"got {context.vp_summary.informational_passed}"
)
@then(r"the validation summary informational_failed is (?P<count>\d+)")
def step_then_summary_informational_failed(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
assert context.vp_summary.informational_failed == cnt, (
f"Expected informational_failed={cnt}, "
f"got {context.vp_summary.informational_failed}"
)
@then(r"all required validations passed")
def step_then_all_required_passed(context: Any) -> None:
assert context.vp_summary is not None
assert context.vp_summary.all_required_passed, (
f"Expected all_required_passed=True, got False "
f"(required_failed={context.vp_summary.required_failed})"
)
@then(r"not all required validations passed")
def step_then_not_all_required_passed(context: Any) -> None:
assert context.vp_summary is not None
assert not context.vp_summary.all_required_passed, (
"Expected all_required_passed=False, got True"
)
@then(r"the validation results are sorted by resource then mode then name")
def step_then_results_sorted(context: Any) -> None:
assert context.vp_summary is not None
results = context.vp_summary.results
keys = [(r.resource_name, r.mode.value, r.validation_name) for r in results]
assert keys == sorted(keys), f"Results not sorted: {keys}"
@then(r'the vp result for "(?P<name>[^"]+)" has timed_out true')
def step_then_result_timed_out(context: Any, name: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert result.timed_out, f"Expected timed_out=True for '{name}'"
@then(r'the vp result for "(?P<name>[^"]+)" has passed false')
def step_then_result_passed_false(context: Any, name: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert not result.passed, f"Expected passed=False for '{name}'"
@then(r'the vp result for "(?P<name>[^"]+)" has passed true')
def step_then_result_passed_true(context: Any, name: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert result.passed, f"Expected passed=True for '{name}'"
@then(r'the vp result for "(?P<name>[^"]+)" has error containing "(?P<text>[^"]+)"')
def step_then_result_error_contains(context: Any, name: str, text: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert result.error is not None, f"Expected error for '{name}', got None"
assert text in result.error, (
f"Expected error containing '{text}', got: {result.error}"
)
@then(r'the vp result for "(?P<name>[^"]+)" has message containing "(?P<text>[^"]+)"')
def step_then_result_message_contains(context: Any, name: str, text: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert text in result.message, (
f"Expected message containing '{text}', got: {result.message}"
)
@then(r'the vp result for "(?P<name>[^"]+)" has data key "(?P<key>[^"]+)"')
def step_then_result_has_data_key(context: Any, name: str, key: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert result.data is not None, f"Expected data for '{name}', got None"
assert key in result.data, (
f"Expected data key '{key}' in {list(result.data.keys())}"
)
@then(r"the results grouped by resource have (?P<count>\d+) groups")
def step_then_groups_count(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
groups = ValidationPipeline.group_by_resource(context.vp_summary.results)
assert len(groups) == cnt, f"Expected {cnt} groups, got {len(groups)}"
@then(r'the vp resource group "(?P<resource>[^"]+)" has (?P<count>\d+) result')
def step_then_group_result_count(context: Any, resource: str, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
groups = ValidationPipeline.group_by_resource(context.vp_summary.results)
assert resource in groups, f"Resource '{resource}' not in groups"
actual = len(groups[resource])
assert actual == cnt, f"Expected {cnt} results for '{resource}', got {actual}"
@then(r"the summary all_required_passed property is true")
def step_then_summary_property_true(context: Any) -> None:
assert context.vp_summary is not None
assert context.vp_summary.all_required_passed
@then(r"the summary all_required_passed property is false")
def step_then_summary_property_false(context: Any) -> None:
assert context.vp_summary is not None
assert not context.vp_summary.all_required_passed
@then(r"the plan metadata contains validation_summary")
def step_then_plan_metadata_has_summary(context: Any) -> None:
assert context.vp_plan_metadata is not None
assert "validation_summary" in context.vp_plan_metadata
@then(r"the plan metadata validation_summary total is (?P<count>\d+)")
def step_then_plan_metadata_total(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_plan_metadata is not None
vs = context.vp_plan_metadata["validation_summary"]
assert vs["total"] == cnt, f"Expected total={cnt}, got {vs['total']}"
@then(r'the vp result for "(?P<name>[^"]+)" has positive duration_ms')
def step_then_result_positive_duration(context: Any, name: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert result.duration_ms > 0, (
f"Expected duration_ms > 0 for '{name}', got {result.duration_ms}"
)
# Reset to default parse matcher
use_step_matcher("parse")
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _find_result(summary: ValidationSummary, name: str) -> ValidationResult:
"""Find a validation result by name."""
for r in summary.results:
if r.validation_name == name:
return r
available = [r.validation_name for r in summary.results]
msg = f"No result for '{name}', available: {available}"
raise AssertionError(msg)