Feature: Sandbox Manager Lifecycle As an execution engine I want a manager that tracks sandbox instances across plan executions So that resources are isolated during execution and cleaned up reliably Background: Given a sandbox factory instance And a sandbox manager with the factory # Initialisation Scenario: A new manager starts with no tracked sandboxes Then the manager should have no active sandboxes And the manager cleanup_on_exit flag should be true Scenario: A manager can be created without automatic exit cleanup Given a sandbox manager with cleanup_on_exit disabled Then the manager cleanup_on_exit flag should be false Scenario: Creating a manager without a factory is rejected When I create a sandbox manager with None factory Then a sandbox ValueError should be raised with message "factory cannot be None" # Lazy sandbox creation Scenario: Requesting a sandbox for a plan creates and tracks a new sandbox When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" Then a sandbox should be returned And the sandbox should have status "created" And the sandbox should be tracked for plan "plan-001" Scenario: Requesting the same plan-resource pair returns the existing sandbox Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" Then the same sandbox instance should be returned Scenario: An active sandbox is reused when requested again Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is activated When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" Then the same sandbox instance should be returned Scenario: A committed sandbox is replaced by a fresh instance on next request Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is committed When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" Then a different sandbox instance should be returned Scenario: A cleaned-up sandbox is replaced by a fresh instance on next request Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is cleaned up When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" Then a different sandbox instance should be returned Scenario: A rolled-back sandbox is still usable and returned on next request Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is rolled back When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" Then the same sandbox instance should be returned Scenario: An errored sandbox is replaced by a fresh instance on next request Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is in errored state When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" Then a different sandbox instance should be returned # Argument validation on sandbox creation Scenario: A sandbox request without a plan identifier is rejected When I get or create a sandbox with empty plan_id Then a sandbox ValueError should be raised with message "plan_id cannot be empty" Scenario: A sandbox request without a resource identifier is rejected When I get or create a sandbox with empty resource_id Then a sandbox ValueError should be raised with message "resource_id cannot be empty" Scenario: A sandbox request without an original path is rejected When I get or create a sandbox with empty original_path Then a sandbox ValueError should be raised with message "original_path cannot be empty" # Multi-resource and multi-plan tracking Scenario: Multiple resources under the same plan are tracked independently When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And I get or create a sandbox for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none" Then 2 sandboxes should be tracked for plan "plan-001" Scenario: Sandboxes from different plans do not interfere with each other When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And I get or create a sandbox for plan "plan-002" resource "res-001" path "/tmp/repo2" strategy "none" Then 1 sandboxes should be tracked for plan "plan-001" And 1 sandboxes should be tracked for plan "plan-002" # Looking up an existing sandbox Scenario: An existing sandbox can be retrieved by plan and resource Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" When I get sandbox for plan "plan-001" resource "res-001" Then a sandbox should be returned Scenario: Looking up a sandbox for an unknown plan returns nothing When I get sandbox for plan "plan-999" resource "res-999" Then the sandbox result should be None Scenario: Looking up a sandbox for a known plan but unknown resource returns nothing Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none" When I get sandbox for plan "plan-001" resource "res-999" Then the sandbox result should be None # Listing sandboxes for a plan Scenario: All sandboxes for a plan can be listed Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none" When I list sandboxes for plan "plan-001" Then the sandbox list should contain 2 sandboxes Scenario: Listing sandboxes for a plan with none returns an empty list When I list sandboxes for plan "plan-999" Then the sandbox list should contain 0 sandboxes # Batch commit (atomic) Scenario: All active sandboxes for a plan are committed together Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is activated And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none" And the sandbox for plan "plan-001" resource "res-002" is activated When I commit all sandboxes for plan "plan-001" Then 2 commit results should be returned And all commit results should be successful Scenario: Batch commit skips sandboxes that are already committed Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is committed When I commit all sandboxes for plan "plan-001" Then 0 commit results should be returned Scenario: Batch commit on a plan with no sandboxes returns an empty result set When I commit all sandboxes for plan "plan-999" Then 0 commit results should be returned Scenario: Batch commit without a plan identifier is rejected When I commit all sandboxes with empty plan_id Then a sandbox ValueError should be raised with message "plan_id cannot be empty" Scenario: A single sandbox failure in atomic commit returns an error result Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is activated And the sandbox for plan "plan-001" resource "res-001" will fail on commit When I commit all sandboxes for plan "plan-001" Then 1 commit results should be returned And the first commit result should have success false Scenario: Newly created sandboxes can be committed before activation Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" When I commit all sandboxes for plan "plan-001" Then 1 commit results should be returned And all commit results should be successful # Atomic commit — rollback on failure Scenario: Atomic commit rolls back already-committed sandboxes when one fails Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is replaced with a committable mock And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none" And the sandbox for plan "plan-001" resource "res-002" is activated And the sandbox for plan "plan-001" resource "res-002" will fail on commit When I commit all sandboxes for plan "plan-001" Then 1 commit results should be returned And the first commit result should have success false And the first commit result error should mention the failed sandbox And the first commit result metadata should list rolled back sandboxes Scenario: Atomic commit error reports the sandbox that failed Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is activated And the sandbox for plan "plan-001" resource "res-001" will fail on commit When I commit all sandboxes for plan "plan-001" Then 1 commit results should be returned And the first commit result should have success false And the first commit result error should contain "Atomic commit failed" Scenario: Atomic commit tolerates rollback errors during recovery Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" will succeed commit then fail rollback And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none" And the sandbox for plan "plan-001" resource "res-002" is activated And the sandbox for plan "plan-001" resource "res-002" will fail on commit When I commit all sandboxes for plan "plan-001" Then 1 commit results should be returned And the first commit result should have success false And the first commit result metadata should list failed rollback sandboxes Scenario: Atomic commit succeeds rollback for a no-change sandbox Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is replaced with a no-change committable mock And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none" And the sandbox for plan "plan-001" resource "res-002" is activated And the sandbox for plan "plan-001" resource "res-002" will fail on commit When I commit all sandboxes for plan "plan-001" Then 1 commit results should be returned And the first commit result should have success false And the no-change sandbox rollback should have succeeded for plan "plan-001" resource "res-001" Scenario: Atomic commit wraps non-SandboxError in AtomicCommitError after rollback Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is replaced with a committable mock And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none" And the sandbox for plan "plan-001" resource "res-002" is activated And the sandbox for plan "plan-001" resource "res-002" will raise RuntimeError on commit When I commit all sandboxes for plan "plan-001" expecting a non-sandbox error Then an AtomicCommitError should have been raised with RuntimeError as cause And the AtomicCommitError should carry rollback metadata And the committable mock for plan "plan-001" resource "res-001" should have been rolled back Scenario: Atomic commit rolls back in reverse order Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is replaced with a committable mock And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none" And the sandbox for plan "plan-001" resource "res-002" is replaced with a committable mock And a sandbox exists for plan "plan-001" resource "res-003" path "/tmp/repo3" strategy "none" And the sandbox for plan "plan-001" resource "res-003" is activated And the sandbox for plan "plan-001" resource "res-003" will fail on commit When I commit all sandboxes for plan "plan-001" Then 1 commit results should be returned And the first commit result should have success false And the rollback order should be reverse of commit order for plan "plan-001" Scenario: Atomic commit orders rollbackable sandboxes before non-rollbackable Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is replaced with a committable mock And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none" And the sandbox for plan "plan-001" resource "res-002" is replaced with a non-rollbackable mock And a sandbox exists for plan "plan-001" resource "res-003" path "/tmp/repo3" strategy "none" And the sandbox for plan "plan-001" resource "res-003" is activated And the sandbox for plan "plan-001" resource "res-003" will fail on commit When I commit all sandboxes for plan "plan-001" Then 1 commit results should be returned And the first commit result should have success false And the non-rollbackable mock for plan "plan-001" resource "res-002" should not have been committed # Batch rollback Scenario: All active sandboxes for a plan are rolled back together Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is activated When I rollback all sandboxes for plan "plan-001" Then no sandbox error should be raised Scenario: Batch rollback skips sandboxes that are not active Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" When I rollback all sandboxes for plan "plan-001" Then no sandbox error should be raised Scenario: Batch rollback on a plan with no sandboxes succeeds silently When I rollback all sandboxes for plan "plan-999" Then no sandbox error should be raised Scenario: Batch rollback without a plan identifier is rejected When I rollback all sandboxes with empty plan_id Then a sandbox ValueError should be raised with message "plan_id cannot be empty" Scenario: A single sandbox failure during batch rollback does not abort the others Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is activated And the sandbox for plan "plan-001" resource "res-001" will fail on rollback When I rollback all sandboxes for plan "plan-001" Then no sandbox error should be raised # Batch cleanup Scenario: All sandboxes for a plan are cleaned up and the plan is untracked Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none" When I cleanup all sandboxes for plan "plan-001" Then no sandbox error should be raised And plan "plan-001" should no longer be tracked Scenario: Batch cleanup without a plan identifier is rejected When I cleanup all sandboxes with empty plan_id Then a sandbox ValueError should be raised with message "plan_id cannot be empty" Scenario: Batch cleanup on a plan with no sandboxes succeeds silently When I cleanup all sandboxes for plan "plan-999" Then no sandbox error should be raised Scenario: A single sandbox failure during batch cleanup does not prevent untracking Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" will fail on cleanup When I cleanup all sandboxes for plan "plan-001" Then no sandbox error should be raised And plan "plan-001" should no longer be tracked # Abandoned sandbox cleanup Scenario: Errored sandboxes are cleaned up as abandoned Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is in errored state When I cleanup abandoned sandboxes Then 1 abandoned sandboxes should be cleaned up Scenario: Committed sandboxes are cleaned up as abandoned Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is committed When I cleanup abandoned sandboxes Then 1 abandoned sandboxes should be cleaned up Scenario: Rolled-back sandboxes are cleaned up as abandoned Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is rolled back When I cleanup abandoned sandboxes Then 1 abandoned sandboxes should be cleaned up Scenario: Active sandboxes are not considered abandoned Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is activated When I cleanup abandoned sandboxes Then 0 abandoned sandboxes should be cleaned up Scenario: A plan entry is removed once all of its sandboxes have been cleaned up Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is committed When I cleanup abandoned sandboxes Then 1 abandoned sandboxes should be cleaned up And plan "plan-001" should no longer be tracked Scenario: Cleanup errors on individual abandoned sandboxes are contained Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is in errored state And the sandbox for plan "plan-001" resource "res-001" will fail on cleanup When I cleanup abandoned sandboxes Then 0 abandoned sandboxes should be cleaned up Scenario: Abandoned sandbox cleanup operates across all tracked plans Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" is committed And a sandbox exists for plan "plan-002" resource "res-002" path "/tmp/repo2" strategy "none" And the sandbox for plan "plan-002" resource "res-002" is in errored state When I cleanup abandoned sandboxes Then 2 abandoned sandboxes should be cleaned up # Graceful process exit Scenario: The exit handler cleans up all remaining sandboxes Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And a sandbox exists for plan "plan-002" resource "res-002" path "/tmp/repo2" strategy "none" When the atexit cleanup handler runs Then plan "plan-001" should no longer be tracked And plan "plan-002" should no longer be tracked Scenario: The exit handler tolerates individual sandbox cleanup failures Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And the sandbox for plan "plan-001" resource "res-001" will fail on cleanup When the atexit cleanup handler runs Then no sandbox error should be raised Scenario: The exit handler swallows exceptions from the cleanup subsystem Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none" And cleanup_all is patched to raise a RuntimeError When the atexit cleanup handler runs Then no sandbox error should be raised