Feature: Overlay filesystem sandbox lifecycle As a developer I want a sandbox that isolates plan changes using overlay filesystem semantics So that changes are captured in an upper layer and merged on commit # --- Creation --- Scenario: Create an overlay sandbox Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" Then the ovl sandbox should be in the "created" state And the ovl sandbox context should reference plan "plan-001" And the ovl sandbox context should have strategy metadata "overlay" And the ovl sandbox merged path should exist Scenario: Creating an overlay sandbox with empty plan_id raises ValueError Given an ovl test directory is initialised When an ovl sandbox is created with empty plan_id Then an ovl ValueError should be raised with message "plan_id cannot be empty" Scenario: Creating an overlay sandbox with empty resource_id raises ValueError Given an ovl test directory is initialised When an ovl sandbox is prepared with empty resource_id Then an ovl ValueError should be raised with message "resource_id cannot be empty" Scenario: Creating an overlay sandbox with empty original_path raises ValueError Given an ovl test directory is initialised When an ovl sandbox is prepared with empty original_path Then an ovl ValueError should be raised with message "original_path cannot be empty" Scenario: Creating an overlay sandbox on a non-existent directory raises SandboxCreationError Given an ovl test directory is initialised Given an ovl non-existent directory When an ovl sandbox is created on the non-existent directory for plan "plan-001" Then an ovl SandboxCreationError should be raised # --- Directory structure --- Scenario: Overlay sandbox creates correct directory structure Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" Then the ovl sandbox should have upper, work, and merged directories # --- Path resolution --- Scenario: Resolve a path in the overlay merged directory Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And the ovl path "data/file.txt" is resolved Then the ovl resolved path should be inside the merged directory And the ovl sandbox should be in the "active" state Scenario: Path traversal is rejected Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And the ovl path "../etc/passwd" is resolved Then an ovl ValueError should be raised with message "Path traversal not allowed" Scenario: Resolving a path on a cleaned-up sandbox raises SandboxStateError Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And the ovl sandbox is cleaned up And the ovl path "file.txt" is resolved on a cleaned-up sandbox Then an ovl SandboxStateError should be raised # --- Commit --- Scenario: Commit with no changes produces empty result Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And the ovl sandbox is committed Then the ovl commit result should indicate success And the ovl commit result should have 0 changed files And the ovl commit result should have 0 added files And the ovl commit result should have 0 deleted files Scenario: Commit with a new file syncs it to original Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And an ovl file "new_file.txt" is created in the sandbox with content "hello world" And the ovl sandbox is committed Then the ovl commit result should indicate success And the ovl commit result should have 1 added files And the ovl file "new_file.txt" should exist in the original directory with content "hello world" Scenario: Commit with a modified file syncs the change Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And the ovl existing file "existing.txt" is modified in the sandbox with content "updated" And the ovl sandbox is committed Then the ovl commit result should indicate success And the ovl commit result should have 1 changed files And the ovl file "existing.txt" in the original should have content "updated" Scenario: Commit with a deleted file removes it from original Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And the ovl existing file "existing.txt" is deleted from the sandbox And the ovl sandbox is committed Then the ovl commit result should indicate success And the ovl commit result should have 1 deleted files And the ovl file "existing.txt" should not exist in the original directory Scenario: Commit on a cleaned-up sandbox raises SandboxStateError Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And the ovl sandbox is cleaned up And the ovl sandbox commit is attempted on cleaned-up sandbox Then an ovl SandboxStateError should be raised # --- Rollback --- Scenario: Rollback restores the sandbox to original state Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And an ovl file "temp.txt" is created in the sandbox with content "temporary" And the ovl path "temp.txt" is resolved And the ovl sandbox is rolled back Then the ovl sandbox should be in the "rolled_back" state And the ovl file "temp.txt" should not exist in the sandbox Scenario: Rollback on a non-active sandbox raises SandboxStateError Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And the ovl sandbox rollback is attempted on created sandbox Then an ovl SandboxStateError should be raised # --- Cleanup --- Scenario: Cleanup removes the overlay directory Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And the ovl sandbox is cleaned up Then the ovl sandbox should be in the "cleaned_up" state And the ovl sandbox base path should not exist Scenario: Cleanup is idempotent Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" And the ovl sandbox is cleaned up And the ovl sandbox is cleaned up again Then the ovl sandbox should be in the "cleaned_up" state # --- Protocol properties --- Scenario: Overlay sandbox has a unique ULID identifier Given an ovl test directory is initialised When an ovl sandbox is instantiated Then the ovl sandbox_id should be a valid ULID And the ovl sandbox status should be "pending" And the ovl sandbox context should be None # --- Fallback mode --- Scenario: Fallback mode works when OverlayFS is unavailable Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-fallback" Then the ovl sandbox should use userspace fallback # --- Coverage boost: edge cases --- Scenario: Commit with a message stores it in metadata Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-msg" And an ovl file "new.txt" is created in the sandbox with content "data" And the ovl sandbox is committed with message "snapshot before deploy" Then the ovl commit result should indicate success And the ovl commit metadata should contain message "snapshot before deploy" Scenario: get_path raises SandboxStateError when merged_dir is None Given an ovl sandbox with merged_dir forced to None When ovl get_path is called with "file.txt" expecting an error Then an ovl SandboxStateError should be raised with message "merged directory not set" Scenario: Commit wraps OSError into SandboxCommitError Given an ovl test directory is initialised And an ovl sandbox is created and activated for plan "plan-commiterr" And ovl shutil.copy2 is patched to raise OSError When the ovl sandbox commit is attempted Then an ovl SandboxCommitError should be raised And the ovl sandbox should be in the "errored" state Scenario: Rollback wraps OSError into SandboxRollbackError Given an ovl test directory is initialised And an ovl sandbox is created and activated for plan "plan-rberr" And ovl shutil.rmtree is patched to raise OSError for rollback When the ovl sandbox rollback is attempted Then an ovl SandboxRollbackError should be raised And the ovl sandbox should be in the "errored" state Scenario: Create wraps OSError into SandboxCreationError Given an ovl test directory is initialised And ovl shutil.copytree is patched to raise OSError When an ovl sandbox create is attempted for plan "plan-createerr" Then an ovl SandboxCreationError should be raised And the ovl sandbox should be in the "errored" state # --- Atomic rollback from COMMITTED --- Scenario: Rollback from COMMITTED resets the merged directory Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-rb-committed" And an ovl file "changed.txt" is created in the sandbox with content "modified" And the ovl sandbox is committed Then the ovl sandbox should be in the "committed" state When the ovl sandbox is rolled back from committed Then the ovl sandbox should be in the "rolled_back" state And the ovl original file "existing.txt" should contain "original content" And the ovl merged directory should not contain stale file "changed.txt" Scenario: Rollback from COMMITTED with no changes is a no-op Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-rb-noop" And the ovl sandbox is committed Then the ovl sandbox should be in the "committed" state When the ovl sandbox is rolled back from committed Then the ovl sandbox should be in the "rolled_back" state And the ovl original file "existing.txt" should contain "original content" # --- Status transitions --- Scenario: Status transitions follow protocol Given an ovl test directory is initialised When an ovl sandbox is created for plan "plan-001" Then the ovl sandbox should be in the "created" state When the ovl path "existing.txt" is resolved Then the ovl sandbox should be in the "active" state