b028c80cab
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 18s
CI / security (pull_request) Successful in 33s
CI / typecheck (pull_request) Successful in 41s
CI / unit_tests (pull_request) Successful in 2m29s
CI / integration_tests (pull_request) Successful in 3m2s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 4m28s
CI / benchmark-regression (pull_request) Successful in 29m5s
Add project-resource isolation to the ACMS context tier system via ScopedBackendView, ResourceScope, alias resolution, DAG expansion, and enforcement hooks. Core types (scoped_view.py): ResourceScope, ScopeViolationError, ScopedBackendView, ScopedBackendSet, create_scoped_backend_set. Scope resolution (scope_resolution.py): ResourceAliasResolver, validate_project_scope, validate_resource_scope, resolve_resource_scope with registry_lookup callback for DAG expansion. Tier integration (scoped_tiers.py): ScopedTierMixin with get_scoped, get_scoped_by_resource, validate_fragment_scope, store_with_scope_check, get_scoped_metrics mixed into ContextTierService. Backward compat: tiers.py re-exports ScopedBackendView. Tests: 74 Behave scenarios (235 steps), 8 Robot Framework cases, 7 ASV benchmark suites. Lint and typecheck clean. ISSUES CLOSED: #193
466 lines
22 KiB
Gherkin
466 lines
22 KiB
Gherkin
Feature: Scoped Backend View Filtering
|
|
As a developer
|
|
I want scoped backend views that filter text/vector/graph queries to project resources
|
|
So that plans never see resources outside their scope (cross-project isolation)
|
|
|
|
# ---- ResourceScope ----
|
|
|
|
Scenario: Create a valid ResourceScope
|
|
Given sv a ResourceScope with resources "RES01", "RES02" and projects "local/api"
|
|
Then sv the scope should contain resource "RES01"
|
|
And sv the scope should contain resource "RES02"
|
|
And sv the scope should not contain resource "RES99"
|
|
And sv the scope should contain project "local/api"
|
|
And sv the scope should not contain project "local/frontend"
|
|
|
|
Scenario: ResourceScope is frozen
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
Then sv modifying the scope should raise an error
|
|
|
|
Scenario: ResourceScope temporal_scope defaults to current
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
Then sv temporal_scope should be "current"
|
|
|
|
Scenario: ResourceScope rejects invalid temporal_scope
|
|
Then sv creating a scope with temporal_scope "invalid" should raise ValidationError
|
|
|
|
Scenario: ResourceScope allows empty resource_ids after filtering
|
|
Then sv creating a scope with no resources should succeed
|
|
|
|
Scenario: ResourceScope rejects empty project_names
|
|
Then sv creating a scope with no projects should raise ValidationError
|
|
|
|
# ---- ResourceScope path filtering ----
|
|
|
|
Scenario: ResourceScope matches_path with include patterns
|
|
Given sv a ResourceScope with include_paths "src/**/*.py", "tests/**/*.py"
|
|
Then sv path "src/main.py" should match
|
|
And sv path "tests/test_foo.py" should match
|
|
And sv path "docs/readme.md" should not match
|
|
|
|
Scenario: ResourceScope matches_path with exclude patterns
|
|
Given sv a ResourceScope with exclude_paths "**/node_modules/**", "**/__pycache__/**"
|
|
Then sv path "src/main.py" should match
|
|
And sv path "node_modules/foo/bar.js" should not match
|
|
And sv path "src/__pycache__/foo.pyc" should not match
|
|
|
|
Scenario: ResourceScope matches_path with both include and exclude
|
|
Given sv a ResourceScope with include_paths "src/**/*.py" and exclude_paths "**/test_*"
|
|
Then sv path "src/main.py" should match
|
|
And sv path "src/test_main.py" should not match
|
|
And sv path "docs/readme.md" should not match
|
|
|
|
# ---- ScopedBackendView: fragment-level filtering ----
|
|
|
|
Scenario: ScopedBackendView filters fragments by project name
|
|
Given sv a project-only ScopedBackendView for "proj-a"
|
|
And sv a fragment "frag-a" in project "proj-a" with resource "RES01"
|
|
And sv a fragment "frag-b" in project "proj-b" with resource "RES02"
|
|
Then sv fragment "frag-a" should be visible
|
|
And sv fragment "frag-b" should not be visible
|
|
|
|
Scenario: ScopedBackendView excludes fragments with empty project_name
|
|
Given sv a project-only ScopedBackendView for "proj-a"
|
|
And sv a fragment "frag-x" with no project
|
|
Then sv fragment "frag-x" should not be visible
|
|
|
|
Scenario: ScopedBackendView filters by resource_ids when set
|
|
Given sv a project-and-resource ScopedBackendView for "proj-a" resources "RES01"
|
|
And sv a fragment "frag-a" in project "proj-a" with resource "RES01"
|
|
And sv a fragment "frag-c" in project "proj-a" with resource "RES99"
|
|
Then sv fragment "frag-a" should be visible
|
|
And sv fragment "frag-c" should not be visible
|
|
|
|
Scenario: ScopedBackendView denied_resource_ids blocks specific resources
|
|
Given sv a deny-resource ScopedBackendView for "proj-a" allow "RES01", "RES02" deny "RES02"
|
|
And sv a fragment "frag-a" in project "proj-a" with resource "RES01"
|
|
And sv a fragment "frag-d" in project "proj-a" with resource "RES02"
|
|
Then sv fragment "frag-a" should be visible
|
|
And sv fragment "frag-d" should not be visible
|
|
|
|
# ---- ScopedBackendView: backend-level search proxying ----
|
|
|
|
Scenario: ScopedBackendView proxies text search with scope injected
|
|
Given sv a ResourceScope with resources "RES01", "RES02" and projects "local/api"
|
|
And sv a ScopedBackendView from that scope
|
|
And sv an InMemoryTextBackend
|
|
When sv I search text for "auth flow"
|
|
Then sv the text search should return an empty list
|
|
And sv the text search should not raise an error
|
|
|
|
Scenario: ScopedBackendView proxies vector search with scope injected
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
And sv a ScopedBackendView from that scope
|
|
And sv an InMemoryVectorBackend
|
|
When sv I search vector with embedding
|
|
Then sv the vector search should return an empty list
|
|
|
|
Scenario: ScopedBackendView proxies graph search with scope injected
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
And sv a ScopedBackendView from that scope
|
|
And sv an InMemoryGraphBackend
|
|
When sv I search graph for "SELECT ?s WHERE { ?s a uko:Container }"
|
|
Then sv the graph search should return an empty GraphResult
|
|
|
|
# ---- ScopedBackendView: effective_scope ----
|
|
|
|
Scenario: effective_scope returns resource_ids minus denied
|
|
Given sv a triple-resource ScopedBackendView for "p" with "R1", "R2", "R3" denying "R2"
|
|
Then sv effective_scope should contain "R1" and "R3" but not "R2"
|
|
|
|
Scenario: effective_scope is empty when all resources denied
|
|
Given sv a single-deny ScopedBackendView for "p" allow "R1" deny "R1"
|
|
Then sv effective_scope should be empty
|
|
|
|
# ---- ScopedBackendView: from_resource_scope ----
|
|
|
|
Scenario: from_resource_scope creates a view with scope's IDs and projects
|
|
Given sv a ResourceScope with two resources "RES01", "RES02" and two projects "local/api", "local/web"
|
|
When sv I create a ScopedBackendView from that ResourceScope
|
|
Then sv the view should allow project "local/api"
|
|
And sv the view should allow project "local/web"
|
|
And sv the view resource_ids should contain "RES01" and "RES02"
|
|
|
|
# ---- ScopedBackendSet ----
|
|
|
|
Scenario: ScopedBackendSet wraps multiple backends
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
And sv a ScopedBackendSet with text, vector, and graph backends
|
|
When sv I search text via backend set for "test query"
|
|
Then sv the backend set text search should succeed
|
|
|
|
Scenario: ScopedBackendSet with no text backend returns empty list
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
And sv a ScopedBackendSet with no text backend
|
|
When sv I search text via backend set for "test query"
|
|
Then sv the backend set text search should return empty list
|
|
|
|
# ---- ScopeViolationError ----
|
|
|
|
Scenario: ScopeViolationError carries resource and project info
|
|
When sv I raise a ScopeViolationError with resource "RES01" and projects "proj-a"
|
|
Then sv the error resource_ids should contain "RES01"
|
|
And sv the error scope_project_names should contain "proj-a"
|
|
|
|
# ---- validate_project_scope ----
|
|
|
|
Scenario: validate_project_scope passes for valid projects
|
|
Then sv validating projects "proj-a" against available "proj-a", "proj-b" should pass
|
|
|
|
Scenario: validate_project_scope rejects out-of-scope projects
|
|
Then sv validating projects "proj-c" against available "proj-a", "proj-b" should raise ScopeViolationError
|
|
|
|
Scenario: validate_project_scope rejects empty requested projects
|
|
Then sv validating empty requested projects should raise ValueError
|
|
|
|
# ---- validate_resource_scope ----
|
|
|
|
Scenario: validate_resource_scope passes for in-scope resources
|
|
Given sv a ResourceScope with resources "RES01", "RES02" and projects "local/api"
|
|
Then sv validating resources "RES01" against that scope should pass
|
|
|
|
Scenario: validate_resource_scope rejects out-of-scope resources
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
Then sv validating resources "RES99" against that scope should raise ScopeViolationError
|
|
|
|
# ---- ResourceAliasResolver ----
|
|
|
|
Scenario: Resolve alias to resource_id
|
|
Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
|
|
And sv a ResourceAliasResolver
|
|
When sv I resolve alias "repo"
|
|
Then sv the resolved resource_id should be "01HQ8ZDRX50000000000000001"
|
|
|
|
Scenario: Resolve by resource_id directly
|
|
Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
|
|
And sv a ResourceAliasResolver
|
|
When sv I resolve alias "01HQ8ZDRX50000000000000001"
|
|
Then sv the resolved resource_id should be "01HQ8ZDRX50000000000000001"
|
|
|
|
Scenario: Resolve unknown alias returns None
|
|
Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
|
|
And sv a ResourceAliasResolver
|
|
When sv I resolve alias "nonexistent"
|
|
Then sv the resolved resource_id should be None
|
|
|
|
Scenario: Resolve rejects empty input
|
|
Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
|
|
And sv a ResourceAliasResolver
|
|
Then sv resolving empty alias should raise ValueError
|
|
|
|
Scenario: resolve_many resolves multiple aliases
|
|
Given sv a project "local/myproj" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
|
|
And sv a ResourceAliasResolver
|
|
When sv I resolve many aliases "repo", "docs"
|
|
Then sv the resolved set should contain "01HQ8ZDRX50000000000000001" and "01HQ8ZDRX50000000000000002"
|
|
|
|
Scenario: validate_uniqueness detects duplicate aliases
|
|
Given sv a project "local/myproj" with duplicate alias "repo" on "01HQ8ZDRX50000000000000001" and "01HQ8ZDRX50000000000000002"
|
|
And sv a ResourceAliasResolver
|
|
When sv I validate alias uniqueness
|
|
Then sv there should be 1 uniqueness error mentioning "repo"
|
|
|
|
Scenario: validate_uniqueness passes for unique aliases
|
|
Given sv a project "local/myproj" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
|
|
And sv a ResourceAliasResolver
|
|
When sv I validate alias uniqueness
|
|
Then sv there should be 0 uniqueness errors
|
|
|
|
# ---- resolve_resource_scope ----
|
|
|
|
Scenario: resolve_resource_scope collects all resources from projects
|
|
Given sv a project "local/api" with linked resources "01HQ8ZDRX50000000000000001", "01HQ8ZDRX50000000000000002"
|
|
When sv I resolve the resource scope
|
|
Then sv the scope should have 2 resource_ids
|
|
And sv the scope should contain resource "01HQ8ZDRX50000000000000001"
|
|
And sv the scope should contain resource "01HQ8ZDRX50000000000000002"
|
|
|
|
Scenario: resolve_resource_scope applies include allowlist
|
|
Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
|
|
When sv I resolve the resource scope with include "repo"
|
|
Then sv the scope should have 1 resource_ids
|
|
And sv the scope should contain resource "01HQ8ZDRX50000000000000001"
|
|
|
|
Scenario: resolve_resource_scope applies exclude denylist
|
|
Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
|
|
When sv I resolve the resource scope with exclude "docs"
|
|
Then sv the scope should have 1 resource_ids
|
|
And sv the scope should contain resource "01HQ8ZDRX50000000000000001"
|
|
|
|
Scenario: resolve_resource_scope rejects empty projects
|
|
Then sv resolving scope with empty projects should raise ValueError
|
|
|
|
# ---- ContextTierService: scope enforcement ----
|
|
|
|
Scenario: get_scoped_by_resource filters by ResourceScope
|
|
Given sv a ContextTierService
|
|
And sv a ResourceScope with resources "RES01" and projects "proj-a"
|
|
When sv I store fragment "f1" project "proj-a" resource "RES01" tier "hot"
|
|
And sv I store fragment "f2" project "proj-b" resource "RES02" tier "hot"
|
|
Then sv get_scoped_by_resource should return 1 fragment
|
|
|
|
Scenario: store_with_scope_check rejects out-of-scope resource
|
|
Given sv a ContextTierService
|
|
And sv a ResourceScope with resources "RES01" and projects "proj-a"
|
|
Then sv storing fragment with resource "RES99" should raise ScopeViolationError
|
|
|
|
Scenario: store_with_scope_check rejects out-of-scope project
|
|
Given sv a ContextTierService
|
|
And sv a ResourceScope with resources "RES01" and projects "proj-a"
|
|
Then sv storing fragment with project "evil-proj" and resource "RES01" should raise ScopeViolationError
|
|
|
|
Scenario: validate_fragment_scope passes for in-scope fragment
|
|
Given sv a ContextTierService
|
|
And sv a ResourceScope with resources "RES01" and projects "proj-a"
|
|
When sv I store fragment "f3" project "proj-a" resource "RES01" tier "hot"
|
|
Then sv validating fragment "f3" against the scope should pass
|
|
|
|
Scenario: validate_fragment_scope rejects out-of-scope fragment
|
|
Given sv a ContextTierService
|
|
And sv a ResourceScope with resources "RES01" and projects "proj-a"
|
|
When sv I store fragment "f4" project "proj-b" resource "RES02" tier "hot"
|
|
Then sv validating fragment "f4" against the scope should raise ScopeViolationError
|
|
|
|
# ---- ScopedBackendSet: vector and graph ----
|
|
|
|
Scenario: ScopedBackendSet proxies vector search
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
And sv a ScopedBackendSet with text, vector, and graph backends
|
|
When sv I search vector via backend set
|
|
Then sv the backend set vector search should return empty list
|
|
|
|
Scenario: ScopedBackendSet proxies graph search
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
And sv a ScopedBackendSet with text, vector, and graph backends
|
|
When sv I search graph via backend set for "SELECT ?s WHERE { ?s a uko:X }"
|
|
Then sv the backend set graph search should return empty GraphResult
|
|
|
|
# ---- Mixed project scope guard ----
|
|
|
|
Scenario: Mixed project scope guard catches unlinked projects
|
|
Then sv validating mixed projects "proj-a", "proj-c" against available "proj-a", "proj-b" should raise ScopeViolationError
|
|
|
|
# ---- Coverage: field validator coercion (scoped_view L226, L237) ----
|
|
|
|
Scenario: ScopedBackendView coerces list resource_ids to frozenset
|
|
Then sv constructing view with list resource_ids should succeed
|
|
|
|
Scenario: ScopedBackendView coerces list denied_resource_ids to frozenset
|
|
Then sv constructing view with list denied_resource_ids should succeed
|
|
|
|
# ---- Coverage: effective_scope project-only (scoped_view L273) ----
|
|
|
|
Scenario: effective_scope returns empty for project-only view
|
|
Given sv a project-only ScopedBackendView for "proj-a"
|
|
Then sv effective_scope should be empty
|
|
|
|
# ---- Coverage: _require_effective_scope raises (scoped_view L326-330) ----
|
|
|
|
Scenario: search_text raises when all resources are denied
|
|
Given sv a single-deny ScopedBackendView for "p" allow "R1" deny "R1"
|
|
And sv an InMemoryTextBackend
|
|
Then sv searching text via denied view should raise ScopeViolationError
|
|
|
|
# ---- Coverage: ScopedBackendSet None backends (scoped_view L489, L505) ----
|
|
|
|
Scenario: ScopedBackendSet search_vector with no vector backend
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
And sv a ScopedBackendSet with no vector backend
|
|
Then sv searching vector via no-vector set should return empty list
|
|
|
|
Scenario: ScopedBackendSet search_graph with no graph backend
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
And sv a ScopedBackendSet with no graph backend
|
|
Then sv searching graph via no-graph set should return empty GraphResult
|
|
|
|
# ---- Coverage: resolve_many unresolvable alias (scoped_view L617) ----
|
|
|
|
Scenario: resolve_many skips unresolvable aliases
|
|
Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
|
|
And sv a ResourceAliasResolver
|
|
When sv I resolve many aliases "repo", "nonexistent"
|
|
Then sv the resolved set size should be 1
|
|
And sv the resolved set should contain "01HQ8ZDRX50000000000000001"
|
|
|
|
# ---- Coverage: validate_uniqueness no-alias branch (scoped_view L638) ----
|
|
|
|
Scenario: validate_uniqueness ignores resources without aliases
|
|
Given sv a project with aliased and unaliased resources
|
|
And sv a ResourceAliasResolver
|
|
When sv I validate alias uniqueness
|
|
Then sv there should be 0 uniqueness errors
|
|
|
|
# ---- Coverage: validate_project_scope empty available (scoped_view L676) ----
|
|
|
|
Scenario: validate_project_scope rejects empty available projects
|
|
Then sv validating projects against empty available should raise ValueError
|
|
|
|
# ---- Coverage: validate_resource_scope empty request (scoped_view L710) ----
|
|
|
|
Scenario: validate_resource_scope passes for empty requested resources
|
|
Given sv a ResourceScope with resources "RES01" and projects "local/api"
|
|
Then sv validating empty resources against scope should pass
|
|
|
|
# ---- Coverage: resolve_resource_scope empty after filter (scoped_view L790) ----
|
|
|
|
Scenario: resolve_resource_scope produces empty after include filtering
|
|
Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
|
|
When sv I resolve the resource scope with include "nonexistent"
|
|
Then sv the scope should have 0 resource_ids
|
|
|
|
# ---- Coverage: context_tiers store empty id (L97) ----
|
|
|
|
Scenario: store rejects empty fragment_id
|
|
Given sv a ContextTierService
|
|
Then sv storing fragment with empty id should raise ValueError
|
|
|
|
# ---- Coverage: context_tiers get_scoped_view empty (L191) ----
|
|
|
|
Scenario: get_scoped_view with empty project_names returns empty
|
|
Given sv a ContextTierService
|
|
Then sv get_scoped_view with empty projects should return empty list
|
|
|
|
# ---- Coverage: context_tiers evict_lru empty store (L269) ----
|
|
|
|
Scenario: evict_lru on empty tier returns empty list
|
|
Given sv a ContextTierService
|
|
Then sv evict_lru on empty cold tier should return empty list
|
|
|
|
# ---- Coverage: context_tiers get_scoped_metrics (L311-316) ----
|
|
|
|
Scenario: get_scoped_metrics counts fragments by project
|
|
Given sv a ContextTierService
|
|
When sv I store fragment "m1" project "proj-a" resource "RES01" tier "hot"
|
|
And sv I store fragment "m2" project "proj-b" resource "RES02" tier "warm"
|
|
Then sv get_scoped_metrics for "proj-a" should show 1 hot and 0 warm
|
|
|
|
# ---- Coverage: context_tiers get_scoped_by_resource 0 rejected (L355) ----
|
|
|
|
Scenario: get_scoped_by_resource returns all when none rejected
|
|
Given sv a ContextTierService
|
|
And sv a ResourceScope with resources "RES01" and projects "proj-a"
|
|
When sv I store fragment "g1" project "proj-a" resource "RES01" tier "hot"
|
|
Then sv get_scoped_by_resource should return 1 fragment
|
|
|
|
# ---- Coverage: context_tiers validate_fragment_scope not found (L382) ----
|
|
|
|
Scenario: validate_fragment_scope rejects non-existent fragment
|
|
Given sv a ContextTierService
|
|
And sv a ResourceScope with resources "RES01" and projects "proj-a"
|
|
Then sv validating non-existent fragment should raise ValueError
|
|
|
|
# ---- Coverage: context_tiers store_with_scope_check happy path (L434) ----
|
|
|
|
Scenario: store_with_scope_check stores in-scope fragment successfully
|
|
Given sv a ContextTierService
|
|
And sv a ResourceScope with resources "RES01" and projects "proj-a"
|
|
Then sv store_with_scope_check should accept in-scope fragment
|
|
|
|
# ---- Coverage: context_tiers budget property (L443) ----
|
|
|
|
Scenario: tier budget property returns TierBudget
|
|
Given sv a ContextTierService
|
|
Then sv tier service budget should be a TierBudget
|
|
|
|
# ---- Coverage: context_tiers get_for_actor no project filter (L161) ----
|
|
|
|
Scenario: get_for_actor without project filter returns fragments
|
|
Given sv a ContextTierService
|
|
When sv I store fragment "a1" project "proj-a" resource "RES01" tier "hot"
|
|
Then sv get_for_actor without projects should return a list
|
|
|
|
# ---- Coverage: context_tiers _summarize_for_cold false branch (L481) ----
|
|
|
|
Scenario: demote warm fragment with short content to cold
|
|
Given sv a ContextTierService
|
|
When sv I store a short warm fragment "sf1"
|
|
Then sv demoting "sf1" to cold should keep content intact
|
|
|
|
# ---- P0-S1: is_visible denies empty resource_id when resource_ids set ----
|
|
|
|
Scenario: is_visible denies fragment with empty resource_id when resource filtering active
|
|
Given sv a ScopedBackendView for projects "proj-a" with resource_ids "RES01"
|
|
When sv I check visibility of a fragment with project "proj-a" and empty resource_id
|
|
Then sv the fragment should NOT be visible
|
|
|
|
Scenario: is_visible allows fragment with empty resource_id when no resource filtering
|
|
Given sv a ScopedBackendView for project "proj-a"
|
|
When sv I check visibility of a fragment with project "proj-a" and empty resource_id
|
|
Then sv the fragment should be visible
|
|
|
|
# ---- P0-S2: get_scoped enforces scope ----
|
|
|
|
Scenario: get_scoped returns fragment when in scope
|
|
Given sv a ContextTierService
|
|
And sv a ResourceScope with resources "RES01" and projects "proj-a"
|
|
When sv I store fragment "f1" project "proj-a" resource "RES01" tier "hot"
|
|
Then sv get_scoped for "f1" should return the fragment
|
|
|
|
Scenario: get_scoped returns None for out-of-scope fragment
|
|
Given sv a ContextTierService
|
|
And sv a ResourceScope with resources "RES01" and projects "proj-a"
|
|
When sv I store fragment "f2" project "proj-b" resource "RES02" tier "hot"
|
|
Then sv get_scoped for "f2" should return None
|
|
|
|
# ---- P1-SPEC1: DAG expansion via registry_lookup ----
|
|
|
|
Scenario: resolve_resource_scope expands child resources via registry_lookup
|
|
Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
|
|
When sv I resolve scope with registry_lookup expanding "01HQ8ZDRX50000000000000001" to "01HQ8ZDRX50000000000000099"
|
|
Then sv the scope should have 3 resource_ids
|
|
And sv the scope should contain resource "01HQ8ZDRX50000000000000099"
|
|
|
|
# ---- P2-B2: tuple coercion in field validators ----
|
|
|
|
Scenario: ScopedBackendView accepts tuple for allowed_projects
|
|
When sv I create a ScopedBackendView with tuple allowed_projects
|
|
Then sv the view should have 1 allowed project
|
|
|
|
# ---- P0-S2: get_scoped with empty resource_id denied ----
|
|
|
|
Scenario: get_scoped denies fragment with empty resource_id when scope has resources
|
|
Given sv a ContextTierService
|
|
And sv a ResourceScope with resources "RES01" and projects "proj-a"
|
|
When sv I store fragment "f3" with project "proj-a" and empty resource_id in tier "hot"
|
|
Then sv get_scoped for "f3" should return None
|