"""Step definitions for CopyOnWriteSandbox uncovered lines and branches. Targets every gap identified in the coverage report for ``copy_on_write.py`` (line-rate=0.9124, branch-rate=0.8519): - Lines 143-145: ``create()`` OSError handler wrapping into SandboxCreationError - Line 196 branch False→199: ``get_path()`` when already ACTIVE (skip CREATED transition) - Line 200: ``get_path()`` raises when ``_sandbox_path`` is ``None`` - Line 230: ``commit()`` raises when ``_sandbox_path`` is ``None`` - Line 242 branch False→244: ``commit()`` skips ``os.makedirs`` when ``dst_dir`` is empty - Line 249 branch False→247: ``commit()`` deleted file already missing from original - Lines 252-254: ``commit()`` OSError handler wrapping into SandboxCommitError - Line 297: ``rollback()`` raises when ``_sandbox_path`` is ``None`` - Lines 311-313: ``rollback()`` OSError handler wrapping into SandboxRollbackError - Line 341 branch False→347: ``cleanup()`` when ``_sandbox_path`` is ``None`` - Line 344 branch False→347: ``cleanup()`` when parent dir already removed All steps use the ``cowcov`` prefix to avoid collisions with existing ``cow`` and ``cowcb`` step prefixes. """ from __future__ import annotations import os import shutil import tempfile from unittest.mock import MagicMock, patch from behave import given, then, when from behave.runner import Context from cleveragents.infrastructure.sandbox.copy_on_write import CopyOnWriteSandbox from cleveragents.infrastructure.sandbox.protocol import ( SandboxCommitError, SandboxCreationError, SandboxRollbackError, SandboxStateError, SandboxStatus, ) # ---------------------------------------------------------------------- # Helpers # ---------------------------------------------------------------------- def _cowcov_make_test_dir() -> str: """Create a temporary directory with seed files for testing.""" d = tempfile.mkdtemp(prefix="cowcov-test-dir-") with open(os.path.join(d, "existing.txt"), "w") as f: f.write("original content") with open(os.path.join(d, "to_delete.txt"), "w") as f: f.write("will be deleted") os.makedirs(os.path.join(d, "sub"), exist_ok=True) with open(os.path.join(d, "sub", "nested.txt"), "w") as f: f.write("nested") return d def _cowcov_cleanup_dir(path: str) -> None: """Remove a directory tree if it still exists.""" if path and os.path.exists(path): shutil.rmtree(path, ignore_errors=True) # ---------------------------------------------------------------------- # Given # ---------------------------------------------------------------------- @given("a cowcov test directory is initialised") def step_cowcov_init_dir(context: Context) -> None: """Create a fresh temporary directory with seed files.""" context.cowcov_test_dir = _cowcov_make_test_dir() context.cowcov_sandbox = None context.cowcov_error = None context.cowcov_commit_result = None context.cowcov_resolved_paths = [] context.cowcov_makedirs_mock = None context._cleanup_handlers.append( lambda: _cowcov_cleanup_dir(context.cowcov_test_dir) ) @given("shutil.copytree is patched to raise OSError in cowcov") def step_cowcov_patch_copytree_oserror(context: Context) -> None: """Patch shutil.copytree in the copy_on_write module to raise OSError.""" patcher = patch( "cleveragents.infrastructure.sandbox.copy_on_write.shutil.copytree", side_effect=OSError("mocked copytree failure"), ) patcher.start() context._cleanup_handlers.append(patcher.stop) @given('a cowcov sandbox is created for plan "{plan_id}"') def step_cowcov_create_sandbox(context: Context, plan_id: str) -> None: """Create a CopyOnWriteSandbox and call create().""" context.cowcov_sandbox = CopyOnWriteSandbox( resource_id="res-cowcov", original_path=context.cowcov_test_dir, ) context.cowcov_sandbox.create(plan_id) sandbox_path = context.cowcov_sandbox._sandbox_path if sandbox_path: parent = os.path.dirname(sandbox_path) context._cleanup_handlers.append(lambda: _cowcov_cleanup_dir(parent)) @given('a cowcov sandbox is created and activated for plan "{plan_id}"') def step_cowcov_create_and_activate(context: Context, plan_id: str) -> None: """Create a sandbox and transition to ACTIVE via get_path.""" context.cowcov_sandbox = CopyOnWriteSandbox( resource_id="res-cowcov", original_path=context.cowcov_test_dir, ) context.cowcov_sandbox.create(plan_id) context.cowcov_sandbox.get_path("existing.txt") assert context.cowcov_sandbox.status == SandboxStatus.ACTIVE, ( f"Expected ACTIVE, got {context.cowcov_sandbox.status}" ) sandbox_path = context.cowcov_sandbox._sandbox_path if sandbox_path: parent = os.path.dirname(sandbox_path) context._cleanup_handlers.append(lambda: _cowcov_cleanup_dir(parent)) @given("a cowcov sandbox forced to ACTIVE with sandbox_path None") def step_cowcov_force_active_none(context: Context) -> None: """Create a sandbox stub with status=ACTIVE and _sandbox_path=None.""" d = _cowcov_make_test_dir() context.cowcov_test_dir = d sandbox = CopyOnWriteSandbox(resource_id="res-cowcov", original_path=d) sandbox._status = SandboxStatus.ACTIVE sandbox._sandbox_path = None context.cowcov_sandbox = sandbox context.cowcov_error = None context.cowcov_commit_result = None context.cowcov_resolved_paths = [] context._cleanup_handlers.append(lambda: _cowcov_cleanup_dir(d)) @given("a cowcov sandbox forced to CREATED with sandbox_path None") def step_cowcov_force_created_none(context: Context) -> None: """Create a sandbox stub with status=CREATED and _sandbox_path=None.""" d = _cowcov_make_test_dir() context.cowcov_test_dir = d sandbox = CopyOnWriteSandbox(resource_id="res-cowcov", original_path=d) sandbox._status = SandboxStatus.CREATED sandbox._sandbox_path = None context.cowcov_sandbox = sandbox context.cowcov_error = None context.cowcov_commit_result = None context.cowcov_resolved_paths = [] context._cleanup_handlers.append(lambda: _cowcov_cleanup_dir(d)) @given("a cowcov sandbox in PENDING state with sandbox_path None") def step_cowcov_pending_none(context: Context) -> None: """Create a sandbox that is still PENDING (never created).""" d = _cowcov_make_test_dir() context.cowcov_test_dir = d sandbox = CopyOnWriteSandbox(resource_id="res-cowcov", original_path=d) assert sandbox._sandbox_path is None, "Expected _sandbox_path to be None" assert sandbox.status == SandboxStatus.PENDING, "Expected PENDING status" context.cowcov_sandbox = sandbox context.cowcov_error = None context.cowcov_commit_result = None context._cleanup_handlers.append(lambda: _cowcov_cleanup_dir(d)) @given("cowcov _compute_diff is patched to return a changed root file") def step_cowcov_patch_compute_diff(context: Context) -> None: """Patch compute_diff to return one changed file at root level.""" patcher = patch( "cleveragents.infrastructure.sandbox.copy_on_write.compute_diff", return_value=(["rootfile.txt"], [], []), ) patcher.start() context._cleanup_handlers.append(patcher.stop) @given("cowcov os.path.dirname is patched to return empty string") def step_cowcov_patch_dirname(context: Context) -> None: """Patch os.path.dirname in the copy_on_write module to return ''.""" patcher = patch( "cleveragents.infrastructure.sandbox.copy_on_write.os.path.dirname", return_value="", ) patcher.start() context._cleanup_handlers.append(patcher.stop) @given("cowcov shutil.copy2 is patched to no-op") def step_cowcov_patch_copy2_noop(context: Context) -> None: """Patch shutil.copy2 in the copy_on_write module to do nothing.""" makedirs_mock = MagicMock() patcher_copy2 = patch( "cleveragents.infrastructure.sandbox.copy_on_write.shutil.copy2", ) patcher_makedirs = patch( "cleveragents.infrastructure.sandbox.copy_on_write.os.makedirs", makedirs_mock, ) patcher_copy2.start() patcher_makedirs.start() context.cowcov_makedirs_mock = makedirs_mock context._cleanup_handlers.append(patcher_copy2.stop) context._cleanup_handlers.append(patcher_makedirs.stop) @given('a cowcov file "existing.txt" is modified in the sandbox') def step_cowcov_modify_existing(context: Context) -> None: """Modify a file inside the sandbox copy.""" sandbox_path = context.cowcov_sandbox._sandbox_path assert sandbox_path is not None, "Sandbox path must be set" fpath = os.path.join(sandbox_path, "existing.txt") with open(fpath, "w") as f: f.write("modified for OSError test") @given("cowcov shutil.copy2 is patched to raise OSError") def step_cowcov_patch_copy2_oserror(context: Context) -> None: """Patch shutil.copy2 in copy_on_write module to raise OSError.""" patcher = patch( "cleveragents.infrastructure.sandbox.copy_on_write.shutil.copy2", side_effect=OSError("mocked copy2 failure"), ) patcher.start() context._cleanup_handlers.append(patcher.stop) @given("cowcov shutil.copytree is patched to raise OSError for rollback") def step_cowcov_patch_copytree_rollback(context: Context) -> None: """Patch shutil.copytree to raise OSError (for rollback re-copy).""" patcher = patch( "cleveragents.infrastructure.sandbox.copy_on_write.shutil.copytree", side_effect=OSError("mocked copytree failure during rollback"), ) patcher.start() context._cleanup_handlers.append(patcher.stop) @given("the cowcov sandbox parent directory is manually removed") def step_cowcov_remove_parent(context: Context) -> None: """Remove the sandbox parent temp dir before cleanup.""" sandbox_path = context.cowcov_sandbox._sandbox_path assert sandbox_path is not None, "Sandbox path must be set" parent = os.path.dirname(sandbox_path) if os.path.exists(parent): shutil.rmtree(parent, ignore_errors=True) # ---------------------------------------------------------------------- # When # ---------------------------------------------------------------------- @when('a cowcov sandbox create is attempted for plan "{plan_id}"') def step_cowcov_attempt_create(context: Context, plan_id: str) -> None: """Attempt to create a sandbox, capturing any error.""" context.cowcov_sandbox = CopyOnWriteSandbox( resource_id="res-cowcov", original_path=context.cowcov_test_dir, ) try: context.cowcov_sandbox.create(plan_id) except (SandboxCreationError, SandboxStateError) as exc: context.cowcov_error = exc @when('cowcov get_path is called with "{path}"') def step_cowcov_get_path(context: Context, path: str) -> None: """Call get_path and record the resolved path.""" resolved = context.cowcov_sandbox.get_path(path) context.cowcov_resolved_paths.append(resolved) @when('cowcov get_path is attempted with "{path}"') def step_cowcov_attempt_get_path(context: Context, path: str) -> None: """Attempt get_path, capturing any error.""" try: context.cowcov_sandbox.get_path(path) except (SandboxStateError, ValueError) as exc: context.cowcov_error = exc @when("cowcov commit is attempted") def step_cowcov_attempt_commit(context: Context) -> None: """Attempt commit, capturing any error.""" try: context.cowcov_commit_result = context.cowcov_sandbox.commit() except (SandboxStateError, SandboxCommitError) as exc: context.cowcov_error = exc @when('the cowcov file "existing.txt" is deleted from both sandbox and original') def step_cowcov_delete_from_both(context: Context) -> None: """Delete a file from both sandbox copy and original directory.""" sandbox_path = context.cowcov_sandbox._sandbox_path assert sandbox_path is not None, "Sandbox path must be set" for base in (sandbox_path, context.cowcov_test_dir): fpath = os.path.join(base, "existing.txt") if os.path.exists(fpath): os.remove(fpath) @when("cowcov rollback is attempted") def step_cowcov_attempt_rollback(context: Context) -> None: """Attempt rollback, capturing any error.""" try: context.cowcov_sandbox.rollback() except (SandboxStateError, SandboxRollbackError) as exc: context.cowcov_error = exc @when("cowcov cleanup is called") def step_cowcov_cleanup(context: Context) -> None: """Call cleanup on the sandbox.""" context.cowcov_sandbox.cleanup() # ---------------------------------------------------------------------- # Then # ---------------------------------------------------------------------- @then("a cowcov SandboxCreationError should be raised") def step_cowcov_check_creation_error(context: Context) -> None: """Assert a SandboxCreationError was captured.""" assert context.cowcov_error is not None, ( "Expected SandboxCreationError but no error occurred" ) assert isinstance(context.cowcov_error, SandboxCreationError), ( f"Expected SandboxCreationError, got {type(context.cowcov_error).__name__}: " f"{context.cowcov_error}" ) @then('the cowcov error message should contain "{fragment}"') def step_cowcov_error_message_contains(context: Context, fragment: str) -> None: """Assert the error message contains the given fragment.""" assert context.cowcov_error is not None, "No error was captured" assert fragment in str(context.cowcov_error), ( f"Expected '{fragment}' in error message, got: {context.cowcov_error}" ) @then('the cowcov sandbox status should be "{status}"') def step_cowcov_check_status(context: Context, status: str) -> None: """Assert the sandbox status matches the expected value.""" expected = SandboxStatus(status) assert context.cowcov_sandbox.status == expected, ( f"Expected status {expected.value}, got {context.cowcov_sandbox.status.value}" ) @then("both cowcov resolved paths should be valid") def step_cowcov_two_paths_valid(context: Context) -> None: """Assert two paths were resolved and both are inside the sandbox.""" assert len(context.cowcov_resolved_paths) == 2, ( f"Expected 2 resolved paths, got {len(context.cowcov_resolved_paths)}" ) sandbox_path = context.cowcov_sandbox._sandbox_path assert sandbox_path is not None, "Sandbox path should be set" for p in context.cowcov_resolved_paths: assert p.startswith(sandbox_path), ( f"Resolved path {p} is not inside sandbox {sandbox_path}" ) @then('a cowcov SandboxStateError should be raised with message "{msg}"') def step_cowcov_check_state_error_msg(context: Context, msg: str) -> None: """Assert a SandboxStateError with the expected message was captured.""" assert context.cowcov_error is not None, ( "Expected SandboxStateError but no error occurred" ) assert isinstance(context.cowcov_error, SandboxStateError), ( f"Expected SandboxStateError, got {type(context.cowcov_error).__name__}: " f"{context.cowcov_error}" ) assert msg in str(context.cowcov_error), ( f"Expected '{msg}' in error message, got: {context.cowcov_error}" ) @then("the cowcov commit should succeed") def step_cowcov_commit_success(context: Context) -> None: """Assert commit completed without error.""" assert context.cowcov_error is None, ( f"Expected no error but got: {context.cowcov_error}" ) assert context.cowcov_commit_result is not None, "Expected a commit result" assert context.cowcov_commit_result.success is True, ( "Expected commit result success=True" ) @then("cowcov os.makedirs should not have been called") def step_cowcov_makedirs_not_called(context: Context) -> None: """Assert os.makedirs was not invoked (dst_dir was empty).""" assert context.cowcov_makedirs_mock is not None, "makedirs mock not set up" context.cowcov_makedirs_mock.assert_not_called() @then("a cowcov SandboxCommitError should be raised") def step_cowcov_check_commit_error(context: Context) -> None: """Assert a SandboxCommitError was captured.""" assert context.cowcov_error is not None, ( "Expected SandboxCommitError but no error occurred" ) assert isinstance(context.cowcov_error, SandboxCommitError), ( f"Expected SandboxCommitError, got {type(context.cowcov_error).__name__}: " f"{context.cowcov_error}" ) @then("a cowcov SandboxRollbackError should be raised") def step_cowcov_check_rollback_error(context: Context) -> None: """Assert a SandboxRollbackError was captured.""" assert context.cowcov_error is not None, ( "Expected SandboxRollbackError but no error occurred" ) assert isinstance(context.cowcov_error, SandboxRollbackError), ( f"Expected SandboxRollbackError, got {type(context.cowcov_error).__name__}: " f"{context.cowcov_error}" )