Files
cleveragents-core/features/consolidated_resource.feature
freemo e201aa07c9
CI / push-validation (pull_request) Successful in 25s
CI / helm (pull_request) Successful in 28s
CI / build (pull_request) Successful in 28s
CI / lint (pull_request) Successful in 53s
CI / quality (pull_request) Successful in 52s
CI / typecheck (pull_request) Successful in 54s
CI / security (pull_request) Successful in 1m27s
CI / unit_tests (pull_request) Successful in 8m0s
CI / docker (pull_request) Successful in 1m54s
CI / coverage (pull_request) Successful in 15m56s
CI / integration_tests (pull_request) Successful in 29m30s
CI / status-check (pull_request) Successful in 3s
CI / helm (push) Successful in 28s
CI / lint (push) Successful in 43s
CI / push-validation (push) Successful in 21s
CI / build (push) Successful in 50s
CI / quality (push) Successful in 1m6s
CI / e2e_tests (push) Successful in 55s
CI / typecheck (push) Successful in 1m27s
CI / security (push) Successful in 1m27s
CI / benchmark-regression (push) Failing after 39s
CI / unit_tests (push) Successful in 15m13s
CI / docker (push) Successful in 1m36s
CI / integration_tests (push) Successful in 25m29s
CI / coverage (push) Successful in 16m5s
CI / status-check (push) Successful in 2s
CI / benchmark-publish (push) Has been cancelled
fix(resource): add overlay to ResourceTypeConfigSchema._VALID_STRATEGIES
Implemented test coverage and documentation updates to ensure overlay is properly recognized and communicated across the sandbox strategy validation surface.

