forked from cleveragents/cleveragents-core
8a87262f86
## 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: cleveragents/cleveragents-core#994 Co-authored-by: Brent Edwards <brent.edwards@cleverthis.com> Co-committed-by: Brent Edwards <brent.edwards@cleverthis.com>
206 lines
9.2 KiB
Gherkin
206 lines
9.2 KiB
Gherkin
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
|
|
|
|
# --- 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
|