Feature: Copy-on-write sandbox lifecycle As a developer I want a sandbox that isolates plan changes in a directory copy So that the original directory is unmodified until changes are committed Background: Given a cow test directory is initialised # --- Creation --- Scenario: Create a copy-on-write sandbox 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 # --- Cleanup --- Scenario: Cleanup removes the sandbox directory 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 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 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 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