202c9bfe75
CI / lint (push) Successful in 13s
CI / e2e_tests (push) Has started running
CI / quality (push) Successful in 23s
CI / benchmark-publish (push) Has started running
CI / typecheck (push) Successful in 43s
CI / benchmark-regression (push) Has been skipped
CI / build (push) Successful in 22s
CI / security (push) Successful in 59s
CI / integration_tests (push) Successful in 2m49s
CI / unit_tests (push) Successful in 4m1s
CI / docker (push) Successful in 1m26s
CI / coverage (push) Successful in 9m14s
## Summary TDD expected-fail tests proving bug #821 exists: `ContextTierService` has data models for hot/warm/cold tiers but **no runtime logic** for automatic promotion, demotion, or eviction. - **Promotion on access**: Accessing a cold-tier fragment repeatedly via `get()` does NOT auto-promote it — `get()` updates `access_count`/`last_accessed` but never calls `promote()` - **Demotion on staleness**: No staleness enforcement method exists — tried `enforce_staleness()`, `apply_tier_policy()`, `tick()`, etc. — none are implemented - **Eviction on budget overflow**: `store()` does NOT enforce `TierBudget.max_tokens_hot` — the hot tier grows without bound ### Files Added | File | Purpose | |------|---------| | `features/tdd_context_tier_runtime.feature` | 3 Behave scenarios tagged `@tdd_expected_fail @tdd_bug @tdd_bug_821 @mock_only` | | `features/steps/tdd_context_tier_runtime_steps.py` | Type-annotated step definitions exercising real `ContextTierService` | | `robot/tdd_context_tier_runtime.robot` | 3 Robot Framework integration tests tagged `tdd_expected_fail` | | `robot/helper_tdd_context_tier_runtime.py` | Helper script for Robot tests with 3 subcommands | ### Verification - `nox -s lint` — passed - `nox -s typecheck` — passed (0 errors) - `nox -s unit_tests -- features/tdd_context_tier_runtime.feature` — **3 scenarios passed** (all assertions fail as expected, `@tdd_expected_fail` inverts to CI pass) ISSUES CLOSED: #840 Reviewed-on: #1058 Co-authored-by: Brent Edwards <brent.edwards@cleverthis.com> Co-committed-by: Brent Edwards <brent.edwards@cleverthis.com>
233 lines
7.6 KiB
Python
233 lines
7.6 KiB
Python
"""Helper script for tdd_context_tier_runtime.robot smoke tests.
|
|
|
|
Each subcommand exercises ``ContextTierService`` to reproduce bug #821.
|
|
The helper reports the **real** outcome: it exits 0 and prints the sentinel
|
|
when the expected behaviour is observed (bug fixed), and exits 1 when the
|
|
bug is still present. The ``tdd_expected_fail_listener`` on the Robot side
|
|
handles pass/fail inversion while the bug remains open.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from collections.abc import Callable
|
|
from datetime import UTC, datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import NoReturn
|
|
|
|
# Ensure local source tree is importable.
|
|
_ROOT: Path = Path(__file__).resolve().parents[1]
|
|
_SRC: str = str(_ROOT / "src")
|
|
_ROBOT: str = str(_ROOT / "robot")
|
|
for _p in (_SRC, _ROBOT):
|
|
if _p not in sys.path:
|
|
sys.path.insert(0, _p)
|
|
|
|
from cleveragents.application.services.context_tiers import ( # noqa: E402
|
|
ContextTierService,
|
|
)
|
|
from cleveragents.domain.models.acms.tiers import ( # noqa: E402
|
|
ContextTier,
|
|
TierBudget,
|
|
TieredFragment,
|
|
)
|
|
|
|
|
|
def _fail(msg: str) -> NoReturn:
|
|
"""Print error message to stderr and exit with code 1."""
|
|
print(msg, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
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]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Subcommands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _promote_on_access() -> None:
|
|
"""Verify that accessing a cold-tier fragment repeatedly auto-promotes it.
|
|
|
|
Bug #821: ``get()`` increments ``access_count`` and updates
|
|
``last_accessed`` but never calls ``promote()`` or any equivalent
|
|
runtime logic. The fragment stays in the cold tier forever.
|
|
"""
|
|
service: ContextTierService = ContextTierService(settings=None)
|
|
frag: TieredFragment = _make_fragment(
|
|
fragment_id="frag-promote-001",
|
|
tier=ContextTier.COLD,
|
|
token_count=50,
|
|
)
|
|
service.store(frag)
|
|
|
|
# Access the fragment 5 times
|
|
for _ in range(5):
|
|
result: TieredFragment | None = service.get("frag-promote-001")
|
|
if result is None:
|
|
_fail("Fragment disappeared during repeated access")
|
|
|
|
# Check if the fragment was promoted
|
|
final: TieredFragment | None = service.get("frag-promote-001")
|
|
if final is None:
|
|
_fail("Fragment not found after repeated access")
|
|
|
|
if final.tier == ContextTier.COLD:
|
|
_fail(
|
|
f"Fragment is still in COLD tier after 5 accesses "
|
|
f"(access_count={final.access_count}). "
|
|
f"ContextTierService.get() does not auto-promote fragments "
|
|
f"based on access patterns (bug #821)."
|
|
)
|
|
print("tdd-promote-on-access-ok")
|
|
|
|
|
|
def _demote_on_staleness() -> None:
|
|
"""Verify that a stale hot-tier fragment is auto-demoted.
|
|
|
|
Bug #821: ContextTierService has no staleness enforcement method.
|
|
No ``enforce_staleness()``, ``apply_tier_policy()``, ``tick()``,
|
|
or similar runtime method exists.
|
|
"""
|
|
service: ContextTierService = ContextTierService(settings=None)
|
|
stale_time: datetime = datetime.now(tz=UTC) - timedelta(hours=24)
|
|
frag: TieredFragment = _make_fragment(
|
|
fragment_id="frag-stale-001",
|
|
tier=ContextTier.HOT,
|
|
token_count=100,
|
|
last_accessed=stale_time,
|
|
)
|
|
service.store(frag)
|
|
|
|
# Attempt to invoke staleness enforcement
|
|
invoked: bool = False
|
|
for method_name in (
|
|
"enforce_staleness",
|
|
"apply_tier_policy",
|
|
"tick",
|
|
"enforce_demotion_policy",
|
|
"run_lifecycle",
|
|
"apply_lifecycle",
|
|
):
|
|
method: object = getattr(service, method_name, None)
|
|
if callable(method):
|
|
method()
|
|
invoked = True
|
|
break
|
|
|
|
if not invoked:
|
|
_fail(
|
|
"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)."
|
|
)
|
|
|
|
# Check if the fragment was demoted
|
|
result: TieredFragment | None = service.get("frag-stale-001")
|
|
if result is None:
|
|
_fail("Stale fragment not found after staleness enforcement")
|
|
|
|
if result.tier == ContextTier.HOT:
|
|
_fail(
|
|
f"Stale fragment is still in HOT tier "
|
|
f"(last_accessed={result.last_accessed}). "
|
|
f"Staleness enforcement did not demote it (bug #821)."
|
|
)
|
|
print("tdd-demote-on-staleness-ok")
|
|
|
|
|
|
def _evict_on_overflow() -> None:
|
|
"""Verify that hot tier budget overflow triggers auto-eviction.
|
|
|
|
Bug #821: ``store()`` does not check ``TierBudget.max_tokens_hot``
|
|
and does not call ``evict_lru()`` when the budget is exceeded.
|
|
"""
|
|
service: ContextTierService = ContextTierService(settings=None)
|
|
service._budget = TierBudget(
|
|
max_tokens_hot=100,
|
|
max_decisions_warm=500,
|
|
max_decisions_cold=5000,
|
|
)
|
|
|
|
# Fill hot tier to exactly 100 tokens (2 x 50-token fragments)
|
|
for i in range(2):
|
|
frag: TieredFragment = _make_fragment(
|
|
fragment_id=f"frag-budget-{i:03d}",
|
|
tier=ContextTier.HOT,
|
|
token_count=50,
|
|
content="x" * 50,
|
|
last_accessed=datetime.now(tz=UTC) - timedelta(minutes=2 - i),
|
|
)
|
|
service.store(frag)
|
|
|
|
oldest_id: str = "frag-budget-000"
|
|
|
|
# Store one more fragment that exceeds the budget
|
|
overflow: TieredFragment = _make_fragment(
|
|
fragment_id="frag-overflow-001",
|
|
tier=ContextTier.HOT,
|
|
token_count=50,
|
|
content="overflow",
|
|
)
|
|
service.store(overflow)
|
|
|
|
# Check hot tier total tokens
|
|
hot_total: int = sum(f.token_count for f in service._hot.values())
|
|
if hot_total > 100:
|
|
_fail(
|
|
f"Hot tier has {hot_total} tokens but budget is 100 tokens. "
|
|
f"store() does not enforce TierBudget.max_tokens_hot or "
|
|
f"trigger automatic LRU eviction (bug #821)."
|
|
)
|
|
|
|
# Check that the oldest fragment was evicted
|
|
oldest: TieredFragment | None = service.get(oldest_id)
|
|
if oldest is not None and oldest.tier == ContextTier.HOT:
|
|
_fail(
|
|
f"Oldest fragment '{oldest_id}' is still in hot tier after "
|
|
f"budget overflow. store() does not auto-evict (bug #821)."
|
|
)
|
|
|
|
print("tdd-evict-on-overflow-ok")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatcher
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_COMMANDS: dict[str, Callable[[], None]] = {
|
|
"promote-on-access": _promote_on_access,
|
|
"demote-on-staleness": _demote_on_staleness,
|
|
"evict-on-overflow": _evict_on_overflow,
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
|
|
print(
|
|
f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
cmd: Callable[[], None] = _COMMANDS[sys.argv[1]]
|
|
cmd()
|