"""Step definitions for Plan Generation Pre-flight Guardrails scenarios. All step patterns are prefixed with ``pfg-`` to avoid AmbiguousStep collisions with other feature step definitions. """ from __future__ import annotations import tempfile from behave import given, then, when from behave.runner import Context from cleveragents.actor.schema import ActorConfigSchema from cleveragents.application.services.plan_preflight_guardrail import ( PlanPreflightGuardrail, PreflightRejection, ) # -- Givens ---------------------------------------------------------------- @given("a plan preflight guardrail") def step_pfg_given_guardrail(context: Context) -> None: context.guardrail = PlanPreflightGuardrail() # Reset state between scenarios context.pfg_action_registry = {} context.pfg_actor_registry = {} context.pfg_tool_registry = {} context.pfg_skill_registry = {} context.pfg_automation_profile = None context.pfg_require_checkpoints = False context.pfg_tool_capabilities = {} context.pfg_resource_paths = () context.pfg_validation_registry = {} context.pfg_check_result = None context.pfg_report = None context.pfg_rejection = None @given('an action registry containing "{name}"') def step_pfg_action_registry_with(context: Context, name: str) -> None: context.pfg_action_registry[name] = {"name": name} @given("an empty action registry") def step_pfg_empty_action_registry(context: Context) -> None: context.pfg_action_registry = {} @given("an actor registry with all 4 roles registered") def step_pfg_actor_all_roles(context: Context) -> None: for role in PlanPreflightGuardrail.ACTOR_ROLES: context.pfg_actor_registry[role] = {"role": role} @given('an actor registry missing "{role}" role') def step_pfg_actor_missing_role(context: Context, role: str) -> None: for r in PlanPreflightGuardrail.ACTOR_ROLES: if r != role: context.pfg_actor_registry[r] = {"role": r} @given('a tool registry containing "{t1}" and "{t2}"') def step_pfg_tool_registry_two(context: Context, t1: str, t2: str) -> None: context.pfg_tool_registry[t1] = {"name": t1} context.pfg_tool_registry[t2] = {"name": t2} @given('a tool registry containing "{name}"') def step_pfg_tool_registry_one(context: Context, name: str) -> None: context.pfg_tool_registry[name] = {"name": name} @given("a valid automation profile") def step_pfg_valid_automation(context: Context) -> None: context.pfg_automation_profile = {"name": "auto"} @given("no automation profile") def step_pfg_no_automation(context: Context) -> None: context.pfg_automation_profile = None @given("require_checkpoints is true") def step_pfg_require_checkpoints_true(context: Context) -> None: context.pfg_require_checkpoints = True @given("require_checkpoints is false") def step_pfg_require_checkpoints_false(context: Context) -> None: context.pfg_require_checkpoints = False @given("tool capabilities showing all tools checkpointable") def step_pfg_all_checkpointable(context: Context) -> None: context.pfg_tool_capabilities = {"tool-a": True, "tool-b": True} @given('tool capabilities with "{name}" not checkpointable') def step_pfg_non_checkpointable(context: Context, name: str) -> None: context.pfg_tool_capabilities = {"safe-tool": True, name: False} @given("resource paths that exist on filesystem") def step_pfg_existing_resources(context: Context) -> None: # Use a temp file to guarantee it exists with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as tmp: tmp_path = tmp.name context.pfg_resource_paths = (tmp_path,) context._pfg_tmp_file = tmp_path @given('resource path "{path}"') def step_pfg_resource_path(context: Context, path: str) -> None: context.pfg_resource_paths = (path,) @given('a validation registry containing "{v1}" and "{v2}"') def step_pfg_validation_registry_two(context: Context, v1: str, v2: str) -> None: context.pfg_validation_registry[v1] = {"name": v1} context.pfg_validation_registry[v2] = {"name": v2} @given('a validation registry containing "{name}"') def step_pfg_validation_registry_one(context: Context, name: str) -> None: context.pfg_validation_registry[name] = {"name": name} @given("a plan preflight guardrail with all registries populated") def step_pfg_all_populated(context: Context) -> None: context.guardrail = PlanPreflightGuardrail() context.pfg_action_registry = {"test-action": {"name": "test-action"}} context.pfg_actor_registry = { role: {"role": role} for role in PlanPreflightGuardrail.ACTOR_ROLES } context.pfg_tool_registry = {"tool-a": {"name": "tool-a"}} context.pfg_skill_registry = {} context.pfg_automation_profile = {"name": "auto"} context.pfg_require_checkpoints = False context.pfg_tool_capabilities = {} context.pfg_resource_paths = () context.pfg_validation_registry = {} context.pfg_rejection = None context.pfg_report = None @given("an estimation actor registry entry without response_format") def step_pfg_estimation_actor_missing_response_format(context: Context) -> None: context.pfg_actor_registry["estimation"] = { "name": "local/estimator", "context_view": "strategist", } @given("an estimation actor registry entry with non-strategist context_view") def step_pfg_estimation_actor_wrong_context_view(context: Context) -> None: context.pfg_actor_registry["estimation"] = { "name": "local/estimator", "context_view": "executor", "response_format": {"type": "object"}, } @given( "an estimation actor registry entry as ActorConfigSchema without response_format" ) def step_pfg_estimation_actor_model_missing_response_format(context: Context) -> None: context.pfg_actor_registry["estimation"] = ActorConfigSchema( name="local/estimator-model", type="llm", description="Model-based estimation actor", model="gpt-4", role_hint="estimation", context_view="strategist", ) @given("a plan preflight guardrail with missing action") def step_pfg_missing_action(context: Context) -> None: context.guardrail = PlanPreflightGuardrail() context.pfg_action_registry = {} context.pfg_actor_registry = { role: {"role": role} for role in PlanPreflightGuardrail.ACTOR_ROLES } context.pfg_tool_registry = {} context.pfg_skill_registry = {} context.pfg_automation_profile = {"name": "auto"} context.pfg_require_checkpoints = False context.pfg_tool_capabilities = {} context.pfg_resource_paths = () context.pfg_validation_registry = {} context.pfg_rejection = None context.pfg_report = None # -- Whens ---------------------------------------------------------------- @when('I check action schema for "{name}"') def step_pfg_check_action(context: Context, name: str) -> None: context.pfg_check_result = context.guardrail.check_action_schema( name, context.pfg_action_registry ) @when("I check actor availability") def step_pfg_check_actors(context: Context) -> None: context.pfg_check_result = context.guardrail.check_actor_availability( context.pfg_actor_registry ) @when('I check skill/tool existence for tools "{tools_csv}"') def step_pfg_check_tools(context: Context, tools_csv: str) -> None: tool_names = tuple(t.strip() for t in tools_csv.split(",")) context.pfg_check_result = context.guardrail.check_skill_tool_existence( tool_names, context.pfg_tool_registry, (), context.pfg_skill_registry, ) @when("I check automation policy") def step_pfg_check_automation(context: Context) -> None: context.pfg_check_result = context.guardrail.check_automation_policy( context.pfg_automation_profile ) @when("I check rollback feasibility") def step_pfg_check_rollback(context: Context) -> None: context.pfg_check_result = context.guardrail.check_rollback_feasibility( context.pfg_require_checkpoints, context.pfg_tool_capabilities, ) @when("I check resource accessibility") def step_pfg_check_resources(context: Context) -> None: context.pfg_check_result = context.guardrail.check_resource_accessibility( context.pfg_resource_paths ) @when('I check validation resolution for "{names_csv}"') def step_pfg_check_validation(context: Context, names_csv: str) -> None: validation_names = tuple(v.strip() for v in names_csv.split(",")) context.pfg_check_result = context.guardrail.check_validation_resolution( validation_names, context.pfg_validation_registry, ) @when("I run all preflight checks") def step_pfg_run_all(context: Context) -> None: try: context.pfg_report = context.guardrail.run_all_checks( action_name="test-action", action_registry=context.pfg_action_registry, actor_registry=context.pfg_actor_registry, tool_names=tuple(context.pfg_tool_registry.keys()), tool_registry=context.pfg_tool_registry, skill_names=tuple(context.pfg_skill_registry.keys()), skill_registry=context.pfg_skill_registry, automation_profile=context.pfg_automation_profile, require_checkpoints=context.pfg_require_checkpoints, tool_capabilities=context.pfg_tool_capabilities, resource_paths=context.pfg_resource_paths, validation_names=(), validation_registry=context.pfg_validation_registry, ) context.pfg_rejection = None except PreflightRejection as exc: context.pfg_rejection = exc context.pfg_report = None @when("I run all preflight checks expecting rejection") def step_pfg_run_all_expecting_rejection(context: Context) -> None: try: context.pfg_report = context.guardrail.run_all_checks( action_name="missing-action", action_registry=context.pfg_action_registry, actor_registry=context.pfg_actor_registry, automation_profile=context.pfg_automation_profile, ) context.pfg_rejection = None except PreflightRejection as exc: context.pfg_rejection = exc context.pfg_report = None # -- Thens ---------------------------------------------------------------- @then('the check should pass with message containing "{text}"') def step_pfg_then_pass_with_msg(context: Context, text: str) -> None: result = context.pfg_check_result assert result is not None, "No check result recorded" assert result.passed is True, f"Expected PASS but got FAIL: {result.message}" assert text in result.message, ( f"Expected '{text}' in message, got: {result.message}" ) @then("the check should pass") def step_pfg_then_pass(context: Context) -> None: result = context.pfg_check_result assert result is not None, "No check result recorded" assert result.passed is True, f"Expected PASS but got FAIL: {result.message}" @then('the check should fail with message containing "{text}"') def step_pfg_then_fail_with_msg(context: Context, text: str) -> None: result = context.pfg_check_result assert result is not None, "No check result recorded" assert result.passed is False, f"Expected FAIL but got PASS: {result.message}" assert text in result.message, ( f"Expected '{text}' in message, got: {result.message}" ) @then("the report should show all checks passed") def step_pfg_then_report_all_passed(context: Context) -> None: report = context.pfg_report assert report is not None, "No report recorded" assert report.all_passed, ( f"Expected all checks passed, failures: {[r.message for r in report.failures]}" ) @then("no PreflightRejection should be raised") def step_pfg_then_no_rejection(context: Context) -> None: assert context.pfg_rejection is None, ( f"Expected no rejection but got: {context.pfg_rejection}" ) @then("a PreflightRejection should be raised") def step_pfg_then_rejection(context: Context) -> None: assert context.pfg_rejection is not None, ( "Expected PreflightRejection but none was raised" ) @then('the rejection should identify "{check_name}"') def step_pfg_then_rejection_check(context: Context, check_name: str) -> None: rejection = context.pfg_rejection assert rejection is not None, "No rejection recorded" assert rejection.check.value == check_name, ( f"Expected check '{check_name}', got '{rejection.check.value}'" ) @then('the preflight warnings should contain "{text}"') def step_pfg_then_warnings_contain(context: Context, text: str) -> None: report = context.pfg_report assert report is not None, "No report recorded" assert any(text in warning.message for warning in report.warnings), ( f"Expected warning containing '{text}', got {report.warnings}" )