Files
cleveragents-core/features/extensibility/custom_sandbox_strategy.feature
freemo 2688c85769 feat(extensibility): implement Custom Sandbox Strategy Registration via SandboxStrategy Protocol
Implement SandboxStrategyProtocol, a 9-method @runtime_checkable Protocol enabling
third-party sandbox strategy registration. Includes:

- SandboxStrategyProtocol with create/read/write/diff/commit/rollback/checkpoint/
  restore_checkpoint/cleanup methods
- SandboxRef (frozen dataclass) and DiffView/DiffEntry (Pydantic models)
- SandboxStrategyRegistry with config-driven registration, Protocol validation,
  thread safety, and clear/list/has operations
- BuiltInSandboxStrategyAdapter wrapping existing Sandbox implementations to conform
  to the new Protocol
- CustomStrategyConfig for YAML/dict-based strategy registration
- SandboxFactory integration with custom_registry parameter,
  has_custom_strategy() and get_custom_strategy_class()
- 25 Behave BDD scenarios (85 steps) covering protocol, registry, adapter, config,
  and factory integration
- 8 Robot Framework integration tests with real filesystem operations
- ASV benchmarks for registry and adapter operations
- Developer documentation

ISSUES CLOSED: #586
2026-03-16 22:50:27 +00:00

194 lines
7.9 KiB
Gherkin

