Files
cleveragents-core/features/steps/scoped_view_tier_steps.py
hamza.khyari 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
feat(acms): add scoped backend view filtering
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
2026-03-06 00:49:33 +00:00

283 lines
9.6 KiB
Python

"""Step definitions for ContextTierService scope enforcement tests.
Extracted from ``scoped_view_steps.py`` to keep step files manageable.
All steps prefixed with ``sv`` to avoid AmbiguousStep collisions.
"""
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 (
resolve_resource_scope,
)
from cleveragents.domain.models.acms.scoped_view import (
ScopeViolationError,
)
from cleveragents.domain.models.acms.tiers import (
ContextTier,
TieredFragment,
)
__all__: list[str] = []
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,
)
# ---------------------------------------------------------------------------
# ContextTierService: scope enforcement
# ---------------------------------------------------------------------------
@given("sv a ContextTierService")
def step_sv_tier_service(context: Any) -> None:
from cleveragents.application.services.context_tiers import ContextTierService
context.sv_tier_svc = ContextTierService()
@when('sv I store fragment "{fid}" project "{proj}" resource "{res}" tier "{tier}"')
def step_sv_store_fragment(
context: Any, fid: str, proj: str, res: str, tier: str
) -> None:
frag = _make_fragment(fid, project=proj, resource=res, tier=tier)
context.sv_tier_svc.store(frag)
@then("sv get_scoped_by_resource should return {count:d} fragment")
def step_sv_scoped_by_resource(context: Any, count: int) -> None:
result = context.sv_tier_svc.get_scoped_by_resource(context.sv_scope)
assert len(result) == count, f"Expected {count} fragments, got {len(result)}"
@then('sv storing fragment with resource "{res}" should raise ScopeViolationError')
def step_sv_store_scope_check(context: Any, res: str) -> None:
frag = _make_fragment("bad-frag", project="proj-a", resource=res)
try:
context.sv_tier_svc.store_with_scope_check(frag, context.sv_scope)
raise AssertionError("Expected ScopeViolationError")
except ScopeViolationError:
pass
@then(
'sv storing fragment with project "{proj}" and resource "{res}"'
" should raise ScopeViolationError"
)
def step_sv_store_scope_check_project(context: Any, proj: str, res: str) -> None:
frag = _make_fragment("bad-proj-frag", project=proj, resource=res)
try:
context.sv_tier_svc.store_with_scope_check(frag, context.sv_scope)
raise AssertionError("Expected ScopeViolationError for out-of-scope project")
except ScopeViolationError:
pass
@then('sv validating fragment "{fid}" against the scope should pass')
def step_sv_validate_fragment(context: Any, fid: str) -> None:
context.sv_tier_svc.validate_fragment_scope(fid, context.sv_scope)
@then(
'sv validating fragment "{fid}" against the scope should raise ScopeViolationError'
)
def step_sv_validate_fragment_fail(context: Any, fid: str) -> None:
try:
context.sv_tier_svc.validate_fragment_scope(fid, context.sv_scope)
raise AssertionError("Expected ScopeViolationError")
except ScopeViolationError:
pass
# ---------------------------------------------------------------------------
# Coverage: context_tiers store empty id
# ---------------------------------------------------------------------------
@then("sv storing fragment with empty id should raise ValueError")
def step_sv_store_empty_id(context: Any) -> None:
frag = TieredFragment.model_construct(
fragment_id="",
content="content",
tier=ContextTier.HOT,
project_name="p",
resource_id="R1",
)
try:
context.sv_tier_svc.store(frag)
raise AssertionError("Expected ValueError")
except ValueError:
pass
# ---------------------------------------------------------------------------
# Coverage: context_tiers get_scoped_view empty
# ---------------------------------------------------------------------------
@then("sv get_scoped_view with empty projects should return empty list")
def step_sv_scoped_view_empty(context: Any) -> None:
result = context.sv_tier_svc.get_scoped_view([])
assert result == []
# ---------------------------------------------------------------------------
# Coverage: context_tiers evict_lru empty
# ---------------------------------------------------------------------------
@then("sv evict_lru on empty cold tier should return empty list")
def step_sv_evict_empty(context: Any) -> None:
result = context.sv_tier_svc.evict_lru(ContextTier.COLD, 1)
assert result == []
# ---------------------------------------------------------------------------
# Coverage: context_tiers get_scoped_metrics
# ---------------------------------------------------------------------------
@then('sv get_scoped_metrics for "{proj}" should show {h:d} hot and {w:d} warm')
def step_sv_scoped_metrics(context: Any, proj: str, h: int, w: int) -> None:
metrics = context.sv_tier_svc.get_scoped_metrics([proj])
assert metrics.hot_count == h, f"Expected hot={h}, got {metrics.hot_count}"
assert metrics.warm_count == w, f"Expected warm={w}, got {metrics.warm_count}"
# ---------------------------------------------------------------------------
# Coverage: context_tiers validate_fragment_scope not found
# ---------------------------------------------------------------------------
@then("sv validating non-existent fragment should raise ValueError")
def step_sv_validate_nonexistent(context: Any) -> None:
try:
context.sv_tier_svc.validate_fragment_scope("ghost", context.sv_scope)
raise AssertionError("Expected ValueError")
except ValueError:
pass
# ---------------------------------------------------------------------------
# Coverage: context_tiers store_with_scope_check happy path
# ---------------------------------------------------------------------------
@then("sv store_with_scope_check should accept in-scope fragment")
def step_sv_store_scope_ok(context: Any) -> None:
frag = _make_fragment("ok-frag", project="proj-a", resource="RES01")
context.sv_tier_svc.store_with_scope_check(frag, context.sv_scope)
result = context.sv_tier_svc.get("ok-frag")
assert result is not None
# ---------------------------------------------------------------------------
# Coverage: context_tiers budget property
# ---------------------------------------------------------------------------
@then("sv tier service budget should be a TierBudget")
def step_sv_budget(context: Any) -> None:
from cleveragents.domain.models.acms.tiers import TierBudget
budget = context.sv_tier_svc.budget
assert isinstance(budget, TierBudget)
# ---------------------------------------------------------------------------
# Coverage: context_tiers get_for_actor no project filter
# ---------------------------------------------------------------------------
@then("sv get_for_actor without projects should return a list")
def step_sv_actor_no_projects(context: Any) -> None:
from cleveragents.domain.models.acms.tiers import ActorRole
result = context.sv_tier_svc.get_for_actor(ActorRole.STRATEGIST)
assert isinstance(result, list)
# ---------------------------------------------------------------------------
# Coverage: context_tiers _summarize_for_cold false branch
# ---------------------------------------------------------------------------
@when('sv I store a short warm fragment "{fid}"')
def step_sv_store_short_warm(context: Any, fid: str) -> None:
frag = TieredFragment(
fragment_id=fid,
content="short",
tier=ContextTier.WARM,
project_name="proj-a",
resource_id="RES01",
)
context.sv_tier_svc.store(frag)
@then('sv demoting "{fid}" to cold should keep content intact')
def step_sv_demote_short(context: Any, fid: str) -> None:
result = context.sv_tier_svc.demote(fid)
assert result is not None
assert result.content == "short"
assert result.tier == ContextTier.COLD
# ---------------------------------------------------------------------------
# P0-S2: get_scoped
# ---------------------------------------------------------------------------
@then('sv get_scoped for "{fid}" should return the fragment')
def step_sv_get_scoped_found(context: Any, fid: str) -> None:
result = context.sv_tier_svc.get_scoped(fid, context.sv_scope)
assert result is not None
assert result.fragment_id == fid
@then('sv get_scoped for "{fid}" should return None')
def step_sv_get_scoped_none(context: Any, fid: str) -> None:
result = context.sv_tier_svc.get_scoped(fid, context.sv_scope)
assert result is None
# ---------------------------------------------------------------------------
# P1-SPEC1: DAG expansion
# ---------------------------------------------------------------------------
@when('sv I resolve scope with registry_lookup expanding "{parent}" to "{child}"')
def step_sv_resolve_with_dag(context: Any, parent: str, child: str) -> None:
def lookup(rid: str) -> frozenset[str]:
if rid == parent:
return frozenset([child])
return frozenset()
context.sv_scope = resolve_resource_scope(
[context.sv_project],
registry_lookup=lookup,
)
@when(
'sv I store fragment "{fid}" with project "{proj}" and empty resource_id in tier "{tier}"'
)
def step_sv_store_empty_rid(context: Any, fid: str, proj: str, tier: str) -> None:
frag = _make_fragment(fid, project=proj, resource="", tier=tier)
context.sv_tier_svc.store(frag)