- Verification: Confirmed that overlay already exists in the _VALID_STRATEGIES frozenset within src/cleveragents/resource/schema.py (introduced in a prior commit that closed issue #2827).
- Comprehensive audit: Audited strategy validation points to ensure consistency:
  - _VALID_STRATEGIES in schema.py
  - _IMPLEMENTED_STRATEGIES and _SUPPORTED_STRATEGIES in factory.py
  - SandboxStrategy enum definitions in both resource.py and resource_type.py
  All include overlay as an allowed strategy.
- Tests: Added a new BDD scenario titled: "Invalid sandbox_strategy error message lists overlay as allowed" to features/consolidated_resource.feature. This asserts that validation errors explicitly list overlay among the allowed values when an invalid strategy is used.
- Documentation: Updated docstring in src/cleveragents/infrastructure/sandbox/strategy_registry.py to reflect that the built-in SandboxFactory handles six strategies, including snapshot and overlay.
- Design considerations: The fix itself was already applied in a previous commit (dbf3b8d7) addressing #2827. Issue #3614, raised by the UAT bot, required additional test coverage and accurate documentation of the available strategies.
- Impact: No behavioral changes beyond ensuring test coverage and documentation are aligned with the existing implementation. The added test guarantees that error messages remain accurate and comprehensive as strategies evolve.
- Affected areas: resource/schema.py, factory.py, resource.py, resource_type.py, features/consolidated_resource.feature, strategy_registry.py.

ISSUES CLOSED: #3614
2026-05-30 16:14:46 -04:00

904 lines
38 KiB
Gherkin

Feature: Consolidated Resource
Combined scenarios from: resource_handler_service_coverage, resource_handlers, resource_registry_model, resource_type_model
# ============================================================
# Originally from: resource_handler_service_coverage.feature
# Feature: ResourceHandlerService full coverage
# ============================================================
Scenario: Resolving a deferred binding raises ValueError
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs binding that is deferred with slot name "repo"
When I resolve the rhs single binding
Then an rhs ValueError is raised with message containing "Cannot resolve deferred binding for slot 'repo'"
Scenario: Resolving a binding with no resource_id raises ValueError
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs binding with no resource_id and slot name "data"
When I resolve the rhs single binding
Then an rhs ValueError is raised with message containing "Cannot resolve deferred binding for slot 'data'"
# --- resolve_binding: successful resolution ---
Scenario: Resolving a valid binding returns a BoundResource
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs non-deferred binding with slot "repo" and resource_id "RES001"
And rhs resource "RES001" typed "git-checkout" located at "/tmp/repo" with strategy "none"
And an rhs type spec "git-checkout" with no handler and sandbox strategy "git_worktree"
And the rhs sandbox manager returns a sandbox with path "/sandbox/abc"
When I resolve the rhs single binding with plan_id "plan-1" and access "read_write"
Then the rhs result is a BoundResource with slot "repo" and sandbox_path "/sandbox/abc"
And the rhs BoundResource has resource_id "RES001" and access "read_write"
# --- _resolve_handler_for_type: type_spec has a handler ---
Scenario: Handler is resolved from type_spec handler reference
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs non-deferred binding with slot "code" and resource_id "RES002"
And rhs resource "RES002" typed "custom/py" located at "/src" with strategy "none"
And an rhs type spec "custom/py" with handler "some.module:Handler" and sandbox strategy "copy_on_write"
And the rhs resolve_handler function returns a mock handler
And the rhs mock handler resolves to a BoundResource with sandbox_path "/sandbox/custom"
When I resolve the rhs single binding with plan_id "plan-2" and access "read_only"
Then the rhs result is a BoundResource with slot "code" and sandbox_path "/sandbox/custom"
# --- _resolve_handler_for_type: handler resolution fails, falls back ---
Scenario: Handler resolution failure falls back to DefaultHandler
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs non-deferred binding with slot "files" and resource_id "RES003"
And rhs resource "RES003" typed "custom/broken" located at "/data" with strategy "copy_on_write"
And an rhs type spec "custom/broken" with handler "bad.module:Missing" and sandbox strategy "copy_on_write"
And the rhs resolve_handler function raises HandlerResolutionError
And the rhs sandbox manager returns a sandbox with path "/sandbox/fallback"
When I resolve the rhs single binding with plan_id "plan-3" and access "read_only"
Then the rhs result is a BoundResource with slot "files" and sandbox_path "/sandbox/fallback"
# --- _resolve_handler_for_type: no handler on type_spec ---
Scenario: No handler on type_spec uses DefaultHandler directly
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs non-deferred binding with slot "dir" and resource_id "RES004"
And rhs resource "RES004" typed "fs-directory" located at "/home/user" with strategy "copy_on_write"
And an rhs type spec "fs-directory" with no handler and sandbox strategy "copy_on_write"
And the rhs sandbox manager returns a sandbox with path "/sandbox/dir"
When I resolve the rhs single binding with plan_id "plan-4" and access "read_only"
Then the rhs result is a BoundResource with slot "dir" and sandbox_path "/sandbox/dir"
# --- resolve_bindings: mixed deferred and non-deferred ---
Scenario: resolve_bindings skips deferred bindings and resolves the rest
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs binding list with:
| slot_name | resource_id | deferred |
| repo | RES010 | false |
| param | | true |
| config | RES011 | false |
And rhs resource "RES010" typed "git-checkout" located at "/repo" with strategy "none"
And rhs resource "RES011" typed "fs-directory" located at "/config" with strategy "none"
And an rhs type spec "git-checkout" with no handler and sandbox strategy "git_worktree"
And an rhs type spec "fs-directory" with no handler and sandbox strategy "copy_on_write"
And the rhs sandbox manager returns a sandbox with path "/sandbox/multi"
When I resolve all rhs bindings with plan_id "plan-5" and access "read_only"
Then the rhs result dict has keys "repo" and "config"
And the rhs result dict does not contain key "param"
# --- resolve_bindings: failure propagation ---
Scenario: resolve_bindings raises when a non-deferred binding fails
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs binding list with:
| slot_name | resource_id | deferred |
| broken | RES_MISSING | false |
And the rhs resource lookup raises NotFoundError for "RES_MISSING"
When I resolve all rhs bindings expecting an error
Then an rhs NotFoundError is raised
# --- resolve_bindings: all deferred returns empty dict ---
Scenario: resolve_bindings with all deferred bindings returns empty dict
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs binding list with:
| slot_name | resource_id | deferred |
| a | | true |
| b | | true |
When I resolve all rhs bindings with plan_id "plan-6" and access "read_only"
Then the rhs result dict is empty
# --- resolve_resource: direct resolution ---
Scenario: resolve_resource resolves a resource directly without a binding
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given rhs resource "RES020" typed "git-checkout" located at "/direct/repo" with strategy "none"
And an rhs type spec "git-checkout" with no handler and sandbox strategy "git_worktree"
And the rhs sandbox manager returns a sandbox with path "/sandbox/direct"
When I resolve the rhs resource directly with plan_id "plan-7" slot "src" and access "read_write"
Then the rhs result is a BoundResource with slot "src" and sandbox_path "/sandbox/direct"
And the rhs BoundResource has resource_id "RES020" and access "read_write"
# --- _DefaultHandler: resource sandbox_strategy overrides type ---
Scenario: DefaultHandler uses resource sandbox_strategy when present
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs non-deferred binding with slot "db" and resource_id "RES030"
And rhs resource "RES030" typed "custom/db" located at "/data/db" with strategy "snapshot"
And an rhs type spec "custom/db" with no handler and sandbox strategy "copy_on_write"
And the rhs sandbox manager returns a sandbox with path "/sandbox/snap"
When I resolve the rhs single binding with plan_id "plan-8" and access "read_only"
Then the rhs sandbox manager was called with strategy "snapshot"
# --- _DefaultHandler: falls back to type_spec sandbox_strategy ---
Scenario: DefaultHandler falls back to type_spec sandbox_strategy when resource has none
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs non-deferred binding with slot "fs" and resource_id "RES031"
And rhs resource "RES031" typed "fs-directory" located at "/mnt/share" with strategy "none"
And an rhs type spec "fs-directory" with no handler and sandbox strategy "copy_on_write"
And the rhs sandbox manager returns a sandbox with path "/sandbox/cow"
When I resolve the rhs single binding with plan_id "plan-9" and access "read_only"
Then the rhs sandbox manager was called with strategy "copy_on_write"
# --- _DefaultHandler: no location error ---
Scenario: DefaultHandler raises ValueError when resource has no location
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs non-deferred binding with slot "empty" and resource_id "RES040"
And rhs resource "RES040" typed "fs-directory" with no location
And an rhs type spec "fs-directory" with no handler and sandbox strategy "copy_on_write"
When I resolve the rhs single binding
Then an rhs ValueError is raised with message containing "has no location"
# --- _DefaultHandler: sandbox context is None ---
Scenario: DefaultHandler raises RuntimeError when sandbox has no context
Given a mock sandbox manager for rhs
And a mock resource lookup for rhs
And a mock type lookup for rhs
And a ResourceHandlerService under test
Given an rhs non-deferred binding with slot "ctx" and resource_id "RES050"
And rhs resource "RES050" typed "fs-directory" located at "/tmp/ctx" with strategy "none"
And an rhs type spec "fs-directory" with no handler and sandbox strategy "copy_on_write"
And the rhs sandbox manager returns a sandbox with no context
When I resolve the rhs single binding
Then an rhs RuntimeError is raised with message containing "has no context"
# ============================================================
# Originally from: resource_handlers.feature
# Feature: Resource handler runtime
# ============================================================
Scenario: GitCheckoutHandler satisfies ResourceHandler protocol
Then GitCheckoutHandler should satisfy the ResourceHandler protocol
Scenario: FsDirectoryHandler satisfies ResourceHandler protocol
Then FsDirectoryHandler should satisfy the ResourceHandler protocol
# === GitCheckoutHandler ===
Scenario: GitCheckoutHandler resolves a git-checkout resource
Given a git-checkout resource with location "/tmp/test-repo"
And a sandbox manager with a mock factory
When I resolve the resource with GitCheckoutHandler for plan "PLAN001"
Then the bound resource should have slot name "repo"
And the bound resource should have resource type "git-checkout"
And the bound resource sandbox path should not be empty
Scenario: GitCheckoutHandler uses resource-level strategy override
Given a git-checkout resource at "/tmp/test-repo" using strategy "copy_on_write"
And a sandbox manager with a mock factory
When I resolve the resource with GitCheckoutHandler for plan "PLAN002"
Then the sandbox was created with strategy "copy_on_write"
Scenario: GitCheckoutHandler rejects resource without location
Given a git-checkout resource without location
And a sandbox manager with a mock factory
When I try to resolve the resource with GitCheckoutHandler
Then a handler ValueError should be raised
And the handler error should mention "no location"
# === FsDirectoryHandler ===
Scenario: FsDirectoryHandler resolves an fs-directory resource
Given an fs-directory resource with location "/tmp/test-dir"
And a sandbox manager with a mock factory
When I resolve the resource with FsDirectoryHandler for plan "PLAN003"
Then the bound resource should have slot name "workdir"
And the bound resource should have resource type "fs-directory"
And the bound resource sandbox path should not be empty
Scenario: FsDirectoryHandler uses copy_on_write by default
Given an fs-directory resource with location "/tmp/test-dir"
And a sandbox manager with a mock factory
When I resolve the resource with FsDirectoryHandler for plan "PLAN004"
Then the sandbox was created with strategy "copy_on_write"
Scenario: FsDirectoryHandler rejects resource without location
Given an fs-directory resource without location
And a sandbox manager with a mock factory
When I try to resolve the resource with FsDirectoryHandler
Then a handler ValueError should be raised
# === Handler Resolver ===
Scenario: Resolve handler from valid module:class string
When I resolve handler "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler"
Then the resolved handler should be a GitCheckoutHandler instance
Scenario: Resolve handler from fs-directory reference
When I resolve handler "cleveragents.resource.handlers.fs_directory:FsDirectoryHandler"
Then the resolved handler should be a FsDirectoryHandler instance
Scenario: Resolve handler caches instances
When I resolve handler "cleveragents.resource.handlers.git_checkout:GitCheckoutHandler"
And I resolve the same handler reference again
Then both resolved handlers should be the same object
Scenario: Resolve handler rejects empty string
When I try to resolve an empty handler reference
Then a HandlerResolutionError should be raised
And the resolution error should mention "empty"
Scenario: Resolve handler rejects missing colon separator
When I try to resolve handler "cleveragents.resource.handlers.git_checkout"
Then a HandlerResolutionError should be raised
And the resolution error should mention "module.path:ClassName"
Scenario: Resolve handler rejects nonexistent module
When I try to resolve handler "nonexistent.module:SomeClass"
Then a HandlerResolutionError should be raised
And the resolution error should mention "Cannot import"
Scenario: Resolve handler rejects nonexistent class in valid module
When I try to resolve handler "cleveragents.resource.handlers.git_checkout:NonExistentClass"
Then a HandlerResolutionError should be raised
And the resolution error should mention "not found"
# === ResourceHandlerService ===
Scenario: ResourceHandlerService resolves a binding to BoundResource
Given a resource handler service with mock lookups
And a binding result for slot "repo" with resource id "RES001"
When I resolve the binding via resource handler service for plan "PLAN010"
Then the handler service should return a BoundResource
And the handler service BoundResource slot should be "repo"
And the handler service BoundResource sandbox path should not be empty
Scenario: ResourceHandlerService skips deferred bindings
Given a resource handler service with mock lookups
And a deferred binding result for slot "extra"
And a binding result for slot "repo" with resource id "RES001"
When I resolve all bindings via resource handler service for plan "PLAN011"
Then the handler service should return 1 bound resource
And the handler service should have resolved slot "repo"
Scenario: ResourceHandlerService rejects deferred single binding
Given a resource handler service with mock lookups
And a deferred binding result for slot "extra"
When I try to resolve the single deferred binding via resource handler service
Then a handler ValueError should be raised
And the handler error should mention "deferred"
Scenario: ResourceHandlerService uses fallback handler when type has no handler string
Given a resource handler service with mock lookups and no handler string
And a binding result for slot "data" with resource id "RES002"
When I resolve the binding via resource handler service for plan "PLAN012"
Then the handler service should return a BoundResource
And the handler service BoundResource sandbox path should not be empty
# ============================================================
# Originally from: resource_registry_model.feature
# Feature: Resource Registry Domain Model
# ============================================================
Scenario: PhysVirt has exactly two values
Then the PhysVirt enum should have values "physical, virtual"
Scenario: PhysVirt physical value
Given a PhysVirt value of "physical"
Then the PhysVirt string value should be "physical"
Scenario: PhysVirt virtual value
Given a PhysVirt value of "virtual"
Then the PhysVirt string value should be "virtual"
Scenario: PhysVirt rejects invalid value
When I try to create a PhysVirt with value "hybrid"
Then a resource validation error should be raised
# SandboxStrategy enum
Scenario: SandboxStrategy has exactly six values
Then the SandboxStrategy enum should have values "git_worktree, copy_on_write, transaction_rollback, snapshot, overlay, none"
Scenario Outline: SandboxStrategy accepts valid values
Given a SandboxStrategy value of "<strategy>"
Then the SandboxStrategy string value should be "<strategy>"
Examples:
| strategy |
| git_worktree |
| copy_on_write |
| transaction_rollback |
| snapshot |
| overlay |
| none |
Scenario: SandboxStrategy accepts overlay
Given a SandboxStrategy value of "overlay"
Then the SandboxStrategy string value should be "overlay"
Scenario: SandboxStrategy rejects versioning
When I try to create a SandboxStrategy with value "versioning"
Then a resource validation error should be raised
# ResourceCapabilities model
Scenario: ResourceCapabilities defaults
Given default ResourceCapabilities
Then the capability readable should be true
And the capability writable should be true
And the capability sandboxable should be true
And the capability checkpointable should be true
Scenario: ResourceCapabilities with custom values
Given ResourceCapabilities with readable false and checkpointable true
Then the capability readable should be false
And the capability writable should be true
And the capability sandboxable should be true
And the capability checkpointable should be true
Scenario: ResourceCapabilities is frozen
Given default ResourceCapabilities
When I try to mutate a ResourceCapabilities field
Then a resource validation error should be raised
# Resource model - creation
Scenario: Create a minimal physical resource
Given a Resource with resource_id "01HZQX7K8M3N4P5R6S7T8V9W0X" type "git-checkout" and classification "physical"
Then the resource resource_id should be "01HZQX7K8M3N4P5R6S7T8V9W0X"
And the resource type name should be "git-checkout"
And the resource classification should be "physical"
And the resource name should be empty
And the resource description should be empty
And the resource properties should be empty
And the resource parents should be empty
And the resource children should be empty
And the resource linked_projects should be empty
And the resource created_at should be set
And the resource updated_at should be set
Scenario: Create a full resource with all fields
Given a full Resource with id "01HZQX7K8M3N4P5R6S7T8V9W0X" name "local/my-repo" type "git-checkout" classification "physical" description "Main repo"
Then the resource name should be "local/my-repo"
And the resource description should be "Main repo"
Scenario: Resource with sandbox strategy override
Given a Resource with sandbox strategy "git_worktree"
Then the resource sandbox strategy should be "git_worktree"
Scenario: Resource with non-sandboxable rejects checkpointable
When I try to create a Resource with sandboxable false and checkpointable true
Then a resource validation error should be raised
# Resource model - validation
Scenario: Resource rejects invalid ULID
When I try to create a Resource with resource_id "invalid-ulid"
Then a resource validation error should be raised
Scenario: Resource rejects empty resource_type_name
When I try to create a Resource with empty resource_type_name
Then a resource validation error should be raised
Scenario: Resource rejects empty string name
When I try to create a Resource with empty string name
Then a resource validation error should be raised
Scenario: Resource accepts None name
Given a Resource with resource_id "01HZQX7K8M3N4P5R6S7T8V9W0X" type "git-checkout" and classification "physical"
Then the resource name should be empty
# Resource model - domain properties
Scenario: Physical resource is_physical
Given a Resource with resource_id "01HZQX7K8M3N4P5R6S7T8V9W0X" type "git-checkout" and classification "physical"
Then the resource is_physical should be true
And the resource is_virtual should be false
Scenario: Virtual resource is_virtual
Given a Resource with resource_id "01HZQX7K8M3N4P5R6S7T8V9W0X" type "git" and classification "virtual"
Then the resource is_physical should be false
And the resource is_virtual should be true
Scenario: Read-only resource
Given a Resource with writable false
Then the resource is_read_only should be true
Scenario: Writable resource
Given a Resource with resource_id "01HZQX7K8M3N4P5R6S7T8V9W0X" type "git-checkout" and classification "physical"
Then the resource is_read_only should be false
# Resource model - frozen
Scenario: Resource is frozen
Given a Resource with resource_id "01HZQX7K8M3N4P5R6S7T8V9W0X" type "git-checkout" and classification "physical"
When I try to mutate a Resource field
Then a resource validation error should be raised
# Resource model - with_updated_at
Scenario: with_updated_at returns new instance
Given a Resource with resource_id "01HZQX7K8M3N4P5R6S7T8V9W0X" type "git-checkout" and classification "physical"
When I call with_updated_at on the resource
Then the new resource should have a different updated_at
And the new resource should have the same resource_id
# Resource model - content_hash and location
Scenario: Resource with content_hash
Given a Resource with content_hash "sha256:abc123"
Then the resource content_hash should be "sha256:abc123"
Scenario: Resource with location
Given a Resource with location "/home/user/repos/main"
Then the resource location should be "/home/user/repos/main"
Scenario: Resource rejects empty location string
When I try to create a Resource with empty location
Then a resource validation error should be raised
# Resource model - parents and children
Scenario: Resource with parents and children
Given a Resource with parents and children
Then the resource should have 1 parent
And the resource should have 2 children
# ============================================================
# Originally from: resource_type_model.feature
# Feature: Resource Type Model and Schema Loader
# ============================================================
Scenario: Built-in resource type with unnamespaced name
Given a resource type config with name "git-checkout" and built_in true
When I create a ResourceTypeSpec from config
Then the resource type should be valid
And the rt_spec name should be "git-checkout"
Scenario: Custom resource type with namespaced name
Given a resource type config with name "myorg/custom-db" and built_in false
When I create a ResourceTypeSpec from config
Then the resource type should be valid
And the rt_spec name should be "myorg/custom-db"
Scenario: Custom type without namespace is rejected
Given a resource type config with name "no-namespace" and built_in false
When I try to create a ResourceTypeSpec from config
Then the resource type creation should fail with "namespace/name"
Scenario: Invalid name with special characters is rejected
Given a resource type config with name "bad!name" and built_in false
When I try to create a ResourceTypeSpec from config
Then the resource type creation should fail with "Invalid resource type name"
# ── ResourceKind ────────────────────────────────────────────
Scenario: Physical resource kind
Given a resource type config with resource_kind "physical"
When I create a ResourceTypeSpec from config
Then the resource type resource_kind should be "physical"
Scenario: Virtual resource kind
Given a resource type config with resource_kind "virtual" and equivalence set
When I create a ResourceTypeSpec from config
Then the resource type resource_kind should be "virtual"
Scenario: Virtual resource kind without equivalence is rejected
Given a resource type config with resource_kind "virtual" and no equivalence
When I try to create a ResourceTypeSpec from config
Then the resource type creation should fail with "equivalence"
# ── SandboxStrategy ─────────────────────────────────────────
Scenario: Git worktree sandbox strategy
Given a resource type config with sandbox_strategy "git_worktree"
When I create a ResourceTypeSpec from config
Then the resource type sandbox_strategy should be "git_worktree"
Scenario: Copy on write sandbox strategy
Given a resource type config with sandbox_strategy "copy_on_write"
When I create a ResourceTypeSpec from config
Then the resource type sandbox_strategy should be "copy_on_write"
Scenario: Transaction rollback sandbox strategy
Given a resource type config with sandbox_strategy "transaction_rollback"
When I create a ResourceTypeSpec from config
Then the resource type sandbox_strategy should be "transaction_rollback"
Scenario: Snapshot sandbox strategy
Given a resource type config with sandbox_strategy "snapshot"
When I create a ResourceTypeSpec from config
Then the resource type sandbox_strategy should be "snapshot"
Scenario: None sandbox strategy
Given a resource type config with sandbox_strategy "none"
When I create a ResourceTypeSpec from config
Then the resource type sandbox_strategy should be "none"
# ── CLI Arguments ───────────────────────────────────────────
Scenario: Valid CLI argument parsing
Given a resource type config with a cli_arg named "path" of type "path"
When I create a ResourceTypeSpec from config
Then the resource type should have 1 cli_arg
And the first cli_arg name should be "path"
Scenario: Multiple CLI arguments
Given a resource type config with cli_args "path" and "branch"
When I create a ResourceTypeSpec from config
Then the resource type should have 2 cli_args
Scenario: CLI argument with invalid name is rejected
Given a resource type config with a cli_arg named "BadName" of type "string"
When I try to create a ResourceTypeSpec from config
Then the resource type creation should fail with "not CLI-safe"
Scenario: CLI argument with invalid type is rejected
Given a resource type config with a cli_arg named "count" of type "bigint"
When I try to create a ResourceTypeSpec from config
Then the resource type creation should fail with "Invalid argument type"
# ── Parent/Child Types ──────────────────────────────────────
Scenario: Resource type with parent types
Given a resource type config with parent_types "git-checkout"
When I create a ResourceTypeSpec from config
Then the resource type should have parent_types containing "git-checkout"
Scenario: Resource type with child types
Given a resource type config with child_types "fs-directory"
When I create a ResourceTypeSpec from config
Then the resource type should have child_types containing "fs-directory"
Scenario: Resource type with both parent and child types
Given a resource type config with parent_types_set "git-checkout" and child_types_set "fs-directory"
When I create a ResourceTypeSpec from config
Then the resource type should have parent_types containing "git-checkout"
And the resource type should have child_types containing "fs-directory"
# ── from_config() ───────────────────────────────────────────
Scenario: from_config with missing name
Given a resource type config dict without name
When I try to call from_config
Then from_config should fail with "must include 'name'"
Scenario: from_config with missing resource_kind
Given a resource type config dict without resource_kind
When I try to call from_config
Then from_config should fail with "must include 'resource_kind'"
Scenario: from_config with missing sandbox_strategy
Given a resource type config dict without sandbox_strategy
When I try to call from_config
Then from_config should fail with "must include 'sandbox_strategy'"
Scenario: from_config with valid full config
Given a full resource type config dict
When I call from_config
Then the resource type spec should be valid
# ── as_cli_dict() ───────────────────────────────────────────
Scenario: as_cli_dict returns expected keys
Given a resource type config with name "myorg/test-type" and built_in false
When I create a ResourceTypeSpec and call as_cli_dict
Then the rt_cli dict should contain key "name" with value "myorg/test-type"
And the rt_cli dict should contain key "resource_kind"
And the rt_cli dict should contain key "sandbox_strategy"
And the rt_cli dict should contain key "capabilities"
# ── Schema loader ───────────────────────────────────────────
Scenario: Schema loader from YAML string
Given a valid resource type YAML string
When I load the resource type via from_yaml
Then the loaded schema should be valid
Scenario: Schema loader from YAML file
Given a valid resource type YAML file
When I load the resource type via from_yaml_file
Then the loaded schema should be valid
Scenario: Schema loader with env var interpolation
Given a resource type YAML with env var "${RT_TEST_HANDLER}"
And the env var "RT_TEST_HANDLER" is set to "test.handler:Handler"
When I load the resource type via from_yaml
Then the handler should be "test.handler:Handler"
Scenario: Schema loader with empty YAML
When I try to load an empty YAML string
Then the schema loader should fail with "empty"
Scenario: Schema loader with None YAML
When I try to load a None YAML string
Then the schema loader should fail with "None"
Scenario: Schema loader with non-mapping YAML
When I try to load a YAML list
Then the schema loader should fail with "mapping"
Scenario: Schema loader with unsupported version
Given a resource type YAML with schema_version "99"
When I try to load the resource type via from_yaml
Then the schema loader should fail with "Unsupported schema version"
Scenario: Schema loader from non-existent file
When I try to load from a non-existent file
Then the schema loader should raise FileNotFoundError
Scenario: Schema loader from directory instead of file
When I try to load from a directory path
Then the schema loader should fail with "not a file"
# ── Capabilities ────────────────────────────────────────────
Scenario: Default capabilities
Given a resource type config with default capabilities
When I create a ResourceTypeSpec from config
Then the capabilities should have read true
And the capabilities should have write true
And the capabilities should have sandbox true
And the capabilities should have checkpoint false
Scenario: Custom capabilities
Given a resource type config with custom capabilities
When I create a ResourceTypeSpec from config
Then the capabilities should have read true
And the capabilities should have write false
And the capabilities should have sandbox true
And the capabilities should have checkpoint true
Scenario: Unknown capability key is rejected
Given a resource type config with unknown capability "teleport"
When I try to create a ResourceTypeSpec from config
Then the resource type creation should fail with "Unknown capability"
# ── CLI argument edge cases ──────────────────────────────────
Scenario: cli_args entry that is not a mapping is rejected
Given a resource type config with a non-mapping cli_arg
When I try to create a ResourceTypeSpec from config
Then the resource type creation should fail with "must be a mapping"
# ── as_cli_dict coverage ──────────────────────────────────────
Scenario: as_cli_dict includes all optional fields when present
Given a full resource type config with handler and auto_discovery
When I create a ResourceTypeSpec and call as_cli_dict
Then the rt_cli dict should contain key "description"
And the rt_cli dict should contain key "cli_args"
And the rt_cli dict should contain key "child_types"
And the rt_cli dict should contain key "handler"
And the rt_cli dict should contain key "auto_discovery"
And the rt_cli dict should contain key "capabilities"
Scenario: as_cli_dict with equivalence field
Given a resource type config with resource_kind "virtual" and equivalence set
When I create a ResourceTypeSpec and call as_cli_dict
Then the rt_cli dict should contain key "equivalence"
Scenario: as_cli_dict with parent_types
Given a resource type config with parent_types "git-checkout"
When I create a ResourceTypeSpec and call as_cli_dict
Then the rt_cli dict should contain key "parent_types"
# ── Schema-level validation ───────────────────────────────────
Scenario: Schema rejects invalid cli_arg name
Given a resource type YAML with invalid cli_arg name
When I try to load the resource type via from_yaml
Then the schema loader should fail with "not CLI-safe"
Scenario: Schema rejects invalid cli_arg type
Given a resource type YAML with invalid cli_arg type
When I try to load the resource type via from_yaml
Then the schema loader should fail with "Invalid argument type"
Scenario: Schema rejects invalid resource type name
Given a resource type YAML with invalid name
When I try to load the resource type via from_yaml
Then the schema loader should fail with "Invalid resource type name"
Scenario: Schema rejects invalid resource_kind
Given a resource type YAML with invalid resource_kind
When I try to load the resource type via from_yaml
Then the schema loader should fail with "Invalid resource_kind"
Scenario: Schema rejects invalid sandbox_strategy
Given a resource type YAML with invalid sandbox_strategy
When I try to load the resource type via from_yaml
Then the schema loader should fail with "Invalid sandbox_strategy"
Scenario: Schema rejects custom type without namespace
Given a resource type YAML with custom type no namespace
When I try to load the resource type via from_yaml
Then the schema loader should fail with "namespace/name"
Scenario: Schema rejects virtual type without equivalence
Given a resource type YAML with virtual kind no equivalence
When I try to load the resource type via from_yaml
Then the schema loader should fail with "equivalence"
Scenario: Schema loader from_yaml_file with None path
When I try to load from a None file path
Then the schema loader should fail with "None"
Scenario: Schema env var interpolation in list items
Given a resource type YAML with env var in list
And the env var "RT_TEST_PARENT" is set to "custom-parent"
When I load the resource type via from_yaml
Then the loaded schema should have parent type "custom-parent"
Scenario: Schema accepts overlay sandbox strategy
Given a resource type YAML with sandbox_strategy "overlay"
When I load the resource type via from_yaml
Then the loaded schema sandbox_strategy should be "overlay"
Scenario: Invalid sandbox_strategy error message lists overlay as allowed
Given a resource type YAML with invalid sandbox_strategy
When I try to load the resource type via from_yaml
Then the schema loader should fail with "overlay"