"""Step definitions for scoped backend view filtering. All steps prefixed with ``sv`` to avoid AmbiguousStep collisions with existing step files in the project. """ from __future__ import annotations from typing import Any from behave import given, then, when # type: ignore[import-untyped] from cleveragents.domain.models.acms.scope_resolution import ( ResourceAliasResolver, resolve_resource_scope, validate_project_scope, validate_resource_scope, ) from cleveragents.domain.models.acms.scoped_view import ( ResourceScope, ScopedBackendView, ScopeViolationError, create_scoped_backend_set, ) from cleveragents.domain.models.acms.stubs import ( InMemoryGraphBackend, InMemoryTextBackend, InMemoryVectorBackend, ) from cleveragents.domain.models.acms.tiers import ( ContextTier, TieredFragment, ) __all__: list[str] = [] # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_fragment( fid: str, project: str = "", resource: str = "", tier: str = "hot", ) -> TieredFragment: """Create a TieredFragment for testing.""" return TieredFragment( fragment_id=fid, content=f"content-{fid}", tier=ContextTier(tier), project_name=project, resource_id=resource, ) def _make_project( name: str, namespace: str = "local", links: list[tuple[str, str | None]] | None = None, ) -> Any: """Create a NamespacedProject with linked resources.""" from cleveragents.domain.models.core.project import ( LinkedResource, NamespacedProject, ) linked: list[LinkedResource] = [] if links: for resource_id, alias in links: linked.append(LinkedResource(resource_id=resource_id, alias=alias)) return NamespacedProject( name=name, namespace=namespace, linked_resources=linked, ) # --------------------------------------------------------------------------- # ResourceScope # --------------------------------------------------------------------------- @given('sv a ResourceScope with resources "{res1}", "{res2}" and projects "{proj}"') def step_sv_scope_two_res_one_proj( context: Any, res1: str, res2: str, proj: str ) -> None: context.sv_scope = ResourceScope( resource_ids=frozenset({res1, res2}), project_names=frozenset({proj}), ) @given('sv a ResourceScope with resources "{res1}" and projects "{proj}"') def step_sv_scope_one_res_one_proj(context: Any, res1: str, proj: str) -> None: context.sv_scope = ResourceScope( resource_ids=frozenset({res1}), project_names=frozenset({proj}), ) @then('sv the scope should contain resource "{res}"') def step_sv_scope_contains_res(context: Any, res: str) -> None: assert context.sv_scope.contains_resource(res), ( f"Expected scope to contain resource '{res}'" ) @then('sv the scope should not contain resource "{res}"') def step_sv_scope_not_contains_res(context: Any, res: str) -> None: assert not context.sv_scope.contains_resource(res), ( f"Expected scope NOT to contain resource '{res}'" ) @then('sv the scope should contain project "{proj}"') def step_sv_scope_contains_proj(context: Any, proj: str) -> None: assert context.sv_scope.contains_project(proj) @then('sv the scope should not contain project "{proj}"') def step_sv_scope_not_contains_proj(context: Any, proj: str) -> None: assert not context.sv_scope.contains_project(proj) @then("sv modifying the scope should raise an error") def step_sv_scope_frozen(context: Any) -> None: from pydantic import ValidationError try: context.sv_scope.resource_ids = frozenset({"NEW"}) raise AssertionError("Expected frozen model error") except (ValidationError, AttributeError, TypeError): pass @then('sv temporal_scope should be "{expected}"') def step_sv_temporal_scope(context: Any, expected: str) -> None: assert context.sv_scope.temporal_scope == expected @then('sv creating a scope with temporal_scope "{ts}" should raise ValidationError') def step_sv_scope_bad_temporal(context: Any, ts: str) -> None: from pydantic import ValidationError try: ResourceScope( resource_ids=frozenset({"R1"}), project_names=frozenset({"p1"}), temporal_scope=ts, ) raise AssertionError("Expected ValidationError") except ValidationError: pass @then("sv creating a scope with no resources should succeed") def step_sv_scope_empty_resources(context: Any) -> None: scope = ResourceScope( resource_ids=frozenset(), project_names=frozenset({"p1"}), ) assert len(scope.resource_ids) == 0 assert scope.contains_project("p1") @then("sv creating a scope with no projects should raise ValidationError") def step_sv_scope_no_projects(context: Any) -> None: from pydantic import ValidationError try: ResourceScope( resource_ids=frozenset({"R1"}), project_names=frozenset(), ) raise AssertionError("Expected ValidationError") except ValidationError: pass # --------------------------------------------------------------------------- # ResourceScope path filtering # --------------------------------------------------------------------------- @given('sv a ResourceScope with include_paths "{p1}", "{p2}"') def step_sv_scope_include_paths(context: Any, p1: str, p2: str) -> None: context.sv_scope = ResourceScope( resource_ids=frozenset({"R1"}), project_names=frozenset({"p1"}), include_paths=(p1, p2), ) @given('sv a ResourceScope with exclude_paths "{p1}", "{p2}"') def step_sv_scope_exclude_paths(context: Any, p1: str, p2: str) -> None: context.sv_scope = ResourceScope( resource_ids=frozenset({"R1"}), project_names=frozenset({"p1"}), exclude_paths=(p1, p2), ) @given('sv a ResourceScope with include_paths "{inc}" and exclude_paths "{exc}"') def step_sv_scope_inc_exc_paths(context: Any, inc: str, exc: str) -> None: context.sv_scope = ResourceScope( resource_ids=frozenset({"R1"}), project_names=frozenset({"p1"}), include_paths=(inc,), exclude_paths=(exc,), ) @then('sv path "{path}" should match') def step_sv_path_matches(context: Any, path: str) -> None: assert context.sv_scope.matches_path(path), ( f"Expected path '{path}' to match scope filters" ) @then('sv path "{path}" should not match') def step_sv_path_not_matches(context: Any, path: str) -> None: assert not context.sv_scope.matches_path(path), ( f"Expected path '{path}' NOT to match scope filters" ) # --------------------------------------------------------------------------- # ScopedBackendView: fragment-level filtering # --------------------------------------------------------------------------- @given('sv a project-only ScopedBackendView for "{proj}"') def step_sv_view_one_proj(context: Any, proj: str) -> None: context.sv_view = ScopedBackendView( allowed_projects=frozenset({proj}), ) context.sv_fragments = {} # dict[str, TieredFragment] @given('sv a project-and-resource ScopedBackendView for "{proj}" resources "{res}"') def step_sv_view_proj_res(context: Any, proj: str, res: str) -> None: context.sv_view = ScopedBackendView( allowed_projects=frozenset({proj}), resource_ids=frozenset({res}), ) context.sv_fragments = {} @given( 'sv a deny-resource ScopedBackendView for "{proj}" allow "{r1}", "{r2}" deny "{d1}"' ) def step_sv_view_proj_res_denied( context: Any, proj: str, r1: str, r2: str, d1: str ) -> None: context.sv_view = ScopedBackendView( allowed_projects=frozenset({proj}), resource_ids=frozenset({r1, r2}), denied_resource_ids=frozenset({d1}), ) context.sv_fragments = {} @given('sv a fragment "{fid}" in project "{proj}" with resource "{res}"') def step_sv_fragment_proj_res(context: Any, fid: str, proj: str, res: str) -> None: context.sv_fragments[fid] = _make_fragment(fid, project=proj, resource=res) @given('sv a fragment "{fid}" with no project') def step_sv_fragment_no_proj(context: Any, fid: str) -> None: context.sv_fragments[fid] = _make_fragment(fid, project="") @then('sv fragment "{fid}" should be visible') def step_sv_fragment_visible(context: Any, fid: str) -> None: frag = context.sv_fragments[fid] assert context.sv_view.is_visible(frag), f"Expected fragment '{fid}' to be visible" @then('sv fragment "{fid}" should not be visible') def step_sv_fragment_not_visible(context: Any, fid: str) -> None: frag = context.sv_fragments[fid] assert not context.sv_view.is_visible(frag), ( f"Expected fragment '{fid}' NOT to be visible" ) # --------------------------------------------------------------------------- # ScopedBackendView: backend search proxying # --------------------------------------------------------------------------- @given("sv a ScopedBackendView from that scope") def step_sv_view_from_scope(context: Any) -> None: context.sv_view = ScopedBackendView.from_resource_scope(context.sv_scope) context.sv_fragments = {} @given("sv an InMemoryTextBackend") def step_sv_text_backend(context: Any) -> None: context.sv_text_backend = InMemoryTextBackend() @given("sv an InMemoryVectorBackend") def step_sv_vector_backend(context: Any) -> None: context.sv_vector_backend = InMemoryVectorBackend() @given("sv an InMemoryGraphBackend") def step_sv_graph_backend(context: Any) -> None: context.sv_graph_backend = InMemoryGraphBackend() @when('sv I search text for "{query}"') def step_sv_search_text(context: Any, query: str) -> None: context.sv_text_results = context.sv_view.search_text( context.sv_text_backend, query ) context.sv_search_error = None @then("sv the text search should return an empty list") def step_sv_text_empty(context: Any) -> None: assert context.sv_text_results == [] @then("sv the text search should not raise an error") def step_sv_text_no_error(context: Any) -> None: assert context.sv_search_error is None @when("sv I search vector with embedding") def step_sv_search_vector(context: Any) -> None: context.sv_vector_results = context.sv_view.search_vector( context.sv_vector_backend, [0.1, 0.2, 0.3] ) @then("sv the vector search should return an empty list") def step_sv_vector_empty(context: Any) -> None: assert context.sv_vector_results == [] @when('sv I search graph for "{query}"') def step_sv_search_graph(context: Any, query: str) -> None: context.sv_graph_result = context.sv_view.search_graph( context.sv_graph_backend, query ) @then("sv the graph search should return an empty GraphResult") def step_sv_graph_empty(context: Any) -> None: assert context.sv_graph_result.triples == [] # --------------------------------------------------------------------------- # effective_scope # --------------------------------------------------------------------------- @given( 'sv a triple-resource ScopedBackendView for "{proj}" with "{r1}", "{r2}", "{r3}" denying "{d}"' ) def step_sv_view_three_res_denied( context: Any, proj: str, r1: str, r2: str, r3: str, d: str ) -> None: context.sv_view = ScopedBackendView( allowed_projects=frozenset({proj}), resource_ids=frozenset({r1, r2, r3}), denied_resource_ids=frozenset({d}), ) context.sv_fragments = {} @given('sv a single-deny ScopedBackendView for "{proj}" allow "{r}" deny "{d}"') def step_sv_view_one_res_denied(context: Any, proj: str, r: str, d: str) -> None: context.sv_view = ScopedBackendView( allowed_projects=frozenset({proj}), resource_ids=frozenset({r}), denied_resource_ids=frozenset({d}), ) context.sv_fragments = {} @then('sv effective_scope should contain "{r1}" and "{r2}" but not "{r3}"') def step_sv_effective_contains_not(context: Any, r1: str, r2: str, r3: str) -> None: eff = context.sv_view.effective_scope() assert r1 in eff, f"Expected {r1} in effective_scope" assert r2 in eff, f"Expected {r2} in effective_scope" assert r3 not in eff, f"Expected {r3} NOT in effective_scope" @then("sv effective_scope should be empty") def step_sv_effective_empty(context: Any) -> None: eff = context.sv_view.effective_scope() assert len(eff) == 0, f"Expected empty effective_scope, got {eff}" # --------------------------------------------------------------------------- # from_resource_scope # --------------------------------------------------------------------------- @when("sv I create a ScopedBackendView from that ResourceScope") def step_sv_create_view_from_scope(context: Any) -> None: context.sv_view = ScopedBackendView.from_resource_scope(context.sv_scope) @then('sv the view should allow project "{proj}"') def step_sv_view_allows_proj(context: Any, proj: str) -> None: assert proj in context.sv_view.allowed_projects @then('sv the view resource_ids should contain "{r1}" and "{r2}"') def step_sv_view_res_contains(context: Any, r1: str, r2: str) -> None: assert r1 in context.sv_view.resource_ids assert r2 in context.sv_view.resource_ids # --------------------------------------------------------------------------- # ScopedBackendSet # --------------------------------------------------------------------------- @given("sv a ScopedBackendSet with text, vector, and graph backends") def step_sv_backend_set_all(context: Any) -> None: context.sv_backend_set = create_scoped_backend_set( context.sv_scope, text_backend=InMemoryTextBackend(), vector_backend=InMemoryVectorBackend(), graph_backend=InMemoryGraphBackend(), ) @given("sv a ScopedBackendSet with no text backend") def step_sv_backend_set_no_text(context: Any) -> None: context.sv_backend_set = create_scoped_backend_set( context.sv_scope, text_backend=None, vector_backend=InMemoryVectorBackend(), graph_backend=InMemoryGraphBackend(), ) @when('sv I search text via backend set for "{query}"') def step_sv_backend_set_search(context: Any, query: str) -> None: context.sv_backend_set_results = context.sv_backend_set.search_text(query) @then("sv the backend set text search should succeed") def step_sv_backend_set_ok(context: Any) -> None: assert isinstance(context.sv_backend_set_results, list) @then("sv the backend set text search should return empty list") def step_sv_backend_set_empty(context: Any) -> None: assert context.sv_backend_set_results == [] @when("sv I search vector via backend set") def step_sv_backend_set_vector(context: Any) -> None: context.sv_backend_set_vec_results = context.sv_backend_set.search_vector( [0.1, 0.2, 0.3] ) @then("sv the backend set vector search should return empty list") def step_sv_backend_set_vec_empty(context: Any) -> None: assert context.sv_backend_set_vec_results == [] @when('sv I search graph via backend set for "{query}"') def step_sv_backend_set_graph(context: Any, query: str) -> None: context.sv_backend_set_graph_result = context.sv_backend_set.search_graph(query) @then("sv the backend set graph search should return empty GraphResult") def step_sv_backend_set_graph_empty(context: Any) -> None: assert context.sv_backend_set_graph_result.triples == [] # --------------------------------------------------------------------------- # ScopeViolationError # --------------------------------------------------------------------------- @when('sv I raise a ScopeViolationError with resource "{res}" and projects "{proj}"') def step_sv_raise_scope_error(context: Any, res: str, proj: str) -> None: context.sv_error = ScopeViolationError( "test error", resource_ids=(res,), scope_project_names=(proj,), ) @then('sv the error resource_ids should contain "{res}"') def step_sv_error_res(context: Any, res: str) -> None: assert res in context.sv_error.resource_ids @then('sv the error scope_project_names should contain "{proj}"') def step_sv_error_proj(context: Any, proj: str) -> None: assert proj in context.sv_error.scope_project_names # --------------------------------------------------------------------------- # validate_project_scope # --------------------------------------------------------------------------- @then('sv validating projects "{req}" against available "{a1}", "{a2}" should pass') def step_sv_validate_proj_pass(context: Any, req: str, a1: str, a2: str) -> None: validate_project_scope(frozenset({req}), frozenset({a1, a2})) @then( 'sv validating projects "{req}" against available "{a1}", "{a2}" should raise ScopeViolationError' ) def step_sv_validate_proj_fail_one(context: Any, req: str, a1: str, a2: str) -> None: try: validate_project_scope(frozenset({req}), frozenset({a1, a2})) raise AssertionError("Expected ScopeViolationError") except ScopeViolationError: pass @then( 'sv validating mixed projects "{r1}", "{r2}" against available "{a1}", "{a2}" should raise ScopeViolationError' ) def step_sv_validate_proj_fail_two( context: Any, r1: str, r2: str, a1: str, a2: str ) -> None: try: validate_project_scope(frozenset({r1, r2}), frozenset({a1, a2})) raise AssertionError("Expected ScopeViolationError") except ScopeViolationError: pass @then("sv validating empty requested projects should raise ValueError") def step_sv_validate_proj_empty(context: Any) -> None: try: validate_project_scope(frozenset(), frozenset({"p1"})) raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # validate_resource_scope # --------------------------------------------------------------------------- @then('sv validating resources "{res}" against that scope should pass') def step_sv_validate_res_pass(context: Any, res: str) -> None: validate_resource_scope(frozenset({res}), context.sv_scope) @then( 'sv validating resources "{res}" against that scope should raise ScopeViolationError' ) def step_sv_validate_res_fail(context: Any, res: str) -> None: try: validate_resource_scope(frozenset({res}), context.sv_scope) raise AssertionError("Expected ScopeViolationError") except ScopeViolationError: pass # --------------------------------------------------------------------------- # ResourceAliasResolver # --------------------------------------------------------------------------- @given('sv a project "{ns}/{name}" with resource "{res}" aliased "{alias}"') def step_sv_proj_one_alias( context: Any, ns: str, name: str, res: str, alias: str ) -> None: context.sv_project = _make_project(name, namespace=ns, links=[(res, alias)]) @given( 'sv a ResourceScope with two resources "{r1}", "{r2}" and two projects "{p1}", "{p2}"' ) def step_sv_scope_two_res_two_proj( context: Any, r1: str, r2: str, p1: str, p2: str ) -> None: context.sv_scope = ResourceScope( resource_ids=frozenset({r1, r2}), project_names=frozenset({p1, p2}), ) @given( 'sv a project "{ns}/{name}" with two resources "{r1}" aliased "{a1}" and "{r2}" aliased "{a2}"' ) def step_sv_proj_two_aliases( context: Any, ns: str, name: str, r1: str, a1: str, r2: str, a2: str ) -> None: context.sv_project = _make_project(name, namespace=ns, links=[(r1, a1), (r2, a2)]) @given('sv a project "{ns}/{name}" with duplicate alias "{alias}" on "{r1}" and "{r2}"') def step_sv_proj_dup_alias( context: Any, ns: str, name: str, alias: str, r1: str, r2: str ) -> None: context.sv_project = _make_project( name, namespace=ns, links=[(r1, alias), (r2, alias)] ) @given("sv a ResourceAliasResolver") def step_sv_resolver(context: Any) -> None: context.sv_resolver = ResourceAliasResolver() @when('sv I resolve alias "{alias}"') def step_sv_resolve_alias(context: Any, alias: str) -> None: context.sv_resolved = context.sv_resolver.resolve(context.sv_project, alias) @then('sv the resolved resource_id should be "{expected}"') def step_sv_resolved_is(context: Any, expected: str) -> None: assert context.sv_resolved == expected, ( f"Expected '{expected}', got '{context.sv_resolved}'" ) @then("sv the resolved resource_id should be None") def step_sv_resolved_none(context: Any) -> None: assert context.sv_resolved is None @then("sv resolving empty alias should raise ValueError") def step_sv_resolve_empty(context: Any) -> None: try: context.sv_resolver.resolve(context.sv_project, "") raise AssertionError("Expected ValueError") except ValueError: pass @when('sv I resolve many aliases "{a1}", "{a2}"') def step_sv_resolve_many(context: Any, a1: str, a2: str) -> None: context.sv_resolved_set = context.sv_resolver.resolve_many( context.sv_project, (a1, a2) ) @then('sv the resolved set should contain "{r1}" and "{r2}"') def step_sv_resolved_set_contains(context: Any, r1: str, r2: str) -> None: assert r1 in context.sv_resolved_set assert r2 in context.sv_resolved_set @then('sv the resolved set should contain "{r1}"') def step_sv_resolved_set_contains_one(context: Any, r1: str) -> None: assert r1 in context.sv_resolved_set @when("sv I validate alias uniqueness") def step_sv_validate_uniqueness(context: Any) -> None: context.sv_uniqueness_errors = context.sv_resolver.validate_uniqueness( context.sv_project ) @then('sv there should be {count:d} uniqueness error mentioning "{alias}"') def step_sv_uniqueness_errors_with_alias(context: Any, count: int, alias: str) -> None: assert len(context.sv_uniqueness_errors) == count if count > 0: assert any(alias in err for err in context.sv_uniqueness_errors) @then("sv there should be {count:d} uniqueness errors") def step_sv_uniqueness_errors_count(context: Any, count: int) -> None: assert len(context.sv_uniqueness_errors) == count # --------------------------------------------------------------------------- # resolve_resource_scope # --------------------------------------------------------------------------- @given('sv a project "{ns}/{name}" with linked resources "{r1}", "{r2}"') def step_sv_proj_linked_resources( context: Any, ns: str, name: str, r1: str, r2: str ) -> None: context.sv_project = _make_project( name, namespace=ns, links=[(r1, None), (r2, None)] ) @when("sv I resolve the resource scope") def step_sv_resolve_scope(context: Any) -> None: context.sv_scope = resolve_resource_scope([context.sv_project]) @when('sv I resolve the resource scope with include "{inc}"') def step_sv_resolve_scope_include(context: Any, inc: str) -> None: context.sv_scope = resolve_resource_scope( [context.sv_project], include_resources=(inc,) ) @when('sv I resolve the resource scope with exclude "{exc}"') def step_sv_resolve_scope_exclude(context: Any, exc: str) -> None: context.sv_scope = resolve_resource_scope( [context.sv_project], exclude_resources=(exc,) ) @then("sv the scope should have {count:d} resource_ids") def step_sv_scope_res_count(context: Any, count: int) -> None: assert len(context.sv_scope.resource_ids) == count, ( f"Expected {count} resource_ids, got {len(context.sv_scope.resource_ids)}: " f"{context.sv_scope.resource_ids}" ) @then("sv resolving scope with empty projects should raise ValueError") def step_sv_resolve_scope_empty(context: Any) -> None: try: resolve_resource_scope([]) raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # Coverage: field validator coercion (scoped_view L226, L237) # --------------------------------------------------------------------------- @then("sv constructing view with list resource_ids should succeed") def step_sv_view_list_res(context: Any) -> None: view = ScopedBackendView( allowed_projects=frozenset({"p"}), resource_ids=["R1", "R2"], # type: ignore[arg-type] ) assert "R1" in view.resource_ids assert isinstance(view.resource_ids, frozenset) @then("sv constructing view with list denied_resource_ids should succeed") def step_sv_view_list_denied(context: Any) -> None: view = ScopedBackendView( allowed_projects=frozenset({"p"}), resource_ids=frozenset({"R1", "R2"}), denied_resource_ids=["R2"], # type: ignore[arg-type] ) assert "R2" in view.denied_resource_ids assert isinstance(view.denied_resource_ids, frozenset) # --------------------------------------------------------------------------- # Coverage: _require_effective_scope raises (scoped_view L326-330) # --------------------------------------------------------------------------- @then("sv searching text via denied view should raise ScopeViolationError") def step_sv_search_text_denied(context: Any) -> None: try: context.sv_view.search_text(context.sv_text_backend, "test") raise AssertionError("Expected ScopeViolationError") except ScopeViolationError: pass # --------------------------------------------------------------------------- # Coverage: ScopedBackendSet None backends (scoped_view L489, L505) # --------------------------------------------------------------------------- @given("sv a ScopedBackendSet with no vector backend") def step_sv_backend_set_no_vector(context: Any) -> None: context.sv_backend_set_novector = create_scoped_backend_set( context.sv_scope, text_backend=InMemoryTextBackend(), vector_backend=None, graph_backend=InMemoryGraphBackend(), ) @then("sv searching vector via no-vector set should return empty list") def step_sv_novector_search(context: Any) -> None: result = context.sv_backend_set_novector.search_vector([0.1]) assert result == [] @given("sv a ScopedBackendSet with no graph backend") def step_sv_backend_set_no_graph(context: Any) -> None: context.sv_backend_set_nograph = create_scoped_backend_set( context.sv_scope, text_backend=InMemoryTextBackend(), vector_backend=InMemoryVectorBackend(), graph_backend=None, ) @then("sv searching graph via no-graph set should return empty GraphResult") def step_sv_nograph_search(context: Any) -> None: result = context.sv_backend_set_nograph.search_graph("SELECT ?s") assert result.triples == [] # --------------------------------------------------------------------------- # Coverage: resolve_many unresolvable (scoped_view L617) # --------------------------------------------------------------------------- @then("sv the resolved set size should be {count:d}") def step_sv_resolved_set_size(context: Any, count: int) -> None: assert len(context.sv_resolved_set) == count, ( f"Expected {count} entries, got {len(context.sv_resolved_set)}" ) # --------------------------------------------------------------------------- # Coverage: validate_uniqueness with no-alias resource (scoped_view L638) # --------------------------------------------------------------------------- @given("sv a project with aliased and unaliased resources") def step_sv_proj_mixed_alias(context: Any) -> None: context.sv_project = _make_project( "myproj", namespace="local", links=[ ("01HQ8ZDRX50000000000000001", "repo"), ("01HQ8ZDRX50000000000000002", None), ], ) # --------------------------------------------------------------------------- # Coverage: validate_project_scope empty available # --------------------------------------------------------------------------- @then("sv validating projects against empty available should raise ValueError") def step_sv_validate_proj_empty_avail(context: Any) -> None: try: validate_project_scope(frozenset({"p1"}), frozenset()) raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # Coverage: validate_resource_scope empty requested # --------------------------------------------------------------------------- @then("sv validating empty resources against scope should pass") def step_sv_validate_res_empty_req(context: Any) -> None: validate_resource_scope(frozenset(), context.sv_scope) # --------------------------------------------------------------------------- # P0-S1: is_visible empty resource_id deny-by-default # --------------------------------------------------------------------------- @given('sv a ScopedBackendView for projects "{proj}" with resource_ids "{rid}"') def step_sv_view_with_resources(context: Any, proj: str, rid: str) -> None: context.sv_view = ScopedBackendView( allowed_projects=frozenset([proj]), resource_ids=frozenset([rid]), ) @given('sv a ScopedBackendView for project "{proj}"') def step_sv_view_project_only(context: Any, proj: str) -> None: context.sv_view = ScopedBackendView( allowed_projects=frozenset([proj]), ) @when('sv I check visibility of a fragment with project "{proj}" and empty resource_id') def step_sv_check_empty_rid(context: Any, proj: str) -> None: frag = TieredFragment( fragment_id="empty-rid-frag", content="test", project_name=proj, resource_id="", tier=ContextTier.HOT, ) context.sv_vis_result = context.sv_view.is_visible(frag) @then("sv the fragment should NOT be visible") def step_sv_not_visible(context: Any) -> None: assert context.sv_vis_result is False @then("sv the fragment should be visible") def step_sv_is_visible(context: Any) -> None: assert context.sv_vis_result is True # --------------------------------------------------------------------------- # P2-B2: tuple coercion # --------------------------------------------------------------------------- @when("sv I create a ScopedBackendView with tuple allowed_projects") def step_sv_tuple_projects(context: Any) -> None: context.sv_view = ScopedBackendView( allowed_projects=("proj-a",), resource_ids=frozenset(), ) @then("sv the view should have {count:d} allowed project") def step_sv_view_project_count(context: Any, count: int) -> None: assert len(context.sv_view.allowed_projects) == count