Files
temp/features/steps/tdd_context_tier_runtime_steps.py
hurui200320 1878998b7a refactor(testing): rename tdd_bug/tdd_bug_N tags to tdd_issue/tdd_issue_N
Rename the TDD tag system from tdd_bug/tdd_bug_<N> to tdd_issue/tdd_issue_<N>
across the entire codebase. The tdd_expected_fail tag is unchanged.

The TDD expected-failure workflow is not limited to bug fixes — it applies
equally to any issue type (features, tasks, refactors). The _bug suffix was
misleading and narrowed the perceived scope. The new _issue suffix accurately
reflects that the TDD tagging system applies to any Forgejo issue.

Changes span 92 files:
- features/environment.py: validate_tdd_tags(), should_invert_result(), and
  apply_tdd_inversion() updated — regex, variables, error messages
- robot/tdd_expected_fail_listener.py: _validate_tdd_tags(), _should_invert_result(),
  start_test(), end_test() updated consistently
- 33 Behave .feature files: all @tdd_bug/@tdd_bug_<N> tags renamed
- 29 Robot .robot files: all tdd_bug/tdd_bug_<N> tags renamed
- 3 Robot fixture files renamed (tdd_bug_alone, tdd_missing_tdd_bug,
  tdd_expected_fail_missing_bug_n) with content and references updated
- Tag validation tests and helpers updated (function names, command dispatch
  keys, output strings, fixture references)
- CONTRIBUTING.md: section renamed from 'TDD Bug Test Tags' to
  'TDD Issue Test Tags', all tag references and examples updated
- noxfile.py: comment references updated
- Step definition files, mock helpers, and benchmark files: docstring
  references updated

ISSUES CLOSED: #965
2026-03-27 05:58:35 +00:00

311 lines
12 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.
On ``master`` (before the fix), ``ContextTierService`` has data models and
manual ``promote()``/``demote()``/``evict_lru()`` methods but NO automatic
runtime logic:
- ``get()`` updates ``access_count`` and ``last_accessed`` but never
auto-promotes a heavily accessed cold/warm fragment.
- There is no staleness enforcement: no method inspects ``last_accessed``
timestamps and auto-demotes stale hot fragments.
- ``store()`` does not enforce ``TierBudget.max_tokens_hot``: storing
beyond the budget does not trigger automatic LRU eviction.
The assertions in these steps will **fail** until the bug is fixed,
proving the bug exists. The ``@tdd_expected_fail`` tag inverts the
result so CI passes.
"""
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)."
)