"""Step definitions for database integration tests.""" import tempfile from datetime import datetime from pathlib import Path from unittest.mock import patch from behave import given, then, when from cleveragents.config.settings import Settings from cleveragents.domain.models.core import ( Change, Context, ContextType, OperationType, Plan, PlanStatus, Project, ProjectSettings, ) from cleveragents.infrastructure.database.unit_of_work import UnitOfWork from features.mocks.mock_ai_provider import MockAIProvider @given("I have a clean test database") def step_clean_test_database(context): """Create a clean test database.""" # Use in-memory SQLite for tests context.database_url = "sqlite:///:memory:" context.unit_of_work = UnitOfWork(context.database_url) context.unit_of_work.init_database() @given("I have initialized the Unit of Work") def step_init_unit_of_work(context): """Initialize Unit of Work.""" if not hasattr(context, "unit_of_work"): context.unit_of_work = UnitOfWork(context.database_url) context.unit_of_work.init_database() @when('I create a project with name "{name}"') def step_create_project(context, name): """Create a project using repository.""" project = Project( name=name, path=Path("/tmp/test-project"), settings=ProjectSettings(), created_at=datetime.now(), updated_at=datetime.now(), ) with context.unit_of_work.transaction() as ctx: context.created_project = ctx.projects.create(project) @then("the project should be saved in the database") def step_verify_project_saved(context): """Verify project is saved.""" assert context.created_project is not None assert context.created_project.id is not None @then("I can retrieve the project using Unit of Work") def step_can_retrieve_project_with_uow(context): """Verify we can retrieve the project using Unit of Work.""" with context.unit_of_work.transaction() as ctx: retrieved = ctx.projects.get_by_name(context.created_project.name) assert retrieved is not None assert retrieved.name == context.created_project.name assert retrieved.id == context.created_project.id @then("the project should have the correct properties") def step_verify_project_properties(context): """Verify project properties.""" with context.unit_of_work.transaction() as ctx: project = ctx.projects.get_by_id(context.created_project.id) assert project.name == context.created_project.name assert project.path == context.created_project.path assert project.settings is not None @when("I start a Unit of Work transaction") def step_start_transaction(context): """Start a UoW transaction.""" context.transaction = context.unit_of_work.transaction() context.transaction_context = context.transaction.__enter__() @when('I create a project "{name}" in the transaction') def step_create_project_in_transaction(context, name): """Create project in transaction.""" project = Project( name=name, path=Path(f"/tmp/{name}"), settings=ProjectSettings(), created_at=datetime.now(), updated_at=datetime.now(), ) context.transaction_project = context.transaction_context.projects.create(project) @when('I create a plan "{name}" in the transaction') def step_create_plan_in_transaction(context, name): """Create plan in transaction.""" plan = Plan( project_id=context.transaction_project.id, name=name, prompt="Test plan", status=PlanStatus.PENDING, current=True, created_at=datetime.now(), updated_at=datetime.now(), ) context.transaction_plan = context.transaction_context.plans.create(plan) @when("I commit the transaction") def step_commit_transaction(context): """Commit the transaction.""" context.transaction.__exit__(None, None, None) @then("both the project and plan should exist in the database") def step_verify_both_exist(context): """Verify both project and plan exist.""" with context.unit_of_work.transaction() as ctx: project = ctx.projects.get_by_id(context.transaction_project.id) assert project is not None plan = ctx.plans.get_by_id(context.transaction_plan.id) assert plan is not None @when("an error occurs during the transaction") def step_error_in_transaction(context): """Simulate an error in transaction.""" try: raise Exception("Simulated error") except Exception as e: context.transaction_error = e @then("the transaction should rollback") def step_verify_rollback(context): """Verify transaction rolled back.""" context.transaction.__exit__( type(context.transaction_error), context.transaction_error, None ) @then("no data should be saved to the database") def step_verify_no_data_saved(context): """Verify no data was saved.""" # Create a new transaction to check the database state # Use a fresh connection to ensure we see the current state from cleveragents.infrastructure.database.unit_of_work import UnitOfWork # Create a new UoW with the same database fresh_uow = UnitOfWork(context.unit_of_work.database_url) fresh_uow.init_database() # Ensure database is initialized with fresh_uow.transaction() as ctx: # Check that the project doesn't exist project = ctx.projects.get_by_name("test-project") # The rollback should have prevented saving # If the project exists, the rollback didn't work assert project is None, "Project was saved despite rollback" @given('I have a project "{name}" in the database') def step_create_project_in_db(context, name): """Create a project in database.""" project = Project( name=name, path=Path(f"/tmp/{name}"), settings=ProjectSettings(), created_at=datetime.now(), updated_at=datetime.now(), ) with context.unit_of_work.transaction() as ctx: context.test_project = ctx.projects.create(project) @when('I create a plan "{name}" for the project') def step_create_plan_for_project(context, name): """Create a plan for the project.""" plan = Plan( project_id=context.test_project.id, name=name, prompt=f"Plan {name}", status=PlanStatus.PENDING, current=False, created_at=datetime.now(), updated_at=datetime.now(), ) with context.unit_of_work.transaction() as ctx: context.first_plan = ctx.plans.create(plan) @when('I create another plan "{name}" for the project') def step_create_another_plan(context, name): """Create another plan.""" plan = Plan( project_id=context.test_project.id, name=name, prompt=f"Plan {name}", status=PlanStatus.PENDING, current=False, created_at=datetime.now(), updated_at=datetime.now(), ) with context.unit_of_work.transaction() as ctx: context.second_plan = ctx.plans.create(plan) @then("I should be able to list all plans for the project") def step_list_plans(context): """List all plans for project.""" with context.unit_of_work.transaction() as ctx: plans = ctx.plans.get_all_for_project(context.test_project.id) assert len(plans) == 2 plan_names = [p.name for p in plans] assert "feature-1" in plan_names assert "feature-2" in plan_names @then('I should be able to set "{name}" as the current plan') def step_set_current_plan(context, name): """Set a plan as current.""" with context.unit_of_work.transaction() as ctx: ctx.plans.set_current(context.test_project.id, context.second_plan.id) @then('the current plan should be "{name}"') def step_verify_current_plan(context, name): """Verify current plan.""" with context.unit_of_work.transaction() as ctx: current = ctx.plans.get_current_for_project(context.test_project.id) assert current is not None assert current.name == name @given("I have a project with a current plan") def step_project_with_current_plan(context): """Create project with current plan.""" project = Project( name="test-project", path=Path("/tmp/test-project"), settings=ProjectSettings(), created_at=datetime.now(), updated_at=datetime.now(), ) with context.unit_of_work.transaction() as ctx: context.test_project = ctx.projects.create(project) plan = Plan( project_id=context.test_project.id, name="current-plan", prompt="Test", status=PlanStatus.PENDING, current=True, created_at=datetime.now(), updated_at=datetime.now(), ) context.current_plan = ctx.plans.create(plan) @when('I add a file "{filename}" to the context') def step_add_file_to_context(context, filename): """Add file to context.""" file_context = Context( plan_id=context.current_plan.id, type=ContextType.FILE, path=filename, content=f"# Content of {filename}", file_hash="abc123", size=100, added_at=datetime.now(), ) with context.unit_of_work.transaction() as ctx: context.first_context = ctx.contexts.add(file_context) @when('I add another file "{filename}" to the context') def step_add_another_file(context, filename): """Add another file to context.""" file_context = Context( plan_id=context.current_plan.id, type=ContextType.FILE, path=filename, content=f"# Content of {filename}", file_hash="def456", size=150, added_at=datetime.now(), ) with context.unit_of_work.transaction() as ctx: context.second_context = ctx.contexts.add(file_context) @then("I should be able to list all context files") def step_list_context_files(context): """List context files.""" with context.unit_of_work.transaction() as ctx: contexts = ctx.contexts.get_for_plan(context.current_plan.id) assert len(contexts) == 2 paths = [c.path for c in contexts] assert "test.py" in paths assert "main.py" in paths @then('I should be able to remove "{filename}" from context') def step_remove_from_context(context, filename): """Remove file from context.""" with context.unit_of_work.transaction() as ctx: ctx.contexts.remove(context.first_context.id) @then('only "{filename}" should remain in context') def step_verify_remaining_context(context, filename): """Verify remaining context.""" with context.unit_of_work.transaction() as ctx: contexts = ctx.contexts.get_for_plan(context.current_plan.id) assert len(contexts) == 1 assert contexts[0].path == filename @given("I have a project with a built plan") def step_project_with_built_plan(context): """Create project with built plan.""" step_project_with_current_plan(context) with context.unit_of_work.transaction() as ctx: context.current_plan.status = PlanStatus.BUILT ctx.plans.update(context.current_plan) @when('I add a change to create "{filename}"') def step_add_create_change(context, filename): """Add a create change.""" change = Change( plan_id=context.current_plan.id, file_path=filename, operation=OperationType.CREATE, new_content="# New file content", applied=False, ) with context.unit_of_work.transaction() as ctx: context.first_change = ctx.changes.add(change) @when('I add a change to modify "{filename}"') def step_add_modify_change(context, filename): """Add a modify change.""" change = Change( plan_id=context.current_plan.id, file_path=filename, operation=OperationType.MODIFY, original_content="# Old content", new_content="# Modified content", applied=False, ) with context.unit_of_work.transaction() as ctx: context.second_change = ctx.changes.add(change) @when("I mark the first change as applied") def step_mark_change_applied(context): """Mark change as applied.""" with context.unit_of_work.transaction() as ctx: ctx.changes.mark_applied(context.first_change.id) @then("the applied change should have an applied timestamp") def step_verify_applied_timestamp(context): """Verify applied timestamp.""" with context.unit_of_work.transaction() as ctx: changes = ctx.changes.get_for_plan(context.current_plan.id) applied_change = next(c for c in changes if c.id == context.first_change.id) assert applied_change.applied is True assert applied_change.applied_at is not None @then("the second change should still be pending") def step_verify_pending_change(context): """Verify pending change.""" with context.unit_of_work.transaction() as ctx: changes = ctx.changes.get_for_plan(context.current_plan.id) pending_change = next(c for c in changes if c.id == context.second_change.id) assert pending_change.applied is False assert pending_change.applied_at is None @when("I use ProjectService to initialize a project") def step_use_project_service(context): """Use ProjectService.""" from cleveragents.application.services.project_service import ProjectService settings = Settings() service = ProjectService(settings, context.unit_of_work) with tempfile.TemporaryDirectory() as tmpdir: context.service_project = service.initialize_project( "service-test", Path(tmpdir) ) @then("the service should use Unit of Work") def step_verify_service_uses_uow(context): """Verify service uses UoW.""" # The service initialized successfully, which means it used UoW assert context.service_project is not None @then("the database infrastructure should be initialized") def step_verify_database_infrastructure_initialized(context): """Verify database infrastructure is initialized.""" # The database was initialized by the service using Unit of Work # We can verify this by checking that we can perform database operations assert context.service_project is not None # Verify we can query the database without errors with context.unit_of_work.transaction() as ctx: # Should be able to query the project # Use the actual project name that was created expected_name = ( context.service_project.name if hasattr(context, "service_project") and context.service_project else "new-project" ) project = ctx.projects.get_by_name(expected_name) assert project is not None, ( f"Project '{expected_name}' should exist in database" ) assert project.name == expected_name @then("the project should be persisted correctly") def step_verify_project_persisted(context): """Verify project is persisted.""" assert context.service_project.name == "service-test" assert context.service_project.settings is not None @given("I have an initialized project for database testing") def step_initialized_project_db(context): """Create initialized project for database tests.""" from cleveragents.application.services.project_service import ProjectService settings = Settings() service = ProjectService(settings, context.unit_of_work) with tempfile.TemporaryDirectory() as tmpdir: context.test_project = service.initialize_project("test-project", Path(tmpdir)) @when("I use PlanService to create a plan") def step_use_plan_service(context): """Use PlanService.""" from cleveragents.application.services.plan_service import PlanService settings = Settings() mock_provider = MockAIProvider() service = PlanService(settings, context.unit_of_work, ai_provider=mock_provider) context.service_plan = service.create_plan( context.test_project, "Create a new feature", "feature-plan" ) @then("the plan should be saved via repository") def step_verify_plan_saved_via_repo(context): """Verify plan saved via repository.""" assert context.service_plan is not None assert context.service_plan.id is not None @then("the plan should be set as current") def step_verify_plan_is_current(context): """Verify plan is current.""" assert context.service_plan.current is True @then("I should be able to retrieve it") def step_retrieve_plan(context): """Retrieve the plan.""" from cleveragents.application.services.plan_service import PlanService settings = Settings() mock_provider = MockAIProvider() service = PlanService(settings, context.unit_of_work, ai_provider=mock_provider) current = service.get_current_plan(context.test_project) assert current is not None assert current.name == "feature-plan" @given("I have a project with a plan") def step_project_with_plan(context): """Create project with plan.""" step_initialized_project_db(context) step_use_plan_service(context) @when("I use ContextService to add files") def step_use_context_service(context): """Use ContextService.""" from cleveragents.application.services.context_service import ContextService settings = Settings() service = ContextService(settings, context.unit_of_work) # Create a temporary file to add with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: f.write("# Test file content\n") context.test_file = Path(f.name) context.added_files = service.add_to_context( context.test_project, context.test_file, recursive=False ) @then("the context should be saved via repository") def step_verify_context_saved(context): """Verify context saved.""" assert len(context.added_files) > 0 @then("the files should be associated with the plan") def step_verify_files_associated(context): """Verify files associated with plan.""" from cleveragents.application.services.context_service import ContextService settings = Settings() service = ContextService(settings, context.unit_of_work) contexts = service.list_context(context.test_project) assert len(contexts) > 0 @then("I should be able to query the context") def step_query_context(context): """Query the context.""" from cleveragents.application.services.context_service import ContextService settings = Settings() service = ContextService(settings, context.unit_of_work) size = service.get_context_size(context.test_project) assert size > 0 # Cleanup if hasattr(context, "test_file"): context.test_file.unlink(missing_ok=True) @given('I have a unit of work without initialization for "{database_url}"') def step_uninitialized_uow(context, database_url: str) -> None: context.database_url = database_url context.unit_of_work = UnitOfWork(database_url) @when("I open a transaction which should auto-run migrations") def step_open_transaction_auto_migration(context) -> None: prompt_calls: list[str] = [] def prompt(message: str) -> bool: prompt_calls.append(message) return True context.unit_of_work = UnitOfWork(context.database_url, prompt_for_migration=prompt) with patch( "cleveragents.infrastructure.database.migration_runner.MigrationRunner.init_or_upgrade", return_value=None, ) as init_mock: with context.unit_of_work.transaction(): pass context.auto_migration_calls = init_mock.call_count context.auto_migration_call_kwargs = ( init_mock.call_args.kwargs if init_mock.call_args else {} ) context.auto_migration_prompt_calls = list(prompt_calls) @then("the migration runner should be invoked once with confirmation") def step_then_migration_runner_called(context) -> None: assert context.auto_migration_calls == 1 assert context.auto_migration_call_kwargs.get("require_confirmation") is True assert context.auto_migration_call_kwargs.get("prompt_for_migration") is not None assert len(context.auto_migration_prompt_calls) == 0