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
283 lines
9.6 KiB
Python
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)
|