Feature: Consolidated Sandbox
  Combined scenarios from: copy_on_write_sandbox, cow_sandbox_coverage_boost, no_sandbox_coverage, sandbox_checkpoints, sandbox_copy_on_write_coverage, sandbox_factory_coverage

  # ============================================================
  # Originally from: copy_on_write_sandbox.feature
  # Feature: Copy-on-write sandbox lifecycle
  # ============================================================

  Scenario: Create a copy-on-write sandbox
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    Then the cow sandbox should be in the "created" state
    And the cow sandbox context should reference plan "plan-001"
    And the cow sandbox context should have strategy metadata "copy_on_write"
    And the cow sandbox path should exist


  Scenario: Creating a sandbox with empty plan_id raises ValueError
    Given a cow test directory is initialised
    When a cow sandbox is created with empty plan_id
    Then a cow ValueError should be raised with message "plan_id cannot be empty"


  Scenario: Creating a sandbox with empty resource_id raises ValueError
    Given a cow test directory is initialised
    When a cow sandbox is prepared with empty resource_id
    Then a cow ValueError should be raised with message "resource_id cannot be empty"


  Scenario: Creating a sandbox with empty original_path raises ValueError
    Given a cow test directory is initialised
    When a cow sandbox is prepared with empty original_path
    Then a cow ValueError should be raised with message "original_path cannot be empty"


  Scenario: Creating a sandbox on a non-existent directory raises SandboxCreationError
    Given a cow test directory is initialised
    Given a cow non-existent directory
    When a cow sandbox is created on the non-existent directory for plan "plan-001"
    Then a cow SandboxCreationError should be raised

    # --- Path resolution ---


  Scenario: Resolve a path in the sandbox copy
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And the cow path "data/file.txt" is resolved
    Then the cow resolved path should be inside the sandbox copy
    And the cow sandbox should be in the "active" state


  Scenario: Path traversal is rejected
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And the cow path "../etc/passwd" is resolved
    Then a cow ValueError should be raised with message "Path traversal not allowed"


  Scenario: Resolving a path on a cleaned-up sandbox raises SandboxStateError
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And the cow sandbox is cleaned up
    And the cow path "file.txt" is resolved on a cleaned-up sandbox
    Then a cow SandboxStateError should be raised

    # --- Commit ---


  Scenario: Commit with no changes produces empty result
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And the cow sandbox is committed
    Then the cow commit result should indicate success
    And the cow commit result should have 0 changed files
    And the cow commit result should have 0 added files
    And the cow commit result should have 0 deleted files


  Scenario: Commit with a new file syncs it to original
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And a cow file "new_file.txt" is created in the sandbox with content "hello world"
    And the cow sandbox is committed
    Then the cow commit result should indicate success
    And the cow commit result should have 1 added files
    And the cow file "new_file.txt" should exist in the original directory with content "hello world"


  Scenario: Commit with a modified file syncs the change
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And the cow existing file "existing.txt" is modified in the sandbox with content "updated"
    And the cow sandbox is committed
    Then the cow commit result should indicate success
    And the cow commit result should have 1 changed files
    And the cow file "existing.txt" in the original should have content "updated"


  Scenario: Commit with a deleted file removes it from original
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And the cow existing file "existing.txt" is deleted from the sandbox
    And the cow sandbox is committed
    Then the cow commit result should indicate success
    And the cow commit result should have 1 deleted files
    And the cow file "existing.txt" should not exist in the original directory


  Scenario: Commit on a cleaned-up sandbox raises SandboxStateError
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And the cow sandbox is cleaned up
    And the cow sandbox commit is attempted on cleaned-up sandbox
    Then a cow SandboxStateError should be raised

    # --- Rollback ---


  Scenario: Rollback restores the sandbox to original state
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And a cow file "temp.txt" is created in the sandbox with content "temporary"
    And the cow path "temp.txt" is resolved
    And the cow sandbox is rolled back
    Then the cow sandbox should be in the "rolled_back" state
    And the cow file "temp.txt" should not exist in the sandbox


  Scenario: Rollback on a non-active sandbox raises SandboxStateError
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And the cow sandbox rollback is attempted on created sandbox
    Then a cow SandboxStateError should be raised


  Scenario: Rollback from COMMITTED restores original via pre-commit backup
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-rb-committed"
    And the cow existing file "existing.txt" is modified in the sandbox with content "modified-for-commit"
    And the cow sandbox is committed
    Then the cow sandbox should be in the "committed" state
    And the cow file "existing.txt" in the original should have content "modified-for-commit"
    When the cow sandbox is rolled back
    Then the cow sandbox should be in the "rolled_back" state
    And the cow file "existing.txt" in the original should have content "original content"

  Scenario: Rollback from COMMITTED with no changes is a no-op
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-rb-noop"
    And the cow sandbox is committed
    Then the cow sandbox should be in the "committed" state
    When the cow sandbox is rolled back
    Then the cow sandbox should be in the "rolled_back" state
    And the cow file "existing.txt" in the original should have content "original content"

    # --- Cleanup ---


  Scenario: Cleanup removes the sandbox directory
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And the cow sandbox is cleaned up
    Then the cow sandbox should be in the "cleaned_up" state
    And the cow sandbox path should not exist


  Scenario: Cleanup is idempotent
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And the cow sandbox is cleaned up
    And the cow sandbox is cleaned up again
    Then the cow sandbox should be in the "cleaned_up" state

    # --- Protocol properties ---


  Scenario: Sandbox has a unique ULID identifier
    Given a cow test directory is initialised
    When a cow sandbox is instantiated
    Then the cow sandbox_id should be a valid ULID
    And the cow sandbox status should be "pending"
    And the cow sandbox context should be None

    # --- Diff computation ---


  Scenario: Diff detects added, modified, and deleted files
    Given a cow test directory is initialised
    When a cow sandbox is created for plan "plan-001"
    And a cow file "added.txt" is created in the sandbox with content "new"
    And the cow existing file "existing.txt" is modified in the sandbox with content "changed"
    And the cow existing file "to_delete.txt" is deleted from the sandbox
    And the cow sandbox is committed
    Then the cow commit result should have 1 added files
    And the cow commit result should have 1 changed files
    And the cow commit result should have 1 deleted files


  # ============================================================
  # Originally from: cow_sandbox_coverage_boost.feature
  # Feature: Copy-on-write sandbox coverage boost
  # ============================================================

  Scenario: Creating a sandbox on a file (not directory) raises SandboxCreationError
    Given a cowcb temporary file instead of a directory
    When a cowcb sandbox is created on that file for plan "plan-err"
    Then a cowcb SandboxCreationError should be raised
    And the cowcb sandbox status should be "errored"

    # --- get_path with _sandbox_path manually set to None ---


  Scenario: get_path raises SandboxStateError when sandbox_path is None
    Given a cowcb sandbox with status CREATED and sandbox_path None
    When cowcb get_path is called with "file.txt"
    Then a cowcb SandboxStateError should be raised with message "Sandbox path not set"

    # --- commit with _sandbox_path manually set to None ---


  Scenario: commit raises SandboxStateError when sandbox_path is None
    Given a cowcb sandbox with status ACTIVE and sandbox_path None
    When cowcb commit is called
    Then a cowcb SandboxStateError should be raised with message "Sandbox path not set"

    # --- commit triggers OSError during sync ---


  Scenario: commit wraps OSError into SandboxCommitError
    Given a cowcb test directory is initialised
    And a cowcb sandbox is created and activated for plan "plan-oserr"
    When the cowcb original directory is removed before commit
    And cowcb commit is called
    Then a cowcb SandboxCommitError should be raised
    And the cowcb sandbox status should be "errored"

    # --- rollback with _sandbox_path manually set to None ---


  Scenario: rollback raises SandboxStateError when sandbox_path is None
    Given a cowcb sandbox with status ACTIVE and sandbox_path None
    When cowcb rollback is called
    Then a cowcb SandboxStateError should be raised with message "Sandbox path not set"

    # --- rollback triggers OSError during re-copy ---


  Scenario: rollback wraps OSError into SandboxRollbackError
    Given a cowcb test directory is initialised
    And a cowcb sandbox is created and activated for plan "plan-rbfail"
    When the cowcb original directory is removed before rollback
    And cowcb rollback is called
    Then a cowcb SandboxRollbackError should be raised
    And the cowcb sandbox status should be "errored"

    # --- cleanup when _sandbox_path is None ---


  Scenario: cleanup succeeds when sandbox_path is None
    Given a cowcb sandbox with status PENDING and sandbox_path None
    When cowcb cleanup is called
    Then the cowcb sandbox status should be "cleaned_up"

    # --- cleanup when sandbox parent directory does not exist ---


  Scenario: cleanup succeeds when sandbox parent directory is already gone
    Given a cowcb test directory is initialised
    And a cowcb sandbox is created for plan "plan-cleanparent"
    When the cowcb sandbox parent directory is removed
    And cowcb cleanup is called
    Then the cowcb sandbox status should be "cleaned_up"

    # --- commit with dst_dir that needs creation (added file in subdirectory) ---


  Scenario: commit creates missing destination subdirectories for added files
    Given a cowcb test directory is initialised
    And a cowcb sandbox is created and activated for plan "plan-subdir"
    When a cowcb file "newdir/nested/added.txt" is created in the sandbox
    And cowcb commit is called
    Then the cowcb commit should succeed
    And the cowcb file "newdir/nested/added.txt" should exist in the original directory

    # --- commit with file deleted from sandbox that is already missing from original ---


  Scenario: commit handles deleted file not present in original
    Given a cowcb test directory is initialised
    And a cowcb sandbox is created and activated for plan "plan-delmissing"
    When the cowcb file "existing.txt" is deleted from both sandbox and original
    And cowcb commit is called
    Then the cowcb commit should succeed


  # ============================================================
  # Originally from: no_sandbox_coverage.feature
  # Feature: Non-sandboxed resource behaviour
  # ============================================================

  Scenario: A non-sandboxed resource rejects an empty resource identifier during setup
    When a non-sandboxed resource is prepared with an empty identifier
    Then the non-sandboxed setup should fail with a resource identifier error


  Scenario: A non-sandboxed resource rejects an empty path during setup
    When a non-sandboxed resource is prepared with an empty path
    Then the non-sandboxed setup should fail with a path error

    # --- Context before creation ---


  Scenario: A non-sandboxed resource has no context before it is created
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    Then the non-sandboxed resource should have no context

    # --- Creation validation ---


  Scenario: Creating a non-sandboxed resource with an empty plan identifier is rejected
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    When the non-sandboxed resource is created with an empty plan identifier
    Then the non-sandboxed setup should fail with a plan identifier error


  Scenario: Creating a non-sandboxed resource records the correct context
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    When the non-sandboxed resource is created for plan "01JKXYZ1234567890ABCDEFGH"
    Then the non-sandboxed context should reference plan "01JKXYZ1234567890ABCDEFGH"
    And the non-sandboxed context should reference resource "my-api"
    And the non-sandboxed context should use the original path as the sandbox path
    And the non-sandboxed context should have strategy metadata "none"

    # --- Path resolution ---


  Scenario: Resolving a path before the resource is created is rejected
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    When a file path is resolved against the non-sandboxed resource
    Then the non-sandboxed path resolution should be rejected as premature


  Scenario: Resolving a path after cleanup is rejected
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    And the non-sandboxed resource has been created and committed and cleaned up
    When a file path is resolved against the non-sandboxed resource
    Then the non-sandboxed path resolution should be rejected as premature


  Scenario: Resolving a path with directory traversal is rejected
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    And the non-sandboxed resource has been created for plan "01JKXYZ1234567890ABCDEFGH"
    When a file path containing directory traversal is resolved
    Then the non-sandboxed path resolution should be rejected as unsafe


  Scenario: The first path resolution activates a non-sandboxed resource
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    And the non-sandboxed resource has been created for plan "01JKXYZ1234567890ABCDEFGH"
    When the file "src/main.py" is resolved against the non-sandboxed resource
    Then the non-sandboxed resource should be in the "active" state
    And the resolved non-sandboxed path should combine the original path and file


  Scenario: Subsequent path resolutions keep the resource active
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    And the non-sandboxed resource has been created for plan "01JKXYZ1234567890ABCDEFGH"
    And the file "first.py" has been resolved against the non-sandboxed resource
    When the file "second.py" is resolved against the non-sandboxed resource
    Then the non-sandboxed resource should be in the "active" state

    # --- Commit ---


  Scenario: Committing from a pending state is rejected
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    When the non-sandboxed resource is committed
    Then the non-sandboxed commit should be rejected as premature


  Scenario: Committing after cleanup is rejected
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    And the non-sandboxed resource has been created and committed and cleaned up
    When the non-sandboxed resource is committed
    Then the non-sandboxed commit should be rejected as premature


  Scenario: A successful commit returns a positive result
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    And the non-sandboxed resource has been created for plan "01JKXYZ1234567890ABCDEFGH"
    When the non-sandboxed resource is committed
    Then the non-sandboxed commit result should indicate success
    And the non-sandboxed resource should be in the "committed" state


  Scenario: A commit from the active state also succeeds
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    And the non-sandboxed resource has been created for plan "01JKXYZ1234567890ABCDEFGH"
    And the file "src/main.py" has been resolved against the non-sandboxed resource
    When the non-sandboxed resource is committed
    Then the non-sandboxed commit result should indicate success

    # --- Rollback ---


  Scenario: Rollback is always rejected for non-sandboxed resources
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    And the non-sandboxed resource has been created for plan "01JKXYZ1234567890ABCDEFGH"
    When the non-sandboxed resource is rolled back
    Then the non-sandboxed rollback should be rejected as impossible


  Scenario: Rollback is rejected even from the active state
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    And the non-sandboxed resource has been created for plan "01JKXYZ1234567890ABCDEFGH"
    And the file "src/main.py" has been resolved against the non-sandboxed resource
    When the non-sandboxed resource is rolled back
    Then the non-sandboxed rollback should be rejected as impossible

    # --- Cleanup ---


  Scenario: Cleaning up an already cleaned resource is harmless
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    And the non-sandboxed resource has been created and committed and cleaned up
    When the non-sandboxed resource is cleaned up again
    Then the non-sandboxed resource should remain in the "cleaned_up" state


  Scenario: Cleanup after commit transitions to cleaned up
    Given a non-sandboxed resource for "my-api" at "/srv/api"
    And the non-sandboxed resource has been created for plan "01JKXYZ1234567890ABCDEFGH"
    And the non-sandboxed resource has been committed
    When the non-sandboxed resource is cleaned up
    Then the non-sandboxed resource should be in the "cleaned_up" state

    # --- Identity ---


  Scenario: Each non-sandboxed resource receives a unique identifier
    Given a non-sandboxed resource for "api-1" at "/srv/api1"
    And another non-sandboxed resource for "api-2" at "/srv/api2"
    Then the two non-sandboxed resources should have different identifiers


  # ============================================================
  # Originally from: sandbox_checkpoints.feature
  # Feature: Sandbox Checkpoint and Rollback Hooks
  # ============================================================

  Scenario: Checkpoint created before plan execution
    Given a checkpoint manager
    Given a sandbox with files in a temporary directory
    When I create a checkpoint with phase "pre_execute" and plan "plan-001"
    Then a checkpoint should be returned
    And the checkpoint phase should be "pre_execute"
    And the checkpoint plan_id should be "plan-001"
    And the checkpoint sandbox_id should match the sandbox


  Scenario: Checkpoint created after successful execution
    Given a checkpoint manager
    Given a sandbox with files in a temporary directory
    When I create a checkpoint with phase "post_execute" and plan "plan-001"
    And I attach metadata "status" as "success"
    Then a checkpoint should be returned
    And the checkpoint phase should be "post_execute"
    And the checkpoint metadata should contain key "status" with value "success"

    # Rollback


  Scenario: Rollback restores sandbox state on failure
    Given a checkpoint manager
    Given a sandbox with files in a temporary directory
    And a checkpoint exists for the sandbox with phase "pre_execute" and plan "plan-001"
    When I modify the sandbox files
    And I rollback to the checkpoint
    Then the rollback should succeed
    And the sandbox files should match the original state


  Scenario: Rollback returns false when snapshot path is missing
    Given a checkpoint manager
    Given a sandbox with files in a temporary directory
    And a checkpoint with an invalid snapshot path
    When I attempt to rollback to the invalid checkpoint
    Then the rollback should fail

    # Listing


  Scenario: Multiple checkpoints maintain order
    Given a checkpoint manager
    Given a sandbox with files in a temporary directory
    When I create a checkpoint with phase "pre_execute" and plan "plan-001"
    And I create a checkpoint with phase "post_execute" and plan "plan-001"
    And I create a checkpoint with phase "pre_apply" and plan "plan-001"
    Then listing checkpoints for the sandbox should return 3 checkpoints
    And the checkpoints should be in creation order

    # Metadata


  Scenario: Checkpoint metadata captures phase and plan information
    Given a checkpoint manager
    Given a sandbox with files in a temporary directory
    When I create a checkpoint with phase "pre_execute" and plan "plan-002"
    Then the checkpoint should have a valid ULID checkpoint_id
    And the checkpoint should have a created_at timestamp
    And the checkpoint should have a non-empty snapshot_path

    # Deletion


  Scenario: Deleting a checkpoint removes it from the list
    Given a checkpoint manager
    Given a sandbox with files in a temporary directory
    And a checkpoint exists for the sandbox with phase "pre_execute" and plan "plan-001"
    When I delete the checkpoint
    Then the deletion should succeed
    And listing checkpoints for the sandbox should return 0 checkpoints


  Scenario: Deleting a non-existent checkpoint returns false
    Given a checkpoint manager
    When I delete a checkpoint with id "nonexistent-id"
    Then the deletion should fail

    # Plan executor integration (optional checkpoint manager)


  Scenario: PlanExecutor works without checkpoint manager
    Given a checkpoint manager
    Given a plan lifecycle service
    And a plan executor without checkpoint manager
    Then the plan executor checkpoint_manager should be None


  Scenario: PlanExecutor accepts optional checkpoint manager
    Given a checkpoint manager
    Given a plan lifecycle service
    And a plan executor with a checkpoint manager
    Then the plan executor checkpoint_manager should not be None

    # PlanApplyService integration


  Scenario: PlanApplyService works without checkpoint manager
    Given a checkpoint manager
    Given a plan lifecycle service
    And a plan apply service without checkpoint manager
    Then the plan apply service checkpoint_manager should be None


  Scenario: PlanApplyService accepts optional checkpoint manager
    Given a checkpoint manager
    Given a plan lifecycle service
    And a plan apply service with a checkpoint manager
    Then the plan apply service checkpoint_manager should not be None


  # ============================================================
  # Originally from: sandbox_copy_on_write_coverage.feature
  # Feature: CopyOnWriteSandbox uncovered lines and branches
  # ============================================================

  Scenario: create wraps OSError from copytree into SandboxCreationError
    Given a cowcov test directory is initialised
    And shutil.copytree is patched to raise OSError in cowcov
    When a cowcov sandbox create is attempted for plan "plan-oserr"
    Then a cowcov SandboxCreationError should be raised
    And the cowcov error message should contain "Failed to copy directory"
    And the cowcov sandbox status should be "errored"

    # ------------------------------------------------------------------
    # get_path() — second call when already ACTIVE (line 196 False branch)
    # Covers missing branch 196 → 199
    # ------------------------------------------------------------------


  Scenario: get_path called twice stays in ACTIVE status
    Given a cowcov test directory is initialised
    And a cowcov sandbox is created for plan "plan-active"
    When cowcov get_path is called with "file1.txt"
    And cowcov get_path is called with "file2.txt"
    Then the cowcov sandbox status should be "active"
    And both cowcov resolved paths should be valid

    # ------------------------------------------------------------------
    # get_path() — _sandbox_path is None with ACTIVE status
    # Covers line 200 and branch 196 False + 199 True
    # ------------------------------------------------------------------


  Scenario: get_path raises SandboxStateError when ACTIVE but sandbox_path is None
    Given a cowcov sandbox forced to ACTIVE with sandbox_path None
    When cowcov get_path is attempted with "file.txt"
    Then a cowcov SandboxStateError should be raised with message "Sandbox path not set"

    # ------------------------------------------------------------------
    # get_path() — _sandbox_path is None with CREATED status
    # Covers line 200 and branch 199 True
    # ------------------------------------------------------------------


  Scenario: get_path raises SandboxStateError when CREATED but sandbox_path is None
    Given a cowcov sandbox forced to CREATED with sandbox_path None
    When cowcov get_path is attempted with "data.csv"
    Then a cowcov SandboxStateError should be raised with message "Sandbox path not set"

    # ------------------------------------------------------------------
    # commit() — _sandbox_path is None
    # Covers line 230
    # ------------------------------------------------------------------


  Scenario: commit raises SandboxStateError when sandbox_path is None
    Given a cowcov sandbox forced to ACTIVE with sandbox_path None
    When cowcov commit is attempted
    Then a cowcov SandboxStateError should be raised with message "Sandbox path not set"

    # ------------------------------------------------------------------
    # commit() — dst_dir is empty (branch 242 False → 244)
    # Covers missing branch at line 242
    # ------------------------------------------------------------------


  Scenario: commit skips makedirs when dst_dir is empty string
    Given a cowcov test directory is initialised
    And a cowcov sandbox is created for plan "plan-dstdir"
    And cowcov _compute_diff is patched to return a changed root file
    And cowcov os.path.dirname is patched to return empty string
    And cowcov shutil.copy2 is patched to no-op
    When cowcov commit is attempted
    Then the cowcov commit should succeed
    And cowcov os.makedirs should not have been called

    # ------------------------------------------------------------------
    # commit() — deleted file not present in original (branch 249 False)
    # Covers missing branch at line 249 → back to 247
    # ------------------------------------------------------------------


  Scenario: commit handles deletion when file already missing from original
    Given a cowcov test directory is initialised
    And a cowcov sandbox is created and activated for plan "plan-delmiss"
    When the cowcov file "existing.txt" is deleted from both sandbox and original
    And cowcov commit is attempted
    Then the cowcov commit should succeed

    # ------------------------------------------------------------------
    # commit() — OSError during sync wraps into SandboxCommitError
    # Covers lines 252-254
    # ------------------------------------------------------------------


  Scenario: commit wraps OSError during file sync into SandboxCommitError
    Given a cowcov test directory is initialised
    And a cowcov sandbox is created and activated for plan "plan-syncerr"
    And a cowcov file "existing.txt" is modified in the sandbox
    And cowcov shutil.copy2 is patched to raise OSError
    When cowcov commit is attempted
    Then a cowcov SandboxCommitError should be raised
    And the cowcov sandbox status should be "errored"

    # ------------------------------------------------------------------
    # rollback() — _sandbox_path is None
    # Covers line 297
    # ------------------------------------------------------------------


  Scenario: rollback raises SandboxStateError when sandbox_path is None
    Given a cowcov sandbox forced to ACTIVE with sandbox_path None
    When cowcov rollback is attempted
    Then a cowcov SandboxStateError should be raised with message "Sandbox path not set"

    # ------------------------------------------------------------------
    # rollback() — OSError during re-copy wraps into SandboxRollbackError
    # Covers lines 311-313
    # ------------------------------------------------------------------


  Scenario: rollback wraps OSError from copytree into SandboxRollbackError
    Given a cowcov test directory is initialised
    And a cowcov sandbox is created and activated for plan "plan-rbfail"
    And cowcov shutil.copytree is patched to raise OSError for rollback
    When cowcov rollback is attempted
    Then a cowcov SandboxRollbackError should be raised
    And the cowcov sandbox status should be "errored"

    # ------------------------------------------------------------------
    # cleanup() — _sandbox_path is None (branch 341 False → 347)
    # Covers missing branch at line 341
    # ------------------------------------------------------------------


  Scenario: cleanup succeeds when sandbox_path was never set
    Given a cowcov sandbox in PENDING state with sandbox_path None
    When cowcov cleanup is called
    Then the cowcov sandbox status should be "cleaned_up"

    # ------------------------------------------------------------------
    # cleanup() — parent directory already gone (branch 344 False → 347)
    # Covers missing branch at line 344
    # ------------------------------------------------------------------


  Scenario: cleanup succeeds when parent directory is already removed
    Given a cowcov test directory is initialised
    And a cowcov sandbox is created for plan "plan-cleanmissing"
    And the cowcov sandbox parent directory is manually removed
    When cowcov cleanup is called
    Then the cowcov sandbox status should be "cleaned_up"


  # ============================================================
  # Originally from: sandbox_factory_coverage.feature
  # Feature: Sandbox factory strategy routing
  # ============================================================

  Scenario: The factory rejects an empty resource identifier
    Given the sandbox factory is available
    When a sandbox is requested with an empty resource identifier
    Then the factory should reject the request due to a missing resource identifier


  Scenario: The factory rejects an empty resource path
    Given the sandbox factory is available
    When a sandbox is requested with an empty resource path
    Then the factory should reject the request due to a missing resource path

    # --- None strategy (passthrough) ---


  Scenario: The none strategy produces a passthrough sandbox
    Given the sandbox factory is available
    When a sandbox is requested for resource "web-api" at "/srv/api" using the none strategy
    Then the factory should produce a passthrough sandbox
    And the produced sandbox should be in the pending state

    # --- Implemented strategies ---


  Scenario: The git worktree strategy produces a git worktree sandbox
    Given the sandbox factory is available
    When a sandbox is requested for resource "repo-1" at "/src/repo" using the git worktree strategy
    Then the factory should produce a git worktree sandbox
    And the produced git worktree sandbox should be in the pending state


  Scenario: The copy-on-write strategy produces a copy-on-write sandbox
    Given the sandbox factory is available
    When a sandbox is requested for resource "docs" at "/srv/docs" using the copy-on-write strategy
    Then the factory should produce a copy-on-write sandbox
    And the produced copy-on-write sandbox should be in the pending state

    # --- Unimplemented strategies ---


  Scenario: The transaction rollback strategy creates a TransactionSandbox
    Given the sandbox factory is available
    When a sandbox is requested for resource "db-main" at "postgres://db/main" using the transaction rollback strategy
    Then the factory should produce a transaction sandbox in the pending state


  Scenario: The snapshot strategy is not yet available
    Given the sandbox factory is available
    When a sandbox is requested for resource "corpus" at "/srv/corpus" using the snapshot strategy
    Then the factory should indicate the strategy is not yet implemented

    # --- Unknown strategy ---


  Scenario: An unrecognised strategy name is rejected
    Given the sandbox factory is available
    When a sandbox is requested for resource "misc" at "/tmp/misc" using an unrecognised strategy
    Then the factory should reject the request due to an unknown strategy


  Scenario: The overlay strategy is supported
    Given the sandbox factory is available
    When the factory is asked whether the overlay strategy is supported
    Then the factory should confirm the strategy is supported


  Scenario: The versioning strategy is now unknown and rejected
    Given the sandbox factory is available
    When a sandbox is requested for resource "corpus" at "/srv/corpus" using the versioning strategy
    Then the factory should reject the request due to an unknown strategy

    # --- Support checks ---


  Scenario: The none strategy is reported as supported
    Given the sandbox factory is available
    When the factory is asked whether the none strategy is supported
    Then the factory should confirm the strategy is supported


  Scenario: The git worktree strategy is reported as supported
    Given the sandbox factory is available
    When the factory is asked whether the git worktree strategy is supported
    Then the factory should confirm the strategy is supported


  Scenario: The copy-on-write strategy is reported as supported
    Given the sandbox factory is available
    When the factory is asked whether the copy-on-write strategy is supported
    Then the factory should confirm the strategy is supported


  Scenario: An unrecognised strategy is reported as unsupported
    Given the sandbox factory is available
    When the factory is asked whether an unrecognised strategy is supported
    Then the factory should indicate the strategy is not supported

    # --- Resource type strategy lookup (spec-aligned types) ---


  Scenario: A git-checkout resource supports worktree, copy-on-write, and none
    Given the sandbox factory is available
    When the compatible strategies for a "git-checkout" resource are queried
    Then the factory should return git worktree, copy-on-write, and none as compatible


  Scenario: A git resource supports only the none strategy
    Given the sandbox factory is available
    When the compatible strategies for a "git" resource are queried
    Then the factory should return only the none strategy as compatible


  Scenario: A fs-mount resource supports copy-on-write, overlay, and none
    Given the sandbox factory is available
    When the compatible strategies for a "fs-mount" resource are queried
    Then the factory should return copy-on-write, overlay, and none as compatible


  Scenario: A fs-directory resource supports copy-on-write, overlay, and none
    Given the sandbox factory is available
    When the compatible strategies for a "fs-directory" resource are queried
    Then the factory should return copy-on-write, overlay, and none as compatible


  Scenario: A fs-file resource supports copy-on-write, overlay, and none
    Given the sandbox factory is available
    When the compatible strategies for a "fs-file" resource are queried
    Then the factory should return copy-on-write, overlay, and none as compatible


  Scenario: An api_endpoint resource supports only the none strategy
    Given the sandbox factory is available
    When the compatible strategies for an "api_endpoint" resource are queried
    Then the factory should return only the none strategy as compatible


  Scenario: An unknown resource type falls back to the none strategy
    Given the sandbox factory is available
    When the compatible strategies for an unknown resource type are queried
    Then the factory should return only the none strategy as compatible
