"""Step definitions for the ACMS Context Tiers feature.""" from __future__ import annotations from typing import Any from behave import given, then, when from pydantic import ValidationError from cleveragents.application.container import get_container, reset_container from cleveragents.application.services.context_tiers import ContextTierService from cleveragents.config.settings import Settings from cleveragents.domain.models.acms.tiers import ( ActorContextView, ActorRole, ContextTier, ScopedBackendView, TierBudget, TieredFragment, TierMetrics, ) __all__: list[str] = [] # --------------------------------------------------------------------------- # ContextTier enum # --------------------------------------------------------------------------- @then('ContextTier should have values "hot", "warm", "cold"') def step_then_context_tier_values(context: Any) -> None: assert ContextTier.HOT == "hot" assert ContextTier.WARM == "warm" assert ContextTier.COLD == "cold" assert len(ContextTier) == 3 # --------------------------------------------------------------------------- # ActorRole enum # --------------------------------------------------------------------------- @then('ActorRole should have values "strategist", "executor", "reviewer", "estimator"') def step_then_actor_role_values(context: Any) -> None: assert ActorRole.STRATEGIST == "strategist" assert ActorRole.EXECUTOR == "executor" assert ActorRole.REVIEWER == "reviewer" assert ActorRole.ESTIMATOR == "estimator" assert "estimator" in [r.value for r in ActorRole] # --------------------------------------------------------------------------- # TieredFragment # --------------------------------------------------------------------------- @given('a TieredFragment with id "{fid}" and content "{content}"') def step_given_tiered_fragment(context: Any, fid: str, content: str) -> None: context.fragment = TieredFragment(fragment_id=fid, content=content) @then('the fragment tier should be "{tier}"') def step_then_fragment_tier(context: Any, tier: str) -> None: assert context.fragment.tier == tier @then("the fragment token_count should be {count:d}") def step_then_fragment_token_count(context: Any, count: int) -> None: assert context.fragment.token_count == count @then("the fragment access_count should be {count:d}") def step_then_fragment_access_count(context: Any, count: int) -> None: assert context.fragment.access_count == count @then("creating a TieredFragment with empty fragment_id should raise ValidationError") def step_then_empty_fragment_id(context: Any) -> None: try: TieredFragment(fragment_id="", content="x") raise AssertionError("Expected ValidationError") except ValidationError: pass @then( "creating a TieredFragment with negative token_count should raise ValidationError" ) def step_then_negative_token_count(context: Any) -> None: try: TieredFragment(fragment_id="f", content="x", token_count=-1) raise AssertionError("Expected ValidationError") except ValidationError: pass # --------------------------------------------------------------------------- # TierBudget # --------------------------------------------------------------------------- @given("a default TierBudget") def step_given_default_tier_budget(context: Any) -> None: context.budget = TierBudget() @then("max_tokens_hot should be {val:d}") def step_then_max_tokens_hot(context: Any, val: int) -> None: assert context.budget.max_tokens_hot == val @then("max_decisions_warm should be {val:d}") def step_then_max_decisions_warm(context: Any, val: int) -> None: assert context.budget.max_decisions_warm == val @then("max_decisions_cold should be {val:d}") def step_then_max_decisions_cold(context: Any, val: int) -> None: assert context.budget.max_decisions_cold == val # --------------------------------------------------------------------------- # ActorContextView # --------------------------------------------------------------------------- @given('an ActorContextView for "{role}"') def step_given_actor_context_view(context: Any, role: str) -> None: context.actor_view = ActorContextView(actor_role=ActorRole(role)) @then('effective tiers should be "hot", "warm", "cold"') def step_then_all_tiers(context: Any) -> None: tiers = context.actor_view.get_effective_tiers() assert tiers == [ContextTier.HOT, ContextTier.WARM, ContextTier.COLD] @then('effective tiers should be "hot", "warm"') def step_then_hot_warm(context: Any) -> None: tiers = context.actor_view.get_effective_tiers() assert tiers == [ContextTier.HOT, ContextTier.WARM] @then('effective tiers should be "hot"') def step_then_hot_only(context: Any) -> None: tiers = context.actor_view.get_effective_tiers() assert tiers == [ContextTier.HOT] # --------------------------------------------------------------------------- # TierMetrics # --------------------------------------------------------------------------- @given("a default TierMetrics") def step_given_default_metrics(context: Any) -> None: context.metrics = TierMetrics() @then("total_fragments should be {val:d}") def step_then_total_fragments(context: Any, val: int) -> None: assert context.metrics.total_fragments == val @then("hot_hit_rate should be {val:g}") def step_then_hot_hit_rate(context: Any, val: float) -> None: assert context.metrics.hot_hit_rate == val # --------------------------------------------------------------------------- # ScopedBackendView # --------------------------------------------------------------------------- @given('a ScopedBackendView for project "{proj}"') def step_given_scoped_view(context: Any, proj: str) -> None: context.scoped_view = ScopedBackendView(allowed_projects=[proj]) if not hasattr(context, "view_fragments"): context.view_fragments = {} @given('a TieredFragment with id "{fid}" and no project') def step_given_fragment_no_project(context: Any, fid: str) -> None: if not hasattr(context, "view_fragments"): context.view_fragments = {} context.view_fragments[fid] = TieredFragment( fragment_id=fid, content="x", project_name="" ) @given('a TieredFragment with id "{fid}" in project "{proj}"') def step_given_fragment_in_project(context: Any, fid: str, proj: str) -> None: if not hasattr(context, "view_fragments"): context.view_fragments = {} context.view_fragments[fid] = TieredFragment( fragment_id=fid, content="x", project_name=proj ) @then('fragment "{fid}" should be visible') def step_then_fragment_visible(context: Any, fid: str) -> None: frag = context.view_fragments[fid] assert context.scoped_view.is_visible(frag) @then('fragment "{fid}" should not be visible') def step_then_fragment_not_visible(context: Any, fid: str) -> None: frag = context.view_fragments[fid] assert not context.scoped_view.is_visible(frag) # --------------------------------------------------------------------------- # ContextTierService: store/get # --------------------------------------------------------------------------- @given("a ContextTierService") def step_given_tier_service(context: Any) -> None: context.tier_service = ContextTierService() @when('I store a fragment with id "{fid}" content "{content}" in tier "{tier}"') def step_when_store_fragment(context: Any, fid: str, content: str, tier: str) -> None: frag = TieredFragment( fragment_id=fid, content=content, tier=ContextTier(tier), ) context.tier_service.store(frag) @when( 'I store fragment "{fid}" with content "{content}" tier "{tier}" project "{proj}"' ) def step_when_store_fragment_project( context: Any, fid: str, content: str, tier: str, proj: str ) -> None: frag = TieredFragment( fragment_id=fid, content=content, tier=ContextTier(tier), project_name=proj, ) context.tier_service.store(frag) @when('I store a long fragment "{fid}" in tier "{tier}"') def step_when_store_long_fragment(context: Any, fid: str, tier: str) -> None: long_content = "x" * 300 # Longer than _COLD_SUMMARY_MAX_CHARS (200) frag = TieredFragment( fragment_id=fid, content=long_content, tier=ContextTier(tier), ) context.tier_service.store(frag) @then('tier getting "{fid}" should return content "{content}"') def step_then_get_content(context: Any, fid: str, content: str) -> None: result = context.tier_service.get(fid) assert result is not None assert result.content == content @then('tier getting "{fid}" should return None') def step_then_get_none(context: Any, fid: str) -> None: assert context.tier_service.get(fid) is None # --------------------------------------------------------------------------- # ContextTierService: promote/demote # --------------------------------------------------------------------------- @when('I promote "{fid}"') def step_when_promote(context: Any, fid: str) -> None: context.last_promoted = context.tier_service.promote(fid) @when('I demote "{fid}"') def step_when_demote(context: Any, fid: str) -> None: context.last_demoted = context.tier_service.demote(fid) @then('"{fid}" should be in tier "{tier}"') def step_then_in_tier(context: Any, fid: str, tier: str) -> None: result = context.tier_service.get(fid) assert result is not None assert result.tier == tier @then('promoting "{fid}" should return None') def step_then_promote_none(context: Any, fid: str) -> None: assert context.tier_service.promote(fid) is None @then('demoting "{fid}" should return None') def step_then_demote_none(context: Any, fid: str) -> None: assert context.tier_service.demote(fid) is None @then('"{fid}" content should be summarized') def step_then_content_summarized(context: Any, fid: str) -> None: result = context.tier_service.get(fid) assert result is not None # Summarised content ends with "..." if original was long assert result.content.endswith("...") # --------------------------------------------------------------------------- # ContextTierService: LRU eviction # --------------------------------------------------------------------------- @when('I evict {count:d} LRU fragments from "{tier}"') def step_when_evict(context: Any, count: int, tier: str) -> None: context.evicted = context.tier_service.evict_lru(ContextTier(tier), count) @then('"{tier}" tier should have {count:d} fragment') def step_then_tier_count_singular(context: Any, tier: str, count: int) -> None: metrics = context.tier_service.get_metrics() if tier == "hot": assert metrics.hot_count == count elif tier == "warm": assert metrics.warm_count == count else: assert metrics.cold_count == count @then('evicting {count:d} from "{tier}" should raise ValueError') def step_then_evict_raises(context: Any, count: int, tier: str) -> None: try: context.tier_service.evict_lru(ContextTier(tier), count) raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # ContextTierService: actor views # --------------------------------------------------------------------------- @then('strategist for project "{proj}" should see {count:d} fragments') def step_then_strategist_sees(context: Any, proj: str, count: int) -> None: frags = context.tier_service.get_for_actor( ActorRole.STRATEGIST, project_names=[proj] ) assert len(frags) == count @then('executor for project "{proj}" should see {count:d} fragments') def step_then_executor_sees(context: Any, proj: str, count: int) -> None: frags = context.tier_service.get_for_actor(ActorRole.EXECUTOR, project_names=[proj]) assert len(frags) == count @then('reviewer for project "{proj}" should see {count:d} fragment') def step_then_reviewer_sees(context: Any, proj: str, count: int) -> None: frags = context.tier_service.get_for_actor(ActorRole.REVIEWER, project_names=[proj]) assert len(frags) == count # --------------------------------------------------------------------------- # ContextTierService: scoped view # --------------------------------------------------------------------------- @then('scoped view for "{proj}" should have {count:d} fragment') def step_then_scoped_count(context: Any, proj: str, count: int) -> None: frags = context.tier_service.get_scoped_view([proj]) assert len(frags) == count # --------------------------------------------------------------------------- # ContextTierService: metrics # --------------------------------------------------------------------------- @when('I get fragment "{fid}"') def step_when_get_fragment(context: Any, fid: str) -> None: context.tier_service.get(fid) @then("hot_hit_count should be {val:d}") def step_then_hot_hit_count(context: Any, val: int) -> None: metrics = context.tier_service.get_metrics() assert metrics.hot_hit_count == val @then("hot_miss_count should be {val:d}") def step_then_hot_miss_count(context: Any, val: int) -> None: metrics = context.tier_service.get_metrics() assert metrics.hot_miss_count == val # --------------------------------------------------------------------------- # DI Container # --------------------------------------------------------------------------- @given("the DI container is reset") def step_given_di_reset(context: Any) -> None: reset_container() context.container = get_container() @then("the container should provide a context_tier_service") def step_then_container_has_tier_service(context: Any) -> None: context.resolved_tier_svc = context.container.context_tier_service() assert context.resolved_tier_svc is not None @then("the context_tier_service should be a ContextTierService") def step_then_tier_svc_type(context: Any) -> None: assert isinstance(context.resolved_tier_svc, ContextTierService) @then("resolving context_tier_service twice should return the same instance") def step_then_tier_svc_singleton(context: Any) -> None: a = context.container.context_tier_service() b = context.container.context_tier_service() assert a is b # --------------------------------------------------------------------------- # Settings # --------------------------------------------------------------------------- @then("settings should have context_max_tokens_hot") def step_then_settings_hot(context: Any) -> None: s = Settings() assert hasattr(s, "context_max_tokens_hot") assert s.context_max_tokens_hot >= 0 @then("settings should have context_max_decisions_warm") def step_then_settings_warm(context: Any) -> None: s = Settings() assert hasattr(s, "context_max_decisions_warm") assert s.context_max_decisions_warm >= 0 @then("settings should have context_max_decisions_cold") def step_then_settings_cold(context: Any) -> None: s = Settings() assert hasattr(s, "context_max_decisions_cold") assert s.context_max_decisions_cold >= 0