Files
temp/features/steps/container_and_repository_coverage_steps.py
freemo 051ee7c290 test(coverage): add Behave BDD tests to improve coverage across 52 source files
Added 52 new .feature files and corresponding _steps.py files targeting
previously uncovered code paths in the following areas:

- TUI layer: app, commands, persona (state/schema/registry), widgets,
  input (shell_exec, reference_parser)
- Application services: plan lifecycle/service/executor, session,
  project, repo indexing, correction, checkpoint, actor, llm_actors,
  strategy coordinator, resource file watcher, service retry wiring
- CLI commands: session, resource, repl, plan, db, automation_profile
- Domain models: retry_policy, resource_type, cost_budget,
  docker_compose_analyzer, detail_level, _sql_string_aware,
  _postgresql_helpers
- Core: circuit_breaker, retry_service_patterns
- Infrastructure: repositories, transaction_sandbox, strategy_registry,
  plugins/loader, container
- Config: settings
- Agents: plan_generation, context_analysis, auto_debug
- A2A: facade

All new tests follow the Behave/Gherkin BDD standard. Resolved step
definition collisions with unique prefixes. Fixed Alembic fileConfig
logger disabling issue (disable_existing_loggers=False).

ISSUES CLOSED: #1068
2026-03-20 21:22:10 +00:00

946 lines
33 KiB
Python

