"""Step definitions for git worktree sandbox feature. All steps use the ``gwt`` prefix to avoid collisions with the 128+ existing step files loaded globally by Behave. """ from __future__ import annotations import os import subprocess import tempfile from behave import given, then, when from behave.runner import Context from cleveragents.infrastructure.sandbox.git_worktree import GitWorktreeSandbox from cleveragents.infrastructure.sandbox.protocol import ( SandboxCreationError, SandboxStateError, SandboxStatus, ) def _init_test_repo(ctx: Context) -> str: """Create a temporary git repo with an initial commit.""" repo_dir = tempfile.mkdtemp(prefix="gwt-test-repo-") subprocess.run(["git", "init"], cwd=repo_dir, capture_output=True, check=True) subprocess.run( ["git", "config", "user.email", "test@test.com"], cwd=repo_dir, capture_output=True, check=True, ) subprocess.run( ["git", "config", "user.name", "Test"], cwd=repo_dir, capture_output=True, check=True, ) subprocess.run( ["git", "config", "commit.gpgSign", "false"], cwd=repo_dir, capture_output=True, check=True, ) # Create initial files readme = os.path.join(repo_dir, "README.md") with open(readme, "w") as f: f.write("# Test Repo\n") subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True, check=True) subprocess.run( ["git", "commit", "-m", "Initial commit"], cwd=repo_dir, capture_output=True, check=True, ) return repo_dir # --------------------------------------------------------------------------- # Background / Given # --------------------------------------------------------------------------- @given("a gwt test git repository is initialised") def step_gwt_init_repo(ctx: Context) -> None: ctx.gwt_repo_dir = _init_test_repo(ctx) ctx.gwt_sandbox = None ctx.gwt_error = None ctx.gwt_commit_result = None ctx.gwt_resolved_path = None @given("a gwt non-git directory") def step_gwt_non_git_dir(ctx: Context) -> None: ctx.gwt_non_git_dir = tempfile.mkdtemp(prefix="gwt-non-git-") # --------------------------------------------------------------------------- # When - instantiation # --------------------------------------------------------------------------- @when("a gwt sandbox is instantiated") def step_gwt_instantiate(ctx: Context) -> None: ctx.gwt_sandbox = GitWorktreeSandbox( resource_id="res-001", original_path=ctx.gwt_repo_dir, ) @when("a gwt sandbox is prepared with empty resource_id") def step_gwt_empty_resource_id(ctx: Context) -> None: try: GitWorktreeSandbox(resource_id="", original_path=ctx.gwt_repo_dir) except ValueError as exc: ctx.gwt_error = exc @when("a gwt sandbox is prepared with empty original_path") def step_gwt_empty_original_path(ctx: Context) -> None: try: GitWorktreeSandbox(resource_id="res-001", original_path="") except ValueError as exc: ctx.gwt_error = exc @when("a gwt sandbox is prepared with zero timeout") def step_gwt_zero_timeout(ctx: Context) -> None: try: GitWorktreeSandbox( resource_id="res-001", original_path=ctx.gwt_repo_dir, git_timeout=0, ) except ValueError as exc: ctx.gwt_error = exc # --------------------------------------------------------------------------- # When - create # --------------------------------------------------------------------------- @when('a gwt sandbox is created for plan "{plan_id}"') def step_gwt_create(ctx: Context, plan_id: str) -> None: ctx.gwt_sandbox = GitWorktreeSandbox( resource_id="res-001", original_path=ctx.gwt_repo_dir, ) ctx.gwt_sandbox.create(plan_id) @when("a gwt sandbox is created with empty plan_id") def step_gwt_create_empty_plan(ctx: Context) -> None: ctx.gwt_sandbox = GitWorktreeSandbox( resource_id="res-001", original_path=ctx.gwt_repo_dir, ) try: ctx.gwt_sandbox.create("") except ValueError as exc: ctx.gwt_error = exc @when('a gwt sandbox is created on the non-git directory for plan "{plan_id}"') def step_gwt_create_non_git(ctx: Context, plan_id: str) -> None: ctx.gwt_sandbox = GitWorktreeSandbox( resource_id="res-001", original_path=ctx.gwt_non_git_dir, ) try: ctx.gwt_sandbox.create(plan_id) except SandboxCreationError as exc: ctx.gwt_error = exc # --------------------------------------------------------------------------- # When - path resolution # --------------------------------------------------------------------------- @when('the gwt path "{path}" is resolved') def step_gwt_resolve_path(ctx: Context, path: str) -> None: try: ctx.gwt_resolved_path = ctx.gwt_sandbox.get_path(path) except (ValueError, SandboxStateError) as exc: ctx.gwt_error = exc @when('the gwt path "{path}" is resolved on a cleaned-up sandbox') def step_gwt_resolve_path_cleaned(ctx: Context, path: str) -> None: try: ctx.gwt_sandbox.get_path(path) except SandboxStateError as exc: ctx.gwt_error = exc # --------------------------------------------------------------------------- # When - file operations in worktree # --------------------------------------------------------------------------- @when('a gwt file "{filename}" is created in the worktree with content "{content}"') def step_gwt_create_file(ctx: Context, filename: str, content: str) -> None: worktree_path = ctx.gwt_sandbox.context.sandbox_path file_path = os.path.join(worktree_path, filename) os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, "w") as f: f.write(content) @when('the gwt existing file "{filename}" is modified in the worktree') def step_gwt_modify_file(ctx: Context, filename: str) -> None: worktree_path = ctx.gwt_sandbox.context.sandbox_path file_path = os.path.join(worktree_path, filename) with open(file_path, "a") as f: f.write("\n# Modified by sandbox\n") # --------------------------------------------------------------------------- # When - commit / rollback / cleanup # --------------------------------------------------------------------------- @when('the gwt sandbox is committed with message "{message}"') def step_gwt_commit(ctx: Context, message: str) -> None: try: ctx.gwt_commit_result = ctx.gwt_sandbox.commit(message) except (SandboxStateError, Exception) as exc: ctx.gwt_error = exc @when("the gwt sandbox commit is attempted on cleaned-up sandbox") def step_gwt_commit_cleaned(ctx: Context) -> None: try: ctx.gwt_sandbox.commit("should fail") except SandboxStateError as exc: ctx.gwt_error = exc @when("the gwt sandbox is rolled back") def step_gwt_rollback(ctx: Context) -> None: try: ctx.gwt_sandbox.rollback() except (SandboxStateError, Exception) as exc: ctx.gwt_error = exc @when("the gwt sandbox rollback is attempted on created sandbox") def step_gwt_rollback_created(ctx: Context) -> None: try: ctx.gwt_sandbox.rollback() except SandboxStateError as exc: ctx.gwt_error = exc @when("the gwt sandbox is cleaned up") def step_gwt_cleanup(ctx: Context) -> None: ctx.gwt_sandbox.cleanup() @when("the gwt sandbox is cleaned up again") def step_gwt_cleanup_again(ctx: Context) -> None: ctx.gwt_sandbox.cleanup() # --------------------------------------------------------------------------- # Then - status / state # --------------------------------------------------------------------------- @then('the gwt sandbox should be in the "{status}" state') def step_gwt_check_status(ctx: Context, status: str) -> None: expected = SandboxStatus(status) assert ctx.gwt_sandbox.status == expected, ( f"Expected status {expected}, got {ctx.gwt_sandbox.status}" ) @then('the gwt sandbox context should reference plan "{plan_id}"') def step_gwt_check_plan(ctx: Context, plan_id: str) -> None: assert ctx.gwt_sandbox.context is not None assert ctx.gwt_sandbox.context.plan_id == plan_id @then('the gwt sandbox context should have strategy metadata "{strategy}"') def step_gwt_check_strategy(ctx: Context, strategy: str) -> None: assert ctx.gwt_sandbox.context is not None assert ctx.gwt_sandbox.context.metadata.get("strategy") == strategy @then("the gwt sandbox worktree path should exist") def step_gwt_worktree_exists(ctx: Context) -> None: assert ctx.gwt_sandbox.context is not None assert os.path.isdir(ctx.gwt_sandbox.context.sandbox_path) @then("the gwt sandbox worktree path should not exist") def step_gwt_worktree_not_exists(ctx: Context) -> None: if ctx.gwt_sandbox.context is not None: assert not os.path.exists(ctx.gwt_sandbox.context.sandbox_path) @then("the gwt sandbox branch should not exist") def step_gwt_branch_not_exists(ctx: Context) -> None: result = subprocess.run( ["git", "branch", "--list"], cwd=ctx.gwt_repo_dir, capture_output=True, text=True, check=False, ) branch_name = ctx.gwt_sandbox._branch_name if branch_name: assert branch_name not in result.stdout # --------------------------------------------------------------------------- # Then - errors # --------------------------------------------------------------------------- @then('a gwt ValueError should be raised with message "{msg}"') def step_gwt_valueerror(ctx: Context, msg: str) -> None: assert ctx.gwt_error is not None, "Expected an error but none occurred" assert isinstance(ctx.gwt_error, ValueError), ( f"Expected ValueError, got {type(ctx.gwt_error).__name__}" ) assert msg in str(ctx.gwt_error), ( f"Expected '{msg}' in error message, got: {ctx.gwt_error}" ) @then("a gwt SandboxCreationError should be raised") def step_gwt_creation_error(ctx: Context) -> None: assert ctx.gwt_error is not None assert isinstance(ctx.gwt_error, SandboxCreationError) @then("a gwt SandboxStateError should be raised") def step_gwt_state_error(ctx: Context) -> None: assert ctx.gwt_error is not None assert isinstance(ctx.gwt_error, SandboxStateError) # --------------------------------------------------------------------------- # Then - path resolution # --------------------------------------------------------------------------- @then("the gwt resolved path should be inside the worktree") def step_gwt_path_in_worktree(ctx: Context) -> None: assert ctx.gwt_resolved_path is not None sandbox_path = ctx.gwt_sandbox.context.sandbox_path assert ctx.gwt_resolved_path.startswith(sandbox_path) # --------------------------------------------------------------------------- # Then - commit results # --------------------------------------------------------------------------- @then("the gwt commit result should indicate success") def step_gwt_commit_success(ctx: Context) -> None: assert ctx.gwt_commit_result is not None assert ctx.gwt_commit_result.success is True @then("the gwt commit result should have {count:d} changed files") def step_gwt_commit_changed(ctx: Context, count: int) -> None: assert ctx.gwt_commit_result is not None assert len(ctx.gwt_commit_result.changed_files) == count @then("the gwt commit result should have {count:d} added files") def step_gwt_commit_added(ctx: Context, count: int) -> None: assert ctx.gwt_commit_result is not None assert len(ctx.gwt_commit_result.added_files) == count @then('the gwt file "{filename}" should exist in the original repo') def step_gwt_file_in_original(ctx: Context, filename: str) -> None: file_path = os.path.join(ctx.gwt_repo_dir, filename) assert os.path.exists(file_path), f"File {filename} not found in original repo" @then('the gwt file "{filename}" should not exist in the original repo') def step_gwt_file_not_in_original(ctx: Context, filename: str) -> None: file_path = os.path.join(ctx.gwt_repo_dir, filename) assert not os.path.exists(file_path), ( f"File {filename} should not exist in original repo after rollback" ) @then('the gwt file "{filename}" should not exist in the worktree') def step_gwt_file_not_in_worktree(ctx: Context, filename: str) -> None: if ctx.gwt_sandbox.context: file_path = os.path.join(ctx.gwt_sandbox.context.sandbox_path, filename) assert not os.path.exists(file_path) # --------------------------------------------------------------------------- # Then - protocol properties # --------------------------------------------------------------------------- @then("the gwt sandbox_id should be a valid ULID") def step_gwt_valid_ulid(ctx: Context) -> None: sid = ctx.gwt_sandbox.sandbox_id assert len(sid) == 26, f"Expected ULID length 26, got {len(sid)}" @then('the gwt sandbox status should be "{status}"') def step_gwt_status_is(ctx: Context, status: str) -> None: assert ctx.gwt_sandbox.status == SandboxStatus(status) @then("the gwt sandbox context should be None") def step_gwt_context_none(ctx: Context) -> None: assert ctx.gwt_sandbox.context is None @then("the gwt sandbox branch name should be safe for git") def step_gwt_branch_safe(ctx: Context) -> None: branch = ctx.gwt_sandbox._branch_name assert branch is not None # Branch name should not contain spaces or special chars assert " " not in branch assert "!" not in branch assert "@" not in branch assert "#" not in branch