forked from HAL9000/cleveragents-core
051ee7c290
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
946 lines
33 KiB
Python
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)
|