"""Step definitions for CLI plan and context command testing.""" import json import os import shlex import shutil import tempfile from pathlib import Path from behave import given, then, when # type: ignore[import-not-found] from behave.runner import Context # type: ignore[import-not-found] from cleveragents.application.container import get_container, reset_container @given("I have a clean test environment") def step_clean_test_environment(context: Context) -> None: """Create a clean test environment.""" # Create a temporary directory for testing context.test_dir = tempfile.mkdtemp(prefix="cleveragents_test_") context.temp_dir = ( context.test_dir ) # Alias for compatibility with other step definitions context.original_cwd = os.getcwd() os.chdir(context.test_dir) # Reset container for clean state reset_container() # Set up mock AI provider for testing os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] = "true" # Auto-approve migrations during tests to avoid interactive prompts os.environ["CLEVERAGENTS_AUTO_APPLY_MIGRATIONS"] = "true" # Use a file-based SQLite DB scoped to the temp directory so subprocess CLI # calls share the same state. db_path = Path(context.test_dir) / ".cleveragents" / "db.sqlite" os.environ["CLEVERAGENTS_DATABASE_URL"] = f"sqlite:///{db_path}" os.environ["CLEVERAGENTS_TEST_DATABASE_URL"] = f"sqlite:///{db_path}" @given("I have set up the test configuration") def step_setup_test_config(context): """Set up test configuration.""" # Configuration is handled by environment variables pass @given("I am in an empty directory") def step_in_empty_directory(context): """Ensure we're in an empty directory.""" assert len(os.listdir(".")) == 0, "Directory is not empty" @given("I have an initialized project") def step_initialized_project(context): """Initialize a project for testing.""" from cleveragents.cli.commands.project import init_command # Use the CLI command directly init_command("test-project", Path.cwd()) # Verify project was initialized assert (Path.cwd() / ".cleveragents").exists() context.project_dir = Path.cwd() / ".cleveragents" # Ensure an actor is available for mock provider flows container = get_container() actor_service = container.actor_service() try: actor_service.upsert_actor( name="local/mock-default", provider="MockProvider", model="mock-gpt-4", set_default=True, is_built_in=True, unsafe=True, ) except Exception: pass @given('I have a file "{filename}" with content "{content}"') def step_create_file_with_content(context, filename, content): """Create a file with specified content.""" Path(filename).write_text(content) assert Path(filename).exists() @given('I have a file "{filename}"') def step_create_file(context, filename): """Create an empty file.""" Path(filename).touch() assert Path(filename).exists() @given("I have an initialized project with a plan") def step_initialized_project_with_plan(context): """Initialize project and create a plan.""" step_initialized_project(context) from cleveragents.cli.commands.plan import tell_command # Create a plan tell_command("Test instruction") # Verify plan was created container = get_container() plan_service = container.plan_service() project_service = container.project_service() project = project_service.get_current_project() current = plan_service.get_current_plan(project) if project else None assert current is not None @given('the plan has an instruction "{instruction}"') def step_plan_has_instruction(context, instruction): """Set plan instruction.""" container = get_container() plan_service = container.plan_service() project_service = container.project_service() # Update the current plan's prompt project = project_service.get_current_project() current = plan_service.get_current_plan(project) if project else None if current: current.prompt = instruction # Save updated plan (would normally use repository) plans_file = Path.cwd() / ".cleveragents" / "plans.json" if plans_file.exists(): data = json.loads(plans_file.read_text()) for plan in data: if plan["name"] == current.name: plan["prompt"] = instruction plans_file.write_text(json.dumps(data, indent=2)) @given("I have an initialized project with a built plan") def step_initialized_project_with_built_plan(context): """Initialize project with a built plan.""" step_initialized_project_with_plan(context) from cleveragents.cli.commands.plan import build_command # Build the plan build_command() # Verify plan was built container = get_container() plan_service = container.plan_service() project_service = container.project_service() project = project_service.get_current_project() current = plan_service.get_current_plan(project) if project else None assert current is not None assert current.status == "built" @given('the plan has a change to create "{filename}"') def step_plan_has_create_change(context, filename): """Replace existing changes with a change to create the specified file.""" from datetime import datetime from cleveragents.application.container import get_container from cleveragents.domain.models.core import Change, OperationType # Get the current plan from the database container = get_container() plan_service = container.plan_service() project_service = container.project_service() project = project_service.get_current_project() current_plan = plan_service.get_current_plan(project) if project else None if current_plan and current_plan.id: # Clear existing changes and add the specific one we want with container.unit_of_work().transaction() as ctx: # Clear existing changes for this plan ctx.changes.clear_for_plan(current_plan.id) # Add the new change change = Change( id=None, plan_id=current_plan.id, file_path=filename, operation=OperationType.CREATE, original_content=None, new_content="# Generated file\nprint('Hello from CleverAgents')\n", new_path=None, applied=False, created_at=datetime.now(), applied_at=None, ) ctx.changes.add(change) @given('I have a plan named "{name}"') def step_have_plan_named(context, name): """Create a plan with specified name.""" from cleveragents.cli.commands.plan import list_command, new_command # Check if plan already exists existing_plans = list_command() if any(p.name == name for p in existing_plans): # Plan already exists, don't create it again return new_command(name) @given('I have plans named "{names}"') def step_have_multiple_plans(context, names): """Create multiple plans.""" from cleveragents.cli.commands.plan import list_command # Parse the plan names - they can be separated by comma or "and" # Handle formats like: "main", "feature-1", "feature-2" # or: "main" and "feature-1" names = names.replace('"', "") # Split by comma first if "," in names: plan_names = [n.strip() for n in names.split(",")] else: # Split by "and" plan_names = [n.strip() for n in names.split(" and ")] # Check existing plans first existing_plans = list_command() existing_names = [p.name for p in existing_plans] for name in plan_names: if name not in existing_names: step_have_plan_named(context, name) @given('"{name}" is the current plan') def step_set_current_plan(context, name): """Set a specific plan as current.""" from cleveragents.cli.commands.plan import cd_command cd_command(name) @given("the plan has previous conversation history") def step_plan_has_conversation(context): """Add conversation history to the plan.""" # This would normally be stored in the database # For now, we'll just note that the plan has history pass @given('I have added files "{files}" to context') def step_add_files_to_context(context, files): """Add multiple files to context.""" from cleveragents.cli.commands.context import add_command # Remove quotes from filenames and split by comma file_list = [f.strip().strip('"') for f in files.split(",")] for filename in file_list: # Create the file if it doesn't exist if not Path(filename).exists(): Path(filename).touch() # Add to context add_command([filename]) @given('I have added "{filename}" to context') def step_add_file_to_context(context, filename): """Add a single file to context.""" step_add_files_to_context(context, filename) @given('I run "{command}"') def step_given_run_command(context, command): """Run a CLI command.""" step_run_command(context, command) @when('I run "{command}"') @when("I run '{command}'") def step_run_command(context, command): """Run a CLI command.""" import subprocess import sys # Run command using subprocess to capture output properly try: # Use shlex to properly handle quoted strings in the command command_parts = shlex.split(command) # If the command starts with "agents" or "cleveragents", run it via the cleveragents module if command_parts[0] in ["agents", "cleveragents"]: # Replace "agents" or "cleveragents" with the proper module invocation command_parts = command_parts[1:] # Remove the command name result = subprocess.run( [sys.executable, "-m", "cleveragents", *command_parts], capture_output=True, text=True, cwd=context.test_dir if hasattr(context, "test_dir") else None, timeout=60, env=os.environ.copy(), # Pass current environment variables ) else: # Run other commands as-is result = subprocess.run( command_parts, capture_output=True, text=True, cwd=context.test_dir if hasattr(context, "test_dir") else None, timeout=60, env=os.environ.copy(), # Pass current environment variables ) output = result.stdout or "" if result.stderr: output += result.stderr context.command_output = output context.command_error = result.stderr context.command_exit_code = result.returncode context.exit_code = result.returncode # For compatibility with other steps except subprocess.TimeoutExpired: context.command_output = "" context.command_error = "Command timed out" context.command_exit_code = 1 context.exit_code = 1 except Exception as e: context.command_output = "" context.command_error = str(e) context.command_exit_code = 1 context.exit_code = 1 @then("the command should succeed") def step_command_succeeds(context): """Verify command succeeded.""" assert context.command_exit_code == 0, f"Command failed: {context.command_error}" @then("the command should fail") def step_command_fails(context): """Verify command failed.""" assert context.command_exit_code != 0, "Command should have failed but succeeded" @then("a .cleveragents directory should be created") def step_cleveragents_dir_created(context): """Verify .cleveragents directory exists.""" assert (Path.cwd() / ".cleveragents").exists() @then("the database should be initialized in current directory") def step_database_initialized_current_dir(context): """Verify database is initialized in current directory.""" db_file = Path.cwd() / ".cleveragents" / "db.sqlite" assert db_file.exists() or (Path.cwd() / ".cleveragents" / "plans.json").exists() @then('the project should be named "{name}"') def step_project_named(context, name): """Verify project name.""" # Check if we can get project info container = get_container() container.project_service() # Project name verification would go here pass @then("the file should be added to context") def step_file_in_context(context): """Verify file was added to context.""" container = get_container() context_service = container.context_service() files = context_service.list_files() assert len(files) > 0 @then('I should see "{text}" in the output') def step_see_text_in_output(context, text): """Verify text appears in command output.""" assert text in context.command_output, ( f"Expected '{text}' in output: {context.command_output}" ) @then("a new plan should be created") def step_new_plan_created(context): """Verify a new plan was created.""" container = get_container() plan_service = container.plan_service() project_service = container.project_service() project = project_service.get_current_project() current = plan_service.get_current_plan(project) if project else None assert current is not None @then('the plan should contain the instruction "{instruction}"') def step_plan_contains_instruction(context, instruction): """Verify plan contains instruction.""" container = get_container() plan_service = container.plan_service() project_service = container.project_service() project = project_service.get_current_project() current = plan_service.get_current_plan(project) if project else None assert current is not None assert instruction in current.prompt @then("the plan should have generated changes") def step_plan_has_changes(context): """Verify plan has generated changes.""" changes_file = Path.cwd() / ".cleveragents" / "changes.json" if changes_file.exists(): changes = json.loads(changes_file.read_text()) assert len(changes) > 0 @then("the changes should be stored in the database") def step_changes_in_database(context): """Verify changes are stored.""" # Changes are stored in JSON for now (or could be in database) changes_file = Path.cwd() / ".cleveragents" / "changes.json" # Also check if build was successful even without changes file # (changes might be in the database) if not changes_file.exists(): # For testing, we consider it successful if the .cleveragents directory exists assert (Path.cwd() / ".cleveragents").exists() else: assert changes_file.exists() @then('the file "{filename}" should exist') def step_file_exists(context, filename): """Verify file exists.""" assert Path(filename).exists(), f"File {filename} does not exist" @then("the changes should be marked as applied") def step_changes_marked_applied(context): """Verify changes are marked as applied.""" changes_file = Path.cwd() / ".cleveragents" / "changes.json" if changes_file.exists(): changes = json.loads(changes_file.read_text()) assert any(c.get("applied") for c in changes) @then('a plan named "{name}" should be created') def step_plan_named_created(context, name): """Verify plan with name was created.""" container = get_container() plan_service = container.plan_service() project_service = container.project_service() project = project_service.get_current_project() plans = plan_service.list_plans(project) if project else [] assert any(p.name == name for p in plans) @then('"{name}" should be the current plan') def step_verify_current_plan(context, name): """Verify current plan name.""" container = get_container() plan_service = container.plan_service() project_service = container.project_service() project = project_service.get_current_project() current = plan_service.get_current_plan(project) if project else None assert current is not None assert current.name == name @then("the plan should have the additional instruction") def step_plan_has_additional(context): """Verify plan has additional instruction.""" container = get_container() plan_service = container.plan_service() project_service = container.project_service() project = project_service.get_current_project() current = plan_service.get_current_plan(project) if project else None assert current is not None # Would check conversation history here @then('"{filename}" should not be in context') def step_file_not_in_context(context, filename): """Verify file is not in context.""" from pathlib import Path container = get_container() context_service = container.context_service() files = context_service.list_files() # Files are Context objects with a path attribute file_names = [Path(f.path if hasattr(f, "path") else str(f)).name for f in files] assert filename not in file_names @then('"{filename}" should still be in context') def step_file_still_in_context(context, filename): """Verify file is still in context.""" from pathlib import Path container = get_container() context_service = container.context_service() files = context_service.list_files() # Files are Context objects with a path attribute file_names = [Path(f.path if hasattr(f, "path") else str(f)).name for f in files] print(f"DEBUG: Looking for {filename} in {file_names}") # Debug output assert filename in file_names, f"{filename} not found in {file_names}" @then("the context should be empty") def step_context_empty(context): """Verify context is empty.""" container = get_container() context_service = container.context_service() files = context_service.list_files() assert len(files) == 0 @then('the error message should mention "{text}"') def step_error_mentions(context, text): """Verify error message contains text.""" # Check both stdout and stderr for error messages combined_output = (context.command_output or "") + (context.command_error or "") assert text.lower() in combined_output.lower(), ( f"Expected '{text}' not found in: {combined_output}" ) @then('the output should mention "{text}"') def step_output_mentions(context, text): """Verify output mentions text.""" output = context.command_output or context.command_error assert text.lower() in output.lower() @then('it should be equivalent to "{command}"') def step_equivalent_command(context, command): """Verify command equivalence.""" # This is more of a conceptual check # The shortcuts should work the same as the full commands pass # Cleanup def after_scenario(context, scenario): """Clean up after each scenario.""" if hasattr(context, "test_dir"): # Change back to original directory if hasattr(context, "original_cwd"): os.chdir(context.original_cwd) # Remove test directory shutil.rmtree(context.test_dir, ignore_errors=True) # Clean up environment if "CLEVERAGENTS_TESTING_USE_MOCK_AI" in os.environ: del os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] # Reset container reset_container()