"""Step definitions for comprehensive infrastructure and application coverage tests."""
import builtins
import os
import sys
from datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock, patch
from behave import given, then, when
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import sessionmaker
# Import modules to test
from cleveragents.application.container import (
Container,
get_ai_provider,
get_container,
override_providers,
reset_container,
)
from cleveragents.core.exceptions import DatabaseError
from cleveragents.domain.models.core import (
Actor,
Change,
DebugAttempt,
OperationType,
Plan,
PlanStatus,
Project,
ProjectSettings,
)
from cleveragents.infrastructure.database import init_database
from cleveragents.infrastructure.database.repositories import (
ActorRepository,
ChangeRepository,
DebugAttemptRepository,
PlanRepository,
ProjectRepository,
)
def _register_cleanup(context, cleanup):
if hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers.append(cleanup)
else:
context._cleanup_handlers = [cleanup]
def _get_features_path_str() -> str:
return str(Path(__file__).resolve().parent.parent)
@given("I have initialized an application container")
def step_init_app_container(context):
"""Initialize an application container."""
context.container = Container()
@when("I request the application settings from the container")
def step_request_settings(context):
"""Request settings from container - covers line 22."""
context.settings = context.container.settings()
@then("the container should return valid settings object")
def step_verify_settings(context):
"""Verify settings object is valid."""
assert context.settings is not None
from cleveragents.config.settings import Settings
assert isinstance(context.settings, Settings)
@when("I request a project service from the container")
def step_request_project_service(context):
"""Request project service - covers line 26."""
context.project_service = context.container.project_service()
@then("the container should return a valid project service instance")
def step_verify_project_service(context):
"""Verify project service is valid."""
assert context.project_service is not None
from cleveragents.application.services.project_service import ProjectService
assert isinstance(context.project_service, ProjectService)
@when("I request a context service from the container")
def step_request_context_service(context):
"""Request context service - covers line 30."""
context.context_service = context.container.context_service()
@then("the container should return a valid context service instance")
def step_verify_context_service(context):
"""Verify context service is valid."""
assert context.context_service is not None
from cleveragents.application.services.context_service import ContextService
assert isinstance(context.context_service, ContextService)
@when("I request a plan service from the container")
def step_request_plan_service(context):
"""Request plan service - covers line 34."""
context.plan_service = context.container.plan_service()
@then("the container should return a valid plan service instance")
def step_verify_plan_service(context):
"""Verify plan service is valid."""
assert context.plan_service is not None
from cleveragents.application.services.plan_service import PlanService
assert isinstance(context.plan_service, PlanService)
@given("I enable the mock AI provider environment flag")
def step_enable_mock_ai_flag(context):
"""Enable the environment flag that triggers mock AI provider loading."""
previous = os.environ.get("CLEVERAGENTS_TESTING_USE_MOCK_AI")
os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] = "true"
def cleanup():
if previous is None:
os.environ.pop("CLEVERAGENTS_TESTING_USE_MOCK_AI", None)
else:
os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] = previous
_register_cleanup(context, cleanup)
@given('I set the "{env_var}" environment variable to "{value}"')
def step_set_environment_variable(context, env_var: str, value: str) -> None:
"""Set an environment variable and register cleanup to restore it."""
previous = os.environ.get(env_var)
os.environ[env_var] = value
def cleanup() -> None:
if previous is None:
os.environ.pop(env_var, None)
else:
os.environ[env_var] = previous
_register_cleanup(context, cleanup)
@given("I ensure the features directory path is not already on sys path")
def step_remove_features_path(context):
"""Ensure the features directory starts off the Python path."""
features_path = _get_features_path_str()
context.features_path = features_path
was_present = features_path in sys.path
if was_present:
sys.path.remove(features_path)
def cleanup():
if was_present and features_path not in sys.path:
sys.path.insert(0, features_path)
elif not was_present and features_path in sys.path:
sys.path.remove(features_path)
_register_cleanup(context, cleanup)
@given("I simulate a failure when importing the mock AI provider")
def step_simulate_mock_ai_import_failure(context):
"""Force import of mock AI provider to fail for coverage."""
original_import = builtins.__import__
def fail_import(name, *args, **kwargs):
if name.startswith("mocks.mock_ai_provider"):
raise ImportError("Simulated mock provider import failure")
return original_import(name, *args, **kwargs)
patcher = patch("builtins.__import__", side_effect=fail_import)
patcher.start()
context.mock_ai_import_patcher = patcher
def cleanup():
patcher.stop()
_register_cleanup(context, cleanup)
@when("I request the AI provider from the container helpers")
def step_request_ai_provider_helper(context):
"""Call get_ai_provider and store the result for assertions."""
context.ai_provider_result = get_ai_provider()
@then("the mock AI provider should be returned and features path added")
def step_verify_mock_ai_provider_returned(context):
"""Verify we received the mock AI provider and path was added."""
from mocks.mock_ai_provider import MockAIProvider
assert isinstance(context.ai_provider_result, MockAIProvider)
features_path = getattr(context, "features_path", _get_features_path_str())
assert features_path in sys.path
@then("the mock AI provider should not be returned due to import failure")
def step_verify_mock_ai_provider_not_returned(context):
"""Verify get_ai_provider returned None after simulated import failure."""
assert context.ai_provider_result is None
@when('I override the database url provider with "{db_url}"')
def step_override_database_url_provider(context, db_url):
"""Override database_url provider so database_url branch executes."""
context.override_database_url = db_url
override_providers(database_url=db_url)
@then("the overridden database url should be used by the container")
def step_verify_database_url_override(context):
"""Ensure the callable provider now returns the overridden value."""
container = get_container()
assert container.database_url() == context.override_database_url
@then("I reset the container overrides to avoid test leakage")
def step_reset_container_overrides(context):
"""Reset container singleton to prevent overrides leaking across tests."""
reset_container()
@given("no global container exists")
def step_no_global_container(context):
"""Ensure no global container exists."""
reset_container() # Covers line 52
@when("I access the global container for the first time")
def step_access_global_container_first(context):
"""Access global container for first time - covers lines 44-46."""
import os
import tempfile
# Create a temp directory for the container to use
context.test_dir = tempfile.mkdtemp(prefix="test_container_")
context.original_cwd = os.getcwd()
os.chdir(context.test_dir)
context.first_container = get_container()
# Clean up after test
def cleanup():
if hasattr(context, "original_cwd"):
os.chdir(context.original_cwd)
if hasattr(context, "test_dir"):
import shutil
if os.path.exists(context.test_dir):
shutil.rmtree(context.test_dir)
# Add cleanup if we have that function
if hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers.append(cleanup)
elif hasattr(context, "add_cleanup"):
context.add_cleanup(cleanup)
@then("a new container instance should be created and cached")
def step_verify_container_created(context):
"""Verify container was created."""
assert context.first_container is not None
# Container instances are DynamicContainer from dependency-injector
from dependency_injector.containers import Container as DIContainer
assert isinstance(context.first_container, DIContainer)
@then("subsequent accesses should return the same instance")
def step_verify_same_instance(context):
"""Verify same instance is returned."""
second_container = get_container()
assert second_container is context.first_container
@given("a global container exists")
def step_global_container_exists(context):
"""Ensure global container exists."""
import os
import tempfile
# Create a temp directory if not already done
if not hasattr(context, "test_dir"):
context.test_dir = tempfile.mkdtemp(prefix="test_container_")
context.original_cwd = os.getcwd()
os.chdir(context.test_dir)
def cleanup():
if hasattr(context, "original_cwd"):
os.chdir(context.original_cwd)
if hasattr(context, "test_dir"):
import shutil
if os.path.exists(context.test_dir):
shutil.rmtree(context.test_dir)
if hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers.append(cleanup)
elif hasattr(context, "add_cleanup"):
context.add_cleanup(cleanup)
context.existing_container = get_container()
@when("I reset the global container")
def step_reset_global_container(context):
"""Reset the global container - covers line 52."""
reset_container()
@then("the global container should be cleared")
def step_verify_container_cleared(context):
"""Verify container was cleared."""
# This is internal state, we verify by next step
pass
@then("the next access should create a new instance")
def step_verify_new_instance(context):
"""Verify new instance is created - covers lines 44-46 again."""
new_container = get_container()
assert new_container is not context.existing_container
from dependency_injector.containers import Container as DIContainer
assert isinstance(new_container, DIContainer)
@given("I have a project repository with database session")
def step_have_project_repo_with_session(context):
"""Create project repository with session."""
context.engine = init_database("sqlite:///:memory:")
Session = sessionmaker(bind=context.engine)
context.session = Session()
context.project_repo = ProjectRepository(context.session)
@given("I have a project repository with a session that fails on create")
def step_project_repo_failing_create(context):
"""Create project repository with failing session for create."""
context.session_mock = MagicMock()
def raise_operational_error(*args, **kwargs):
raise OperationalError("insert project", {}, Exception("db failure"))
context.session_mock.add.side_effect = raise_operational_error
context.session_mock.flush = MagicMock()
context.session_mock.refresh = MagicMock()
context.session_mock.rollback = MagicMock()
context.project_repo = ProjectRepository(context.session_mock)
@when('I attempt to create a project named "{name}" with that failing session')
def step_attempt_create_project_failure(context, name):
"""Attempt to create project and capture database error."""
project = Project(
name=name,
path=Path(f"/test/{name}"),
settings=ProjectSettings(),
)
context.create_error = None
try:
context.project_repo.create(project)
except DatabaseError as exc:
context.create_error = exc
@then("a database error should be raised when creating the project")
def step_verify_create_database_error(context):
"""Verify database error was raised during project creation."""
assert context.create_error is not None
assert isinstance(context.create_error, DatabaseError)
@then("the session rollback should be triggered for the project create failure")
def step_verify_create_rollback(context):
"""Verify session rollback executed on create failure."""
assert context.session_mock.rollback.call_count >= 1
@given("I have a project repository with a session that fails on query")
def step_project_repo_failing_query(context):
"""Create project repository with failing query session."""
context.session_mock = MagicMock()
def raise_query_error(*args, **kwargs):
raise OperationalError("select project", {}, Exception("db failure"))
context.session_mock.query.side_effect = raise_query_error
context.project_repo = ProjectRepository(context.session_mock)
@when("I query the project repository for ID {project_id:d} expecting a failure")
def step_query_project_failure(context, project_id):
"""Attempt to retrieve project and capture database error."""
context.project_lookup_error = None
try:
context.project_repo.get_by_id(project_id)
except DatabaseError as exc:
context.project_lookup_error = exc
@then("a database error should be raised for the project lookup")
def step_verify_project_lookup_error(context):
"""Verify database error was raised for project lookup."""
assert context.project_lookup_error is not None
assert isinstance(context.project_lookup_error, DatabaseError)
@when("I query for a project with non-existent ID {project_id:d}")
def step_query_nonexistent_project(context, project_id):
"""Query for non-existent project - covers line 53 (return None)."""
context.retrieved_project = context.project_repo.get_by_id(project_id)
@then("the repository should return None for missing project")
def step_verify_none_for_missing_project(context):
"""Verify None is returned."""
assert context.retrieved_project is None
@given('I have created a project named "{name}"')
def step_create_named_project(context, name):
"""Create a project with given name."""
project = Project(
name=name,
path=Path(f"/test/{name}"),
settings=ProjectSettings(),
)
context.created_project = context.project_repo.create(project)
@when('I modify the project name to "{new_name}"')
def step_modify_project_name(context, new_name):
"""Update project name - covers lines 86-93 (update path)."""
context.created_project.name = new_name
context.updated_project = context.project_repo.update(context.created_project)
@then("the project should be updated in the database")
def step_verify_project_updated(context):
"""Verify project was updated."""
retrieved = context.project_repo.get_by_id(context.updated_project.id)
assert retrieved.name == context.updated_project.name
@then("the updated_at timestamp should be refreshed")
def step_verify_timestamp_refreshed(context):
"""Verify updated_at was refreshed."""
# The update happens in the repository
assert context.updated_project is not None
@when("I attempt to update a project that does not exist")
def step_update_nonexistent_project(context):
"""Update non-existent project - covers line 86 (if db_project) false branch and line 95."""
fake_project = Project(
id=99999,
name="non-existent",
path=Path("/fake"),
settings=ProjectSettings(),
)
context.update_result = context.project_repo.update(fake_project)
@then("the update operation should complete without error")
def step_verify_update_completes(context):
"""Verify update completes."""
# No exception should be raised
assert context.update_result is not None
@then("the original project object should be returned unchanged")
def step_verify_project_unchanged(context):
"""Verify project is unchanged."""
assert context.update_result.id == 99999
assert context.update_result.name == "non-existent"
@given("I have a plan repository with database session")
def step_have_plan_repo_with_session(context):
"""Create plan repository with session."""
if not hasattr(context, "engine"):
context.engine = init_database("sqlite:///:memory:")
Session = sessionmaker(bind=context.engine)
context.session = Session()
context.plan_repo = PlanRepository(context.session)
# Create a project for plans
if not hasattr(context, "project_repo"):
context.project_repo = ProjectRepository(context.session)
project = Project(
name="test-project",
path=Path("/test"),
settings=ProjectSettings(),
)
context.test_project = context.project_repo.create(project)
@when("I query for current plan of non-existent project {project_id:d}")
def step_query_current_plan_nonexistent(context, project_id):
"""Query current plan for non-existent project - covers line 127 (return None)."""
context.current_plan = context.plan_repo.get_current_for_project(project_id)
@then("the repository should return None for missing current plan")
def step_verify_none_for_missing_plan(context):
"""Verify None is returned."""
assert context.current_plan is None
@given("I have created a plan without build information")
def step_create_plan_without_build(context):
"""Create a plan without build info."""
plan = Plan(
project_id=context.test_project.id,
name="test-plan",
prompt="test prompt",
status=PlanStatus.PENDING,
)
context.created_plan = context.plan_repo.create(plan)
@when("carcov I retrieve the plan by ID")
def step_retrieve_plan_by_id(context):
"""Retrieve plan by ID - covers line 153 (if db_plan.build_started_at) false branch."""
context.retrieved_plan = context.plan_repo.get_by_id(context.created_plan.id)
@then("the plan should be returned with null build field")
def step_verify_plan_null_build(context):
"""Verify plan has null build."""
assert context.retrieved_plan is not None
assert context.retrieved_plan.build is None
@when("I query for a plan with non-existent ID {plan_id:d}")
def step_query_missing_plan(context, plan_id):
"""Query for non-existent plan."""
context.retrieved_plan = context.plan_repo.get_by_id(plan_id)
@then("the plan repository should return None for missing plan")
def step_verify_plan_missing_none(context):
"""Verify plan lookup returns None for missing plan."""
assert context.retrieved_plan is None
@when("I update the plan with a new applied timestamp")
def step_update_plan_applied_timestamp(context):
"""Update plan with new applied timestamp to cover compatibility fields."""
context.new_applied_at = datetime.now()
context.created_plan.applied_at = context.new_applied_at
context.plan_repo.update(context.created_plan)
context.updated_plan = context.plan_repo.get_by_id(context.created_plan.id)
@then("the plan record should store the applied timestamp value")
def step_verify_plan_applied_timestamp(context):
"""Verify applied timestamp persisted."""
assert context.updated_plan is not None
assert context.updated_plan.applied_at == context.new_applied_at
@when("I request the current plan for the project")
def step_request_current_plan_for_project(context):
"""Request current plan for existing project."""
context.current_plan = context.plan_repo.get_current(context.test_project.id)
@given("I have a change repository with the shared database session")
def step_have_change_repo(context):
"""Create change repository using shared session."""
if not hasattr(context, "session"):
context.engine = init_database("sqlite:///:memory:")
Session = sessionmaker(bind=context.engine)
context.session = Session()
context.change_repo = ChangeRepository(context.session)
@given("I have added a change for the plan")
def step_add_change_for_plan(context):
"""Add a change for the current plan."""
change = Change(
plan_id=context.created_plan.id,
file_path="README.md",
operation=OperationType.CREATE,
original_content=None,
new_content="# Added change",
)
context.added_change = context.change_repo.add(change)
@when("I request all changes for the plan")
def step_request_all_changes(context):
"""Retrieve all changes for verification."""
context.retrieved_changes = context.change_repo.get_all()
@then("the repository should include the added change")
def step_verify_change_in_results(context):
"""Verify the added change is present in results."""
assert context.retrieved_changes
assert any(
change.id == context.added_change.id for change in context.retrieved_changes
)
def _ensure_debug_attempt_repo(context):
if not hasattr(context, "debug_session"):
context.debug_engine = init_database("sqlite:///:memory:")
DebugSession = sessionmaker(bind=context.debug_engine)
context.debug_session = DebugSession()
context.debug_project_repo = ProjectRepository(context.debug_session)
context.debug_plan_repo = PlanRepository(context.debug_session)
context.debug_attempt_repo = DebugAttemptRepository(context.debug_session)
project = Project(
name="debug-project",
path=Path("/debug"),
settings=ProjectSettings(),
)
context.debug_project = context.debug_project_repo.create(project)
plan = Plan(
project_id=context.debug_project.id,
name="debug-plan",
prompt="debug prompt",
status=PlanStatus.PENDING,
)
context.debug_plan = context.debug_plan_repo.create(plan)
return context.debug_attempt_repo, context.debug_plan
@given("I have a debug attempt repository with database session")
def step_have_debug_attempt_repo(context):
"""Create debug attempt repository with isolated session."""
_ensure_debug_attempt_repo(context)
@when("I query for a debug attempt with non-existent ID {attempt_id:d}")
def step_query_missing_debug_attempt(context, attempt_id):
"""Query debug attempt that does not exist."""
repo, _plan = _ensure_debug_attempt_repo(context)
context.missing_debug_attempt = repo.get(attempt_id)
@then("the debug attempt repository should return None for missing attempt")
def step_verify_missing_debug_attempt(context):
"""Verify missing debug attempt returns None."""
assert context.missing_debug_attempt is None
@when("I attempt to update a debug attempt that does not exist")
def step_update_missing_debug_attempt(context):
"""Attempt to update missing debug attempt to trigger error."""
repo, plan = _ensure_debug_attempt_repo(context)
missing_attempt = DebugAttempt(
id=99999,
plan_id=plan.id,
error_message="not-found",
attempted_fix=None,
success=False,
attempt_number=1,
created_at=datetime.now(),
)
try:
repo.update(missing_attempt)
context.debug_update_error = None
except Exception as exc:
context.debug_update_error = exc
@then("the debug attempt repository should raise an error for missing attempt")
def step_verify_update_debug_attempt_error(context):
"""Ensure ValueError is raised for missing debug attempt update."""
assert isinstance(context.debug_update_error, ValueError)
@given("I have stored multiple debug attempts for the plan")
def step_store_debug_attempts(context):
"""Store multiple debug attempts for coverage."""
repo, plan = _ensure_debug_attempt_repo(context)
context.stored_debug_attempts = []
for idx in (1, 2):
attempt = DebugAttempt(
plan_id=plan.id,
error_message=f"error-{idx}",
attempted_fix=f"fix-{idx}",
success=idx % 2 == 0,
attempt_number=idx,
created_at=datetime.now(),
)
context.stored_debug_attempts.append(repo.add(attempt))
@when("I request all debug attempts")
def step_request_all_debug_attempts(context):
"""Retrieve all debug attempts for verification."""
repo, _plan = _ensure_debug_attempt_repo(context)
context.all_debug_attempts = repo.get_all()
@then("the repository should return all persisted debug attempts")
def step_verify_all_debug_attempts(context):
"""Verify get_all returns stored attempts."""
assert context.all_debug_attempts
assert len(context.all_debug_attempts) >= len(context.stored_debug_attempts)
@when("I clear debug attempts for the plan")
def step_clear_debug_attempts(context):
"""Clear debug attempts for a plan."""
repo, plan = _ensure_debug_attempt_repo(context)
repo.clear_for_plan(plan.id)
@then("the repository should return no debug attempts for that plan")
def step_verify_cleared_debug_attempts(context):
"""Verify debug attempts were cleared for the plan."""
repo, plan = _ensure_debug_attempt_repo(context)
assert repo.get_for_plan(plan.id) == []
def _ensure_actor_repo(context):
if not hasattr(context, "actor_session"):
context.actor_engine = init_database("sqlite:///:memory:")
ActorSession = sessionmaker(bind=context.actor_engine)
context.actor_session = ActorSession()
context.actor_repo = ActorRepository(context.actor_session)
return context.actor_repo
def _make_actor(
name: str,
*,
built_in: bool = False,
default: bool = False,
config_blob: dict | None = None,
provider: str | None = None,
model_name: str | None = None,
):
blob = config_blob or {"name": name}
provider_value, model_value = ([*name.split("/", 1), ""])[:2]
return Actor(
id=None,
name=name,
provider=provider or provider_value or "provider",
model=model_name or model_value or "model",
config_blob=blob,
config_hash=Actor.compute_hash(blob),
graph_descriptor=None,
unsafe=False,
is_built_in=built_in,
is_default=default,
created_at=datetime.now(),
updated_at=datetime.now(),
)
@given("I have an actor repository with database session")
def step_have_actor_repository(context):
"""Initialize actor repository with isolated session."""
_ensure_actor_repo(context)
@given('I have stored custom actors "{first}" and "{second}"')
def step_store_custom_actors(context, first: str, second: str):
"""Store multiple custom actors for ordering coverage."""
repo = _ensure_actor_repo(context)
for name in (first, second):
repo.upsert(_make_actor(name))
@when("I list actors from the repository")
def step_list_actors_repo(context):
"""List all actors in repository."""
repo = _ensure_actor_repo(context)
context.listed_actors = repo.list_all()
@then("the repository should return the actors ordered by name")
def step_verify_actor_ordering(context):
"""Verify actors are returned in alphabetical order."""
names = [actor.name for actor in context.listed_actors]
assert names == sorted(names)
@given('I have stored a built-in actor named "{name}"')
def step_store_built_in_actor(context, name: str):
"""Persist a built-in actor for overwrite checks."""
repo = _ensure_actor_repo(context)
repo.upsert(_make_actor(name, built_in=True))
@when("I try to upsert a custom actor with the same name")
def step_try_upsert_over_builtin(context):
"""Attempt to overwrite built-in actor with custom entry."""
repo = _ensure_actor_repo(context)
conflicting_actor = _make_actor("provider/model", built_in=False)
try:
repo.upsert(conflicting_actor)
context.actor_upsert_error = None
except Exception as exc:
context.actor_upsert_error = exc
@then("the actor repository should raise when overwriting built-in actor")
def step_verify_builtin_overwrite_error(context):
"""Ensure overwriting built-in actor is rejected."""
assert isinstance(context.actor_upsert_error, ValueError)
@given('I have stored a custom actor named "{name}"')
def step_store_custom_actor(context, name: str):
"""Persist a custom actor for update scenarios."""
repo = _ensure_actor_repo(context)
context.stored_actor = repo.upsert(_make_actor(name))
@when("I update that actor with new configuration values")
def step_update_custom_actor(context):
"""Update an existing custom actor to trigger update branch."""
repo = _ensure_actor_repo(context)
updated_actor = _make_actor(
context.stored_actor.name,
config_blob={"updated": True},
provider="updated-provider",
model_name="updated-model",
)
context.updated_actor = repo.upsert(updated_actor)
@then("the repository should persist the updated actor values")
def step_verify_actor_update(context):
"""Verify actor fields were updated and ID preserved."""
repo = _ensure_actor_repo(context)
refreshed = repo.get_by_name(context.stored_actor.name)
assert refreshed is not None
assert refreshed.provider == "updated-provider"
assert refreshed.model == "updated-model"
assert refreshed.config_blob == {"updated": True}
assert context.updated_actor.id == refreshed.id
@when('I upsert a built-in actor named "{name}"')
def step_upsert_built_in_actor(context, name: str):
"""Use upsert_built_in to persist actor."""
repo = _ensure_actor_repo(context)
actor = _make_actor(name)
context.built_in_actor = repo.upsert_built_in(actor)
@then("the actor should be stored as built-in in the repository")
def step_verify_upsert_built_in(context):
"""Verify actor saved via upsert_built_in is marked built-in."""
repo = _ensure_actor_repo(context)
stored = repo.get_by_name(context.built_in_actor.name)
assert stored is not None
assert stored.is_built_in is True
@given('I have stored a default actor named "{name}"')
def step_store_default_actor(context, name: str):
"""Persist a default actor for deletion tests."""
repo = _ensure_actor_repo(context)
repo.upsert(_make_actor(name, default=True))
@when('I delete a non-existent actor named "{name}"')
def step_delete_missing_actor(context, name: str):
"""Delete an actor that does not exist to cover early return."""
repo = _ensure_actor_repo(context)
repo.delete(name)
@when('I attempt to delete the built-in actor named "{name}"')
def step_delete_built_in_actor(context, name: str):
"""Attempt to delete built-in actor and capture error."""
repo = _ensure_actor_repo(context)
try:
repo.delete(name)
context.built_in_delete_error = None
except Exception as exc:
context.built_in_delete_error = exc
@when('I attempt to delete the default actor named "{name}"')
def step_delete_default_actor(context, name: str):
"""Attempt to delete default actor and capture error."""
repo = _ensure_actor_repo(context)
try:
repo.delete(name)
context.default_delete_error = None
except Exception as exc:
context.default_delete_error = exc
@when('I delete the custom actor named "{name}"')
def step_delete_custom_actor(context, name: str):
"""Delete a removable custom actor."""
repo = _ensure_actor_repo(context)
repo.delete(name)
@then("the built-in actor deletion should raise an error")
def step_verify_built_in_delete_error(context):
"""Verify built-in actor deletion is blocked."""
assert isinstance(context.built_in_delete_error, ValueError)
@then("the default actor deletion should raise an error")
def step_verify_default_delete_error(context):
"""Verify default actor deletion is blocked."""
assert isinstance(context.default_delete_error, ValueError)
@then("the custom actor should be removed successfully")
def step_verify_custom_actor_removed(context):
"""Ensure the custom actor was deleted."""
repo = _ensure_actor_repo(context)
assert repo.get_by_name("local/deletable") is None
@when('I set the default actor to "{name}"')
def step_set_default_missing_actor(context, name: str):
"""Attempt to set default actor for missing entry."""
repo = _ensure_actor_repo(context)
try:
repo.set_default(name)
context.set_default_error = None
except Exception as exc:
context.set_default_error = exc
@then("the actor repository should raise when setting default for missing actor")
def step_verify_set_default_error(context):
"""Ensure setting default on missing actor raises error."""
assert isinstance(context.set_default_error, ValueError)