"""Step definitions for Automation Profile CLI guards coverage boost. Targets uncovered lines 91-97 (_guards_dict non-None branch) and lines 194-204 (_print_profile rich guards display block). """ from __future__ import annotations import os import tempfile from behave import given, then, when from behave.runner import Context from typer.testing import CliRunner from cleveragents.cli.commands.automation_profile import ( _guards_dict, _profile_spec_dict, ) from cleveragents.cli.commands.automation_profile import ( app as profile_app, ) from cleveragents.domain.models.core.automation_guard import AutomationGuard from cleveragents.domain.models.core.automation_profile import AutomationProfile def _create_in_memory_profile_service(): """Create an AutomationProfileService backed by an in-memory SQLite DB.""" from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from cleveragents.application.services.automation_profile_service import ( AutomationProfileService, ) from cleveragents.infrastructure.database.models import Base from cleveragents.infrastructure.database.repositories import ( AutomationProfileRepository, ) engine = create_engine("sqlite:///:memory:", echo=False) Base.metadata.create_all(engine) factory = sessionmaker(bind=engine, expire_on_commit=False) repo = AutomationProfileRepository(session_factory=factory, auto_commit=True) return AutomationProfileService(repo=repo) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_guarded_profile( name: str = "acme/guarded", description: str = "Profile with guards", max_tool_calls: int = 5, max_total_cost: float = 100.0, tool_allowlist: list[str] | None = None, tool_denylist: list[str] | None = None, require_approval_for_writes: bool = True, require_approval_for_apply: bool = False, ) -> AutomationProfile: """Create an AutomationProfile with guards attached.""" guards = AutomationGuard( max_tool_calls_per_step=max_tool_calls, max_total_cost=max_total_cost, tool_allowlist=tool_allowlist, tool_denylist=tool_denylist, require_approval_for_writes=require_approval_for_writes, require_approval_for_apply=require_approval_for_apply, ) return AutomationProfile( name=name, description=description, schema_version="1.0", decompose_task=0.5, create_tool=0.4, select_tool=0.3, guards=guards, ) def _write_temp_yaml(context: Context, content: str) -> str: """Write content to a temporary YAML file, tracked for cleanup.""" fd, path = tempfile.mkstemp(suffix=".yaml") with os.fdopen(fd, "w") as fh: fh.write(content) if not hasattr(context, "_temp_files"): context._temp_files = [] context._temp_files.append(path) return path # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("a fresh automation profile CLI runner for guards coverage") def step_fresh_guards_runner(context: Context) -> None: from unittest.mock import patch context.runner = CliRunner() context.result = None context.guards_dict_result = None context.profile_spec_result = None context.guard_obj = None context.guarded_profile = None # Create an in-memory service and patch _get_service for this scenario context._ap_service = _create_in_memory_profile_service() context._ap_patcher = patch( "cleveragents.cli.commands.automation_profile._get_service", return_value=context._ap_service, ) context._ap_patcher.start() if not hasattr(context, "_cleanup_handlers"): context._cleanup_handlers = [] context._cleanup_handlers.append(context._ap_patcher.stop) # --------------------------------------------------------------------------- # _guards_dict direct tests (lines 91-97) # --------------------------------------------------------------------------- @given( "an AutomationGuard with max_tool_calls_per_step {calls:d} and max_total_cost {cost:g}" ) def step_create_guard_with_calls_and_cost( context: Context, calls: int, cost: float, ) -> None: context.guard_obj = AutomationGuard( max_tool_calls_per_step=calls, max_total_cost=cost, tool_allowlist=None, tool_denylist=None, require_approval_for_writes=False, require_approval_for_apply=False, ) @given( 'an AutomationGuard with tool_allowlist "{allowlist}" and tool_denylist "{denylist}"' ) def step_create_guard_with_lists( context: Context, allowlist: str, denylist: str, ) -> None: context.guard_obj = AutomationGuard( max_tool_calls_per_step=None, max_total_cost=None, tool_allowlist=allowlist.split(","), tool_denylist=denylist.split(","), require_approval_for_writes=False, require_approval_for_apply=False, ) @when("I call _guards_dict with the guard object") def step_call_guards_dict(context: Context) -> None: context.guards_dict_result = _guards_dict(context.guard_obj) @when("I call _guards_dict with None") def step_call_guards_dict_none(context: Context) -> None: context.guards_dict_result = _guards_dict(None) @then('the guards dict should contain key "{key}" with value {value:d}') def step_guards_dict_int_value(context: Context, key: str, value: int) -> None: result = context.guards_dict_result assert result is not None, "Expected non-None result from _guards_dict" assert key in result, f"Key '{key}' not found in guards dict: {result}" assert result[key] == value, f"Expected {key}={value}, got {result[key]}" @then('the guards dict should contain key "{key}" with value {value:g}') def step_guards_dict_float_value(context: Context, key: str, value: float) -> None: result = context.guards_dict_result assert result is not None, "Expected non-None result from _guards_dict" assert key in result, f"Key '{key}' not found in guards dict: {result}" assert result[key] == value, f"Expected {key}={value}, got {result[key]}" @then('the guards dict should contain key "{key}"') def step_guards_dict_has_key(context: Context, key: str) -> None: result = context.guards_dict_result assert result is not None, "Expected non-None result from _guards_dict" assert key in result, f"Key '{key}' not found in guards dict: {result}" @then("the guards dict result should be None") def step_guards_dict_is_none(context: Context) -> None: assert context.guards_dict_result is None, ( f"Expected None, got {context.guards_dict_result}" ) @then('the guards dict tool_allowlist should be "{expected}"') def step_guards_dict_allowlist(context: Context, expected: str) -> None: result = context.guards_dict_result assert result is not None expected_list = expected.split(",") assert result["tool_allowlist"] == expected_list, ( f"Expected {expected_list}, got {result['tool_allowlist']}" ) @then('the guards dict tool_denylist should be "{expected}"') def step_guards_dict_denylist(context: Context, expected: str) -> None: result = context.guards_dict_result assert result is not None expected_list = expected.split(",") assert result["tool_denylist"] == expected_list, ( f"Expected {expected_list}, got {result['tool_denylist']}" ) # --------------------------------------------------------------------------- # _profile_spec_dict with guards (lines 91-97 indirect) # --------------------------------------------------------------------------- @given("an AutomationProfile with guards having require_approval_for_writes true") def step_create_profile_with_guards(context: Context) -> None: context.guarded_profile = _make_guarded_profile( name="acme/spectest", require_approval_for_writes=True, ) @when("I call _profile_spec_dict with the guarded profile") def step_call_profile_spec_dict(context: Context) -> None: context.profile_spec_result = _profile_spec_dict(context.guarded_profile) @then("the profile spec dict guards should not be None") def step_spec_dict_guards_not_none(context: Context) -> None: result = context.profile_spec_result assert result is not None assert result.get("guards") is not None, ( f"Expected guards to be non-None, got: {result.get('guards')}" ) @then('the profile spec dict guards should have "{key}" as true') def step_spec_dict_guards_key_true(context: Context, key: str) -> None: result = context.profile_spec_result assert result is not None guards = result["guards"] assert guards is not None assert guards[key] is True, f"Expected {key}=True, got {guards[key]}" # --------------------------------------------------------------------------- # CLI show with guards - rich format (lines 194-204) # --------------------------------------------------------------------------- @given('a custom guarded profile "{name}" is stored in the repo') def step_store_guarded_profile(context: Context, name: str) -> None: profile = _make_guarded_profile( name=name, max_tool_calls=5, max_total_cost=100.0, tool_allowlist=["read_file", "search"], tool_denylist=["rm_rf"], require_approval_for_writes=True, require_approval_for_apply=True, ) context._ap_service._repo.upsert(profile) @when('I run automation-profile show "{name}" in rich format') def step_show_guarded_rich(context: Context, name: str) -> None: context.result = context.runner.invoke(profile_app, ["show", name]) @when('I run automation-profile show "{name}" with format "{fmt}"') def step_show_guarded_fmt(context: Context, name: str, fmt: str) -> None: context.result = context.runner.invoke( profile_app, ["show", name, "--format", fmt], ) @then("the guards coverage show command should succeed") def step_guards_coverage_show_succeed(context: Context) -> None: assert context.result is not None assert context.result.exit_code == 0, ( f"Expected exit 0, got {context.result.exit_code}. " f"Output: {context.result.output}" ) @then('the guards coverage output should contain "{text}"') def step_guards_coverage_output_contains(context: Context, text: str) -> None: assert context.result is not None assert text in context.result.output, ( f"Expected '{text}' in output. Got:\n{context.result.output}" ) # --------------------------------------------------------------------------- # CLI add with guards config (lines 194-204 via add path) # --------------------------------------------------------------------------- @given('a YAML config file for a guarded profile "{name}"') def step_guarded_yaml_config(context: Context, name: str) -> None: yaml_content = f"""\ name: {name} description: Guarded profile for testing schema_version: "1.0" decompose_task: 0.5 create_tool: 0.4 select_tool: 0.3 guards: max_tool_calls_per_step: 8 max_total_cost: 200.0 tool_allowlist: - read_file - search tool_denylist: - rm_rf require_approval_for_writes: true require_approval_for_apply: false """ context.guarded_yaml_path = _write_temp_yaml(context, yaml_content) @when("I run automation-profile add with the guarded config file") def step_add_guarded_config(context: Context) -> None: context.result = context.runner.invoke( profile_app, ["add", "--config", context.guarded_yaml_path], ) @then("the guards coverage add command should succeed") def step_guards_coverage_add_succeed(context: Context) -> None: assert context.result is not None assert context.result.exit_code == 0, ( f"Expected exit 0, got {context.result.exit_code}. " f"Output: {context.result.output}" ) # --------------------------------------------------------------------------- # CLI remove guarded profile with --format json (lines 91-97 via remove) # --------------------------------------------------------------------------- @when('I run automation-profile remove "{name}" with --yes and format "{fmt}"') def step_remove_guarded_fmt(context: Context, name: str, fmt: str) -> None: context.result = context.runner.invoke( profile_app, ["remove", name, "--yes", "--format", fmt], ) @then("the guards coverage remove command should succeed") def step_guards_coverage_remove_succeed(context: Context) -> None: assert context.result is not None assert context.result.exit_code == 0, ( f"Expected exit 0, got {context.result.exit_code}. " f"Output: {context.result.output}" )