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
921 lines
30 KiB
Python
921 lines
30 KiB
Python
"""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
|