Files
temp/features/steps/tdd_context_tier_runtime_steps.py
brent.edwards f324575a3b fix(test): remove lingering @tdd_expected_fail tags for closed bugs #821 and #932 (#1229)
The @tdd_expected_fail tag was already absent from the tag lines of both
features/tdd_context_tier_runtime.feature (bug #821) and
features/tdd_plan_apply_yes_flag.feature (bug #932). However, the feature
description and step-definition docstring for bug #821 still referenced the
@tdd_expected_fail tag and described the bug as unfixed.

Updated the feature description in tdd_context_tier_runtime.feature and the
module docstring in tdd_context_tier_runtime_steps.py to reflect that bug #821
has been fixed and these tests now serve as permanent regression guards. No
changes were needed for bug #932 as its files had no stale references.

Permanent tags @tdd_issue / @tdd_issue_821 and @tdd_issue / @tdd_issue_932
remain in place per the CONTRIBUTING.md TDD Issue Test Tags convention.

ISSUES CLOSED: #1206

Co-authored-by: Brent E. Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent E. Edwards <brent.edwards@cleverthis.com>
2026-04-02 16:51:15 +00:00

301 lines
11 KiB
Python

"""Step definitions for TDD Issue #821 — context tier runtime logic.
These steps exercise ``ContextTierService`` and verify that it automatically
promotes fragments on repeated access, demotes stale fragments, and evicts
fragments when the hot tier budget overflows.
Bug #821 identified that ``ContextTierService`` had data models and manual
``promote()``/``demote()``/``evict_lru()`` methods but NO automatic runtime
logic. The bug has been fixed — these tests now serve as a permanent
regression guard confirming the runtime logic works correctly.
"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.context_tiers import ContextTierService
from cleveragents.domain.models.acms.tiers import (
ContextTier,
TierBudget,
TieredFragment,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_FRAGMENT_ID: str = "frag-tier-runtime-001"
_STALE_FRAGMENT_ID: str = "frag-stale-hot-001"
def _make_fragment(
fragment_id: str,
tier: ContextTier,
token_count: int = 50,
content: str = "test content",
last_accessed: datetime | None = None,
) -> TieredFragment:
"""Create a ``TieredFragment`` with the given properties."""
kwargs: dict[str, object] = {
"fragment_id": fragment_id,
"content": content,
"tier": tier,
"token_count": token_count,
"project_name": "test-project",
}
if last_accessed is not None:
kwargs["last_accessed"] = last_accessed
return TieredFragment(**kwargs) # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# Scenario 1: Promotion on repeated access
# ---------------------------------------------------------------------------
@given("a context tier service with default budget")
def step_service_default_budget(context: Context) -> None:
"""Create a ContextTierService with the default TierBudget."""
context.tier_service = ContextTierService(settings=None)
@given("a fragment stored in the cold tier")
def step_fragment_in_cold(context: Context) -> None:
"""Store a fragment in the cold tier."""
frag: TieredFragment = _make_fragment(
fragment_id=_FRAGMENT_ID,
tier=ContextTier.COLD,
token_count=50,
)
service: ContextTierService = context.tier_service
service.store(frag)
# Verify it is actually in cold tier
stored: TieredFragment | None = service.get(_FRAGMENT_ID)
assert stored is not None, "Fragment was not stored"
assert stored.tier == ContextTier.COLD, (
f"Fragment should be in cold tier, got {stored.tier}"
)
context.fragment_id = _FRAGMENT_ID
@when("I access the fragment {n:d} times via get")
def step_access_n_times(context: Context, n: int) -> None:
"""Access the fragment N times through the get() method."""
service: ContextTierService = context.tier_service
frag_id: str = context.fragment_id
for _ in range(n):
result: TieredFragment | None = service.get(frag_id)
assert result is not None, "Fragment disappeared during access"
context.accessed_fragment = service.get(frag_id)
@then("the fragment should have been promoted to warm or hot tier")
def step_fragment_promoted(context: Context) -> None:
"""Assert the fragment was automatically promoted after repeated access.
Bug #821: ``get()`` increments ``access_count`` and updates
``last_accessed`` but never calls ``promote()`` or any equivalent
runtime tier-transition logic. The fragment remains in the cold
tier regardless of how many times it is accessed.
"""
service: ContextTierService = context.tier_service
frag: TieredFragment | None = service.get(context.fragment_id)
assert frag is not None, "Fragment not found"
# The fragment should have been auto-promoted out of cold tier
assert frag.tier != ContextTier.COLD, (
f"Fragment is still in COLD tier after repeated access "
f"(access_count={frag.access_count}). ContextTierService.get() "
f"updates access metadata but does not auto-promote fragments "
f"based on access patterns (bug #821). Expected tier to be "
f"WARM or HOT."
)
# ---------------------------------------------------------------------------
# Scenario 2: Demotion on staleness
# ---------------------------------------------------------------------------
@given("a fragment stored in the hot tier with a stale last_accessed timestamp")
def step_stale_hot_fragment(context: Context) -> None:
"""Store a fragment in the hot tier with a very old last_accessed."""
stale_time: datetime = datetime.now(tz=UTC) - timedelta(hours=24)
frag: TieredFragment = _make_fragment(
fragment_id=_STALE_FRAGMENT_ID,
tier=ContextTier.HOT,
token_count=100,
last_accessed=stale_time,
)
service: ContextTierService = context.tier_service
service.store(frag)
context.stale_fragment_id = _STALE_FRAGMENT_ID
@when("I invoke the staleness enforcement runtime")
def step_invoke_staleness(context: Context) -> None:
"""Attempt to invoke the staleness enforcement method.
Bug #821: There is no method on ContextTierService that enforces
staleness-based demotion. Methods like ``enforce_staleness()``,
``apply_tier_policy()``, or ``tick()`` do not exist. We attempt
to call the most likely candidate names; if none exist, we record
the failure.
"""
service: ContextTierService = context.tier_service
context.staleness_invoked = False
# Try plausible method names for staleness enforcement
for method_name in (
"enforce_staleness",
"apply_tier_policy",
"tick",
"enforce_demotion_policy",
"run_lifecycle",
"apply_lifecycle",
):
method = getattr(service, method_name, None)
if callable(method):
method()
context.staleness_invoked = True
return
# No runtime method found — record this for the assertion step
context.staleness_invoked = False
@then("the fragment should have been demoted to warm or cold tier")
def step_fragment_demoted(context: Context) -> None:
"""Assert the stale fragment was automatically demoted.
Bug #821: ContextTierService has no staleness enforcement runtime.
There is no method that inspects ``last_accessed`` timestamps and
auto-demotes stale hot-tier fragments. The fragment stays in the
hot tier indefinitely.
"""
service: ContextTierService = context.tier_service
# First check: was there even a method to invoke?
assert context.staleness_invoked, (
"No staleness enforcement method found on ContextTierService. "
"Tried: enforce_staleness(), apply_tier_policy(), tick(), "
"enforce_demotion_policy(), run_lifecycle(), apply_lifecycle(). "
"ContextTierService has no runtime logic for automatic demotion "
"of stale fragments (bug #821)."
)
# Second check: did the fragment actually move?
frag: TieredFragment | None = service.get(context.stale_fragment_id)
assert frag is not None, "Stale fragment not found"
assert frag.tier != ContextTier.HOT, (
f"Stale fragment is still in HOT tier "
f"(last_accessed={frag.last_accessed}). "
f"ContextTierService has no runtime staleness enforcement — "
f"stale fragments are never automatically demoted (bug #821). "
f"Expected tier to be WARM or COLD."
)
# ---------------------------------------------------------------------------
# Scenario 3: Eviction on budget overflow
# ---------------------------------------------------------------------------
@given("a context tier service with a small hot tier budget of {n:d} tokens")
def step_service_small_budget(context: Context, n: int) -> None:
"""Create a ContextTierService with a constrained hot tier budget."""
# ContextTierService accepts Settings; we pass None and override budget
service: ContextTierService = ContextTierService(settings=None)
service._budget = TierBudget(
max_tokens_hot=n,
max_decisions_warm=500,
max_decisions_cold=5000,
)
context.tier_service = service
context.hot_budget_tokens = n
@given("the hot tier is filled to its token budget limit")
def step_fill_hot_tier(context: Context) -> None:
"""Fill the hot tier to exactly the token budget limit."""
service: ContextTierService = context.tier_service
budget_tokens: int = context.hot_budget_tokens
# Store fragments totalling exactly the budget limit
# Use 50-token fragments so 100 tokens = 2 fragments
frag_size: int = 50
num_frags: int = budget_tokens // frag_size
context.initial_hot_fragment_ids = []
for i in range(num_frags):
frag_id: str = f"frag-budget-{i:03d}"
frag: TieredFragment = _make_fragment(
fragment_id=frag_id,
tier=ContextTier.HOT,
token_count=frag_size,
content="x" * frag_size,
last_accessed=datetime.now(tz=UTC) - timedelta(minutes=num_frags - i),
)
service.store(frag)
context.initial_hot_fragment_ids.append(frag_id)
# Record the oldest fragment (lowest last_accessed)
context.oldest_fragment_id = context.initial_hot_fragment_ids[0]
metrics = service.get_metrics()
assert metrics.hot_count == num_frags, (
f"Expected {num_frags} fragments in hot tier, got {metrics.hot_count}"
)
@when("I store one more fragment in the hot tier")
def step_store_overflow(context: Context) -> None:
"""Store one more fragment that would exceed the hot tier budget."""
service: ContextTierService = context.tier_service
overflow_frag: TieredFragment = _make_fragment(
fragment_id="frag-overflow-001",
tier=ContextTier.HOT,
token_count=50,
content="overflow content",
)
service.store(overflow_frag)
context.overflow_fragment_id = "frag-overflow-001"
@then("the oldest fragment should have been evicted from the hot tier")
def step_oldest_evicted(context: Context) -> None:
"""Assert the oldest hot-tier fragment was automatically evicted.
Bug #821: ``store()`` does not check ``TierBudget.max_tokens_hot``
and does not trigger ``evict_lru()`` when the budget is exceeded.
The hot tier grows without bound.
"""
service: ContextTierService = context.tier_service
budget_tokens: int = context.hot_budget_tokens
# Calculate total tokens now in hot tier
hot_total_tokens: int = sum(f.token_count for f in service._hot.values())
# The hot tier should not exceed its budget
assert hot_total_tokens <= budget_tokens, (
f"Hot tier has {hot_total_tokens} tokens but budget is "
f"{budget_tokens} tokens. store() does not enforce "
f"TierBudget.max_tokens_hot or trigger automatic LRU "
f"eviction when the budget is exceeded (bug #821)."
)
# The oldest fragment should have been evicted
oldest_id: str = context.oldest_fragment_id
oldest_frag: TieredFragment | None = service.get(oldest_id)
assert oldest_frag is None or oldest_frag.tier != ContextTier.HOT, (
f"Oldest fragment '{oldest_id}' is still in the hot tier after "
f"budget overflow. ContextTierService.store() does not auto-evict "
f"the least-recently-used fragment when hot tier budget is "
f"exceeded (bug #821)."
)