"""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}"