Files
cleveragents-core/features/overlay_sandbox.feature
brent.edwards 8a87262f86 feat(sandbox): implement overlay filesystem sandbox strategy (#994)
## 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>
2026-03-21 03:14:40 +00:00

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