Feature: ResourceHandlerService full coverage As a plan execution engine I want the ResourceHandlerService to resolve bindings into BoundResources So that tools receive sandbox-backed resource paths during execution Background: 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 # --- resolve_binding: deferred / missing resource_id --- Scenario: Resolving a deferred binding raises ValueError 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 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 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 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 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 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 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 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 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 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 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 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 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 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"