@extensibility @custom_sandbox_strategy
Feature: Custom Sandbox Strategy Registration via SandboxStrategy Protocol
As a CleverAgents developer
I want to register custom sandbox strategies via configuration
So that specialized resource types can use domain-specific isolation
# ---------------------------------------------------------------------------
# SandboxRef model
# ---------------------------------------------------------------------------
@sandbox_ref
Scenario: SandboxRef is a frozen dataclass
Given a SandboxRef with sandbox_id "sb-001" and plan_id "plan-001"
Then the SandboxRef sandbox_id should be "sb-001"
And the SandboxRef plan_id should be "plan-001"
And the SandboxRef should be immutable
@sandbox_ref
Scenario: SandboxRef carries metadata
Given a SandboxRef with metadata key "backend" value "redis"
Then the SandboxRef metadata should contain key "backend"
# ---------------------------------------------------------------------------
# DiffView / DiffEntry models
# ---------------------------------------------------------------------------
@diff_view
Scenario: DiffEntry creation with required fields
Given a DiffEntry with path "src/main.py" and operation "modified"
Then the DiffEntry path should be "src/main.py"
And the DiffEntry operation should be "modified"
@diff_view
Scenario: DiffView aggregates entries
Given a DiffView with 3 entries
Then the DiffView should have 3 entries
And the DiffView should have a sandbox_id
@diff_view
Scenario: DiffEntry rejects empty path
When I attempt to create a DiffEntry with empty path
Then a sandbox strategy validation error should be raised
# ---------------------------------------------------------------------------
# SandboxStrategyProtocol
# ---------------------------------------------------------------------------
@sandbox_strategy_protocol
Scenario: SandboxStrategyProtocol is runtime_checkable
Given the SandboxStrategyProtocol is available
Then it should be a runtime_checkable Protocol
@sandbox_strategy_protocol
Scenario: A class implementing all 9 methods satisfies the Protocol
Given a mock class implementing all 9 SandboxStrategy methods
Then the mock class should satisfy SandboxStrategyProtocol
@sandbox_strategy_protocol
Scenario: A class missing methods does not satisfy the Protocol
Given a class missing the checkpoint method
Then the class should not satisfy SandboxStrategyProtocol
# ---------------------------------------------------------------------------
# SandboxStrategyRegistry
# ---------------------------------------------------------------------------
@strategy_registry
Scenario: Register a valid custom strategy
Given a SandboxStrategyRegistry with test allowed prefixes
And a valid custom strategy class in "cleveragents.infrastructure.sandbox.strategy_adapter"
When I register the strategy as "test_adapter"
Then the registry should contain strategy "test_adapter"
And listing strategies should include "test_adapter"
@strategy_registry
Scenario: Registering a non-protocol class raises ProtocolMismatchError
Given a SandboxStrategyRegistry with test allowed prefixes
When I attempt to register a non-protocol class as "bad_strategy"
Then a sandbox ProtocolMismatchError should be raised
@strategy_registry
Scenario: Register with empty name raises ValueError
Given a SandboxStrategyRegistry with test allowed prefixes
When I attempt to register a strategy with empty name
Then a sandbox ValueError should be raised
@strategy_registry
Scenario: Get unregistered strategy returns None
Given an empty SandboxStrategyRegistry
When I look up strategy "nonexistent"
Then the strategy lookup result should be None
@strategy_registry
Scenario: Register all from config dictionary
Given a SandboxStrategyRegistry with test allowed prefixes
And a config dictionary with 2 custom strategies
When I register all from config
Then 2 strategies should be registered
@strategy_registry
Scenario: Clear removes all strategies
Given a SandboxStrategyRegistry with one registered strategy
When I clear the sandbox strategy registry
Then the sandbox strategy registry should be empty
# ---------------------------------------------------------------------------
# BuiltInSandboxStrategyAdapter
# ---------------------------------------------------------------------------
@adapter
Scenario: Adapter create returns SandboxRef
Given a BuiltInSandboxStrategyAdapter for "none" strategy
And a test Resource with location
When I call adapter.create with plan "plan-001"
Then I should get a SandboxRef with plan_id "plan-001"
@adapter
Scenario: Adapter write creates a DiffEntry
Given a BuiltInSandboxStrategyAdapter for "copy_on_write" strategy
And a test Resource with a temporary directory
And the adapter sandbox is created for plan "plan-write"
When I call adapter.write with path "test.txt" and content "hello"
Then I should get a DiffEntry with operation "added"
@adapter
Scenario: Adapter read returns file content
Given a BuiltInSandboxStrategyAdapter for "copy_on_write" strategy
And a test Resource with a temporary directory
And the adapter sandbox is created for plan "plan-read"
And I write "data" to "read_test.txt" via the adapter
When I call adapter.read for "read_test.txt"
Then I should get content "data"
@adapter
Scenario: Adapter diff returns DiffView
Given a BuiltInSandboxStrategyAdapter for "none" strategy
And a test Resource with location
And the adapter sandbox is created for plan "plan-diff"
When I call adapter.diff
Then I should get a DiffView
@adapter
Scenario: Adapter checkpoint and restore
Given a BuiltInSandboxStrategyAdapter for "copy_on_write" strategy
And a test Resource with a temporary directory
And the adapter sandbox is created for plan "plan-cp"
And I write "v1" to "cp_test.txt" via the adapter
And I checkpoint as "cp-1"
And I write "v2" to "cp_test.txt" via the adapter
When I restore checkpoint "cp-1"
Then reading "cp_test.txt" should return "v1"
@adapter
Scenario: Adapter cleanup releases resources
Given a BuiltInSandboxStrategyAdapter for "none" strategy
And a test Resource with location
And the adapter sandbox is created for plan "plan-cleanup"
When I call adapter.cleanup
Then the adapter should have no tracked sandboxes
# ---------------------------------------------------------------------------
# CustomStrategyConfig
# ---------------------------------------------------------------------------
@config
Scenario: CustomStrategyConfig rejects empty name
When I attempt to create a CustomStrategyConfig with empty name
Then a sandbox ValueError should be raised
@config
Scenario: CustomStrategyConfig rejects empty module
When I attempt to create a CustomStrategyConfig with empty module
Then a sandbox ValueError should be raised
@config
Scenario: CustomStrategyConfig rejects empty class_name
When I attempt to create a CustomStrategyConfig with empty class_name
Then a sandbox ValueError should be raised
# ---------------------------------------------------------------------------
# Factory custom strategy integration
# ---------------------------------------------------------------------------
@factory_integration
Scenario: Factory reports custom strategy availability
Given a SandboxFactory with a custom registry containing "my_custom"
Then the factory should report "my_custom" as a custom strategy
And the factory should not report "nonexistent" as a custom strategy
@factory_integration
Scenario: Factory without registry reports no custom strategies
Given a SandboxFactory without a custom registry
Then the factory should not report "anything" as a custom strategy