forked from cleveragents/cleveragents-core
2688c85769
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
194 lines
7.9 KiB
Gherkin
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
|