8a87262f86
CI / build (push) Successful in 17s
CI / lint (push) Successful in 3m41s
CI / quality (push) Successful in 3m47s
CI / unit_tests (push) Successful in 3m58s
CI / typecheck (push) Successful in 4m17s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m22s
CI / docker (push) Successful in 1m22s
CI / e2e_tests (push) Successful in 8m57s
CI / coverage (push) Successful in 11m16s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 19m45s
CI / integration_tests (push) Failing after 20m53s
## Summary Implement the overlay filesystem sandbox strategy with OverlayFS support and userspace fallback. ### Implementation **`OverlaySandbox`** (`infrastructure/sandbox/overlay.py`, 497 lines): - Detects OverlayFS availability at runtime via `/proc/filesystems` + `os.geteuid() == 0` check - **Real OverlayFS mode** (requires root): creates upper/work/merged dirs, mounts overlay filesystem, captures writes in upper layer - **Userspace fallback** (default in CI/containers): `shutil.copytree` the original into merged dir, tracks changes via `filecmp` diff on commit - `create()`: sets up directory structure, mounts if available - `commit()`: copies changed/added files from overlay to original, removes deleted files - `rollback()`: unmounts (or removes) merged, recreates from scratch - `cleanup()`: unmounts, removes all temp dirs, idempotent ### Domain Model Updates - Added `OVERLAY = "overlay"` to `SandboxStrategy` enum in both `resource_type.py` and `resource.py` - Added `STRATEGY_OVERLAY` to `SandboxFactory`, registered for `fs-mount`, `fs-directory`, `fs-file` resources ### Tests - **22 Behave scenarios**: full lifecycle (create/commit/rollback/cleanup), status transitions, path traversal guard, fallback detection, error handling - **6 Robot integration tests**: end-to-end overlay sandbox operations ### Quality Gates | Session | Result | |---|---| | `nox -s lint` | PASS | | `nox -s typecheck` | PASS (0 errors) | | `nox -s unit_tests` | PASS (10,917 scenarios) | | `nox -s coverage_report` | 97% (>= 97%) | Closes #880 Reviewed-on: #994 Co-authored-by: Brent Edwards <brent.edwards@cleverthis.com> Co-committed-by: Brent Edwards <brent.edwards@cleverthis.com>
842 lines
35 KiB
Gherkin
842 lines
35 KiB
Gherkin
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
|
|
|
|
# --- 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
|
|
|