Files
cleveragents-core/features/steps/validation_apply_steps.py
T
2026-02-25 10:03:21 +00:00

384 lines
13 KiB
Python

"""Step definitions for validation apply gate tests."""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.validation_apply import (
ApplyValidationGate,
ApplyValidationResult,
ApplyValidationSummary,
AttachmentScope,
DefaultValidationRunner,
ValidationAttachment,
)
from cleveragents.domain.models.core.tool import ValidationMode
__all__: list[str] = []
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a validation apply test environment")
def step_given_val_apply_env(context: Context) -> None:
context.val_attachments = [] # list[ValidationAttachment]
context.val_run_context = {} # dict[str, str]
context.val_result = None # ApplyValidationResult | None
context.val_summary = None # ApplyValidationSummary | None
context.val_gate_summary = None # ApplyValidationSummary | None
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given('a validation attachment "{name}" for resource "{res}" with mode "{mode}"')
def step_given_attachment(context: Context, name: str, res: str, mode: str) -> None:
idx = len(context.val_attachments)
att = ValidationAttachment(
attachment_id=f"01ATT{idx:021d}",
validation_name=name,
resource_id=res,
mode=ValidationMode(mode),
)
context.val_attachments.append(att)
@given('a project-scoped attachment "{name}" for resource "{res}" targeting "{target}"')
def step_given_project_attachment(
context: Context, name: str, res: str, target: str
) -> None:
idx = len(context.val_attachments)
att = ValidationAttachment(
attachment_id=f"01ATT{idx:021d}",
validation_name=name,
resource_id=res,
mode=ValidationMode.REQUIRED,
scope=AttachmentScope.PROJECT,
scope_target=target,
)
context.val_attachments.append(att)
@given('a plan-scoped attachment "{name}" for resource "{res}" targeting "{target}"')
def step_given_plan_attachment(
context: Context, name: str, res: str, target: str
) -> None:
idx = len(context.val_attachments)
att = ValidationAttachment(
attachment_id=f"01ATT{idx:021d}",
validation_name=name,
resource_id=res,
mode=ValidationMode.REQUIRED,
scope=AttachmentScope.PLAN,
scope_target=target,
)
context.val_attachments.append(att)
@given('a validation run context with key "{key}" and value "{value}"')
def step_given_val_context(context: Context, key: str, value: str) -> None:
context.val_run_context[key] = value
@given('the attachment has argument "{key}" with value "{value}"')
def step_given_attachment_arg(context: Context, key: str, value: str) -> None:
context.val_attachments[-1].arguments[key] = value
@given("an apply validation summary with {p} required passed and {f} required failed")
def step_given_apply_summary(context: Context, p: str, f: str) -> None:
results: list[ApplyValidationResult] = []
for i in range(int(p)):
results.append(
ApplyValidationResult(
attachment_id=f"01ATT{i:021d}",
validation_name=f"pass-val-{i}",
resource_id=f"res-{i}",
mode=ValidationMode.REQUIRED,
passed=True,
message="ok",
)
)
for i in range(int(f)):
results.append(
ApplyValidationResult(
attachment_id=f"01ATT{int(p) + i:021d}",
validation_name=f"fail-val-{i}",
resource_id=f"res-{int(p) + i}",
mode=ValidationMode.REQUIRED,
passed=False,
message="failed check",
)
)
context.val_summary = ApplyValidationSummary(
plan_id="01VALTEST0000000000001",
results=results,
)
@given("the summary has {ip} informational passed and {iff} informational failed")
def step_given_informational_results(context: Context, ip: str, iff: str) -> None:
base = len(context.val_summary.results)
for i in range(int(ip)):
context.val_summary.results.append(
ApplyValidationResult(
attachment_id=f"01ATT{base + i:021d}",
validation_name=f"info-pass-{i}",
resource_id=f"res-info-{i}",
mode=ValidationMode.INFORMATIONAL,
passed=True,
message="info ok",
)
)
base2 = len(context.val_summary.results)
for i in range(int(iff)):
context.val_summary.results.append(
ApplyValidationResult(
attachment_id=f"01ATT{base2 + i:021d}",
validation_name=f"info-fail-{i}",
resource_id=f"res-info-fail-{i}",
mode=ValidationMode.INFORMATIONAL,
passed=False,
message="info failed",
)
)
@given("a validation run context that raises an error during iteration")
def step_given_error_context(context: Context) -> None:
"""Set up a context dict-like object whose items() raises."""
class _BadContext(dict): # type: ignore[type-arg]
def items(self) -> None: # type: ignore[override]
msg = "boom"
raise RuntimeError(msg)
context.val_run_context = _BadContext()
@given("an apply validation summary with a bare failure result")
def step_given_summary_bare_failure(context: Context) -> None:
context.val_summary = ApplyValidationSummary(
plan_id="01VALTEST0000000000001",
results=[
ApplyValidationResult(
attachment_id="01ATT000000000000000000000",
validation_name="bare-val",
resource_id="res-bare",
mode=ValidationMode.REQUIRED,
passed=False,
message="",
error=None,
),
],
)
context.val_gate_summary = context.val_summary
@given("an apply validation summary with an error result")
def step_given_summary_with_error(context: Context) -> None:
context.val_summary = ApplyValidationSummary(
plan_id="01VALTEST0000000000001",
results=[
ApplyValidationResult(
attachment_id="01ATT000000000000000000000",
validation_name="broken-val",
resource_id="res-broken",
mode=ValidationMode.REQUIRED,
passed=False,
message="",
error="execution timed out",
),
],
)
context.val_gate_summary = context.val_summary
@given("an empty apply validation summary")
def step_given_empty_apply_summary(context: Context) -> None:
context.val_summary = ApplyValidationSummary(
plan_id="01VALTEST0000000000001",
results=[],
)
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when("I execute the validation with DefaultValidationRunner")
def step_when_execute_default_runner(context: Context) -> None:
runner = DefaultValidationRunner()
context.val_result = runner.run_validation(
context.val_attachments[-1],
context.val_run_context,
)
@when('I run the apply validation gate for plan "{plan_id}"')
def step_when_run_gate(context: Context, plan_id: str) -> None:
runner = DefaultValidationRunner()
gate = ApplyValidationGate(runner=runner)
context.val_gate_summary = gate.run(
plan_id=plan_id,
attachments=context.val_attachments,
context=context.val_run_context,
)
# Also store as val_summary for summary assertions
context.val_summary = context.val_gate_summary
@when('I run the apply validation gate for plan "{plan_id}" with no attachments')
def step_when_run_gate_empty(context: Context, plan_id: str) -> None:
runner = DefaultValidationRunner()
gate = ApplyValidationGate(runner=runner)
context.val_gate_summary = gate.run(
plan_id=plan_id,
attachments=[],
context=context.val_run_context,
)
context.val_summary = context.val_gate_summary
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then('the attachment mode should be "{mode}"')
def step_then_attachment_mode(context: Context, mode: str) -> None:
att = context.val_attachments[-1]
assert att.mode == ValidationMode(mode), (
f"Expected mode '{mode}', got '{att.mode.value}'"
)
@then('the attachment scope should be "{scope}"')
def step_then_attachment_scope(context: Context, scope: str) -> None:
att = context.val_attachments[-1]
assert att.scope == AttachmentScope(scope), (
f"Expected scope '{scope}', got '{att.scope.value}'"
)
@then('the attachment scope target should be "{target}"')
def step_then_scope_target(context: Context, target: str) -> None:
att = context.val_attachments[-1]
assert att.scope_target == target
@then("the validation result should be passed")
def step_then_val_result_passed(context: Context) -> None:
assert context.val_result is not None
assert context.val_result.passed, (
f"Expected passed, got failed: {context.val_result.message}"
)
@then("the validation result should be failed")
def step_then_val_result_failed(context: Context) -> None:
assert context.val_result is not None
assert not context.val_result.passed
@then("the validation result duration_ms should be >= {n}")
def step_then_val_result_duration(context: Context, n: str) -> None:
assert context.val_result is not None
assert context.val_result.duration_ms >= int(n), (
f"Expected duration_ms >= {n}, got {context.val_result.duration_ms}"
)
@then("the validation result should have an error message")
def step_then_val_result_has_error(context: Context) -> None:
assert context.val_result is not None
assert context.val_result.error is not None and context.val_result.error != "", (
f"Expected error message, got: {context.val_result.error!r}"
)
@then("the apply summary should report all required passed")
def step_then_all_required_passed(context: Context) -> None:
assert context.val_summary is not None
assert context.val_summary.all_required_passed
@then("the apply summary should not report all required passed")
def step_then_not_all_required_passed(context: Context) -> None:
assert context.val_summary is not None
assert not context.val_summary.all_required_passed
@then("the apply summary total should be {n}")
def step_then_apply_summary_total(context: Context, n: str) -> None:
assert context.val_summary is not None
assert context.val_summary.total == int(n), (
f"Expected total {n}, got {context.val_summary.total}"
)
@then("the apply summary required failed count should be {n}")
def step_then_required_failed(context: Context, n: str) -> None:
assert context.val_summary is not None
assert context.val_summary.required_failed == int(n)
@then("the apply summary informational failed count should be {n}")
def step_then_informational_failed(context: Context, n: str) -> None:
assert context.val_summary is not None
assert context.val_summary.informational_failed == int(n)
@then("the apply summary should be empty")
def step_then_apply_summary_empty(context: Context) -> None:
assert context.val_summary is not None
assert context.val_summary.is_empty
@then('the plan metadata should have key "{key}"')
def step_then_plan_metadata_key(context: Context, key: str) -> None:
assert context.val_summary is not None
md = context.val_summary.to_plan_metadata()
assert key in md, f"Key '{key}' not in {list(md.keys())}"
@then('the validation gate CLI output should contain "{text}"')
def step_then_val_gate_cli_output(context: Context, text: str) -> None:
assert context.val_summary is not None
output = context.val_summary.format_cli_output()
assert text in output, f"Expected '{text}' in:\n{output}"
@then("the gate should allow apply")
def step_then_gate_allows(context: Context) -> None:
assert context.val_gate_summary is not None
gate = ApplyValidationGate(runner=DefaultValidationRunner())
assert not gate.should_block_apply(context.val_gate_summary), (
"Expected gate to allow apply but it blocked"
)
@then("the gate should block apply")
def step_then_gate_blocks(context: Context) -> None:
assert context.val_gate_summary is not None
gate = ApplyValidationGate(runner=DefaultValidationRunner())
assert gate.should_block_apply(context.val_gate_summary), (
"Expected gate to block apply but it allowed"
)
@then('the gate failure reasons should mention "{text}"')
def step_then_failure_reasons(context: Context, text: str) -> None:
assert context.val_gate_summary is not None
gate = ApplyValidationGate(runner=DefaultValidationRunner())
reasons = gate.get_failure_reasons(context.val_gate_summary)
combined = " ".join(reasons)
assert text in combined, f"Expected '{text}' in failure reasons: {reasons}"