57fcc880c3
ISSUES CLOSED: #176
384 lines
13 KiB
Python
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}"
|