Feature: Sandbox Boundary Algebra
  As a plan execution engine
  I want to compute sandbox boundaries by walking the resource DAG
  So that resources sharing a boundary share one sandbox instance

  Background:
    Given a resource registry for boundary algebra tests

  # -- sandbox_boundary(r) basic resolution --

  Scenario: A resource that is itself a sandbox boundary returns itself
    Given a sandboxable resource "checkout-01" with strategy "git_worktree"
    When I compute the sandbox boundary for "checkout-01"
    Then the boundary should be "checkout-01"

  Scenario: A child resource resolves to its sandboxable parent
    Given a sandboxable resource "checkout-01" with strategy "git_worktree"
    And a non-sandboxable resource "file-01" with parent "checkout-01"
    When I compute the sandbox boundary for "file-01"
    Then the boundary should be "checkout-01"

  Scenario: A deeply nested resource resolves to a distant boundary
    Given a sandboxable resource "checkout-01" with strategy "git_worktree"
    And a non-sandboxable resource "dir-01" with parent "checkout-01"
    And a non-sandboxable resource "subdir-01" with parent "dir-01"
    And a non-sandboxable resource "file-deep" with parent "subdir-01"
    When I compute the sandbox boundary for "file-deep"
    Then the boundary should be "checkout-01"

  Scenario: A resource with no sandboxable ancestor returns None
    Given a non-sandboxable resource "orphan-01" with no parents
    When I compute the sandbox boundary for "orphan-01"
    Then the boundary should be None

  Scenario: A resource whose parent is not in the registry returns None
    Given a non-sandboxable resource "dangling-01" with missing parent "ghost-99"
    When I compute the sandbox boundary for "dangling-01"
    Then the boundary should be None

  # -- is_sandbox_boundary checks --

  Scenario: A resource with sandboxable capability and non-none strategy is a boundary
    Given a sandboxable resource "boundary-res" with strategy "copy_on_write"
    When I check if "boundary-res" is a sandbox boundary
    Then it should be a sandbox boundary

  Scenario: A resource with sandboxable capability but none strategy is not a boundary
    Given a resource "non-boundary" with sandboxable true and strategy "none"
    When I check if "non-boundary" is a sandbox boundary
    Then it should not be a sandbox boundary

  Scenario: A resource with no sandboxable capability is not a boundary
    Given a non-sandboxable resource "no-cap" with no parents
    When I check if "no-cap" is a sandbox boundary
    Then it should not be a sandbox boundary

  Scenario: Checking is_sandbox_boundary with None raises ValueError
    When I check is_sandbox_boundary with None resource
    Then the error should be a ValueError with text "resource cannot be None"

  # -- domain grouping --

  Scenario: Resources with the same boundary are grouped into one domain
    Given a sandboxable resource "checkout-01" with strategy "git_worktree"
    And a non-sandboxable resource "file-a" with parent "checkout-01"
    And a non-sandboxable resource "file-b" with parent "checkout-01"
    When I compute sandbox domains for "file-a" and "file-b"
    Then both resources should be in the domain of "checkout-01"

  Scenario: Multiple boundaries produce separate domains
    Given a sandboxable resource "checkout-01" with strategy "git_worktree"
    And a sandboxable resource "dir-root" with strategy "copy_on_write"
    And a non-sandboxable resource "file-git" with parent "checkout-01"
    And a non-sandboxable resource "file-fs" with parent "dir-root"
    When I compute sandbox domains for "file-git" and "file-fs"
    Then "file-git" should be in the domain of "checkout-01"
    And "file-fs" should be in the domain of "dir-root"

  Scenario: Unsandboxable resources are grouped under the special key
    Given a non-sandboxable resource "orphan-a" with no parents
    And a non-sandboxable resource "orphan-b" with no parents
    When I compute sandbox domains for "orphan-a" and "orphan-b"
    Then both resources should be in the unsandboxable domain

  # -- boundary caching --

  Scenario: Boundary results are cached across lookups
    Given a sandboxable resource "checkout-01" with strategy "git_worktree"
    And a non-sandboxable resource "file-cached" with parent "checkout-01"
    And a fresh boundary cache
    When I look up the boundary for "file-cached" via the cache
    And I look up the boundary for "file-cached" via the cache again
    Then the cache should contain 1 entry
    And the cached boundary for "file-cached" should be "checkout-01"

  Scenario: Cache can be cleared
    Given a sandboxable resource "checkout-01" with strategy "git_worktree"
    And a non-sandboxable resource "file-c" with parent "checkout-01"
    And a fresh boundary cache
    When I look up the boundary for "file-c" via the cache
    And I clear the boundary cache
    Then the cache should contain 0 entries

  Scenario: Cache returns None for unsandboxable resources
    Given a non-sandboxable resource "orphan-cached" with no parents
    And a fresh boundary cache
    When I look up the boundary for "orphan-cached" via the cache
    Then the cached boundary result should be None
    And the cache should contain 1 entry

  # -- SandboxManager boundary-aware keying --

  Scenario: Manager resolves sandbox key via boundary algebra
    Given a located sandboxable resource "checkout-01" strategy "git_worktree" location "/tmp/repo"
    And a non-sandboxable resource "file-01" with parent "checkout-01"
    And a sandbox factory instance
    And a sandbox manager with the factory
    When I resolve the sandbox key for plan "plan-001" resource "file-01"
    Then the sandbox key should be "checkout-01"

  Scenario: Manager creates sandbox keyed by boundary ID
    Given a located sandboxable resource "checkout-01" strategy "git_worktree" location "/tmp/repo"
    And a non-sandboxable resource "file-01" with parent "checkout-01"
    And a non-sandboxable resource "file-02" with parent "checkout-01"
    And a mock sandbox factory
    And a sandbox manager with the mock factory
    When I get or create a boundary sandbox for plan "plan-001" resource "file-01"
    And I get or create a boundary sandbox for plan "plan-001" resource "file-02"
    Then both resources should share the same sandbox instance

  Scenario: Manager raises NoSandboxBoundaryError for orphan resource
    Given a non-sandboxable resource "orphan-mgr" with no parents
    And a sandbox factory instance
    And a sandbox manager with the factory
    When I try to resolve the sandbox key for plan "plan-001" resource "orphan-mgr"
    Then a NoSandboxBoundaryError should be raised

  Scenario: Manager boundary cache is cleared on demand
    Given a located sandboxable resource "checkout-01" strategy "git_worktree" location "/tmp/repo"
    And a non-sandboxable resource "file-01" with parent "checkout-01"
    And a sandbox factory instance
    And a sandbox manager with the factory
    When I resolve the sandbox key for plan "plan-001" resource "file-01"
    Then the manager boundary cache should have 1 entry
    When I clear the manager boundary cache
    Then the manager boundary cache should have 0 entries

  # -- argument validation --

  Scenario: sandbox_boundary with None resource raises ValueError
    When I call sandbox_boundary with None resource
    Then the error should be a ValueError with text "resource cannot be None"

  Scenario: sandbox_boundary with None registry raises ValueError
    Given a non-sandboxable resource "dummy" with no parents
    When I call sandbox_boundary with None registry for "dummy"
    Then the error should be a ValueError with text "resource_registry cannot be None"

  Scenario: compute_sandbox_domains with None resources raises ValueError
    When I call compute_sandbox_domains with None resources
    Then the error should be a ValueError with text "resources cannot be None"

  Scenario: compute_sandbox_domains with None registry raises ValueError
    Given a non-sandboxable resource "dummy" with no parents
    When I call compute_sandbox_domains with None registry
    Then the error should be a ValueError with text "resource_registry cannot be None"

  Scenario: BoundaryCache with None resource raises ValueError
    Given a fresh boundary cache
    When I call boundary cache get_boundary with None resource
    Then the error should be a ValueError with text "resource cannot be None"

  Scenario: BoundaryCache with None registry raises ValueError
    Given a non-sandboxable resource "dummy" with no parents
    And a fresh boundary cache
    When I call boundary cache get_boundary with None registry for "dummy"
    Then the error should be a ValueError with text "resource_registry cannot be None"

  Scenario: Boundary walk skips already-visited parent IDs
    Given a sandboxable resource "checkout-01" with strategy "git_worktree"
    And a non-sandboxable resource "dir-a" with parent "checkout-01"
    And a non-sandboxable resource "dir-b" with parent "checkout-01"
    And a non-sandboxable resource "file-shared" with parents "dir-a" and "dir-b"
    When I compute the sandbox boundary for "file-shared"
    Then the boundary should be "checkout-01"
