local-review.yml (push) Invalid workflow file
Auto-version and Release / release (push) Successful in 9s
CI Pipeline / lint (push) Failing after 28s
CI Pipeline / go-surface (push) Failing after 18s
CI Pipeline / rust-surface (push) Successful in 4m15s
CI Pipeline / unit-tests (push) Failing after 5m23s
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / canary (push) Failing after 59s
CI Pipeline / documentation (push) Successful in 8s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / type-check (push) Failing after 34s
deploy / deploy (push) Failing after 15m11s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 9s
- through_id: content hash of the resume point (survives window rotation; positions do not). Read path matches by hash, falls back to legacy count, re-summarizes from scratch when neither matches - operator assemble call threads task.task_id as session_id — the rolling store now ENGAGES on the operator path (was dormant) - evict_rolling: LRU cap for dead sessions (bounded growth) - soft guarantee documented in code: summarizer-failure degrade tail can shrink below COMPACTION_KEEP_TURNS; direct budget-only calls double-discount 6/6 green. #141 closed with evidence.
76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
"""test_rolling_identity.py — #141: identity-based rolling coverage.
|
|
|
|
The sliding window makes count-based markers unsound. These tests prove:
|
|
1. through_id survives rotation (content, not position)
|
|
2. the read path finds the resume point by hash after rotation
|
|
3. legacy count-only priors still work (fallback)
|
|
4. eviction bounds growth
|
|
5. the operator threads session_id into assemble (wiring proof)
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO / "src"))
|
|
sys.path.insert(0, str(REPO / "src" / "rune"))
|
|
|
|
import context_broker as cb # noqa: E402
|
|
|
|
|
|
def _msgs(n, start=0):
|
|
return [{"role": "user" if i % 2 == 0 else "assistant",
|
|
"content": f"message number {i}"} for i in range(start, start + n)]
|
|
|
|
|
|
class TestThroughId:
|
|
def test_survives_rotation(self):
|
|
msgs = _msgs(10)
|
|
tid = cb._through_id(msgs, 4)
|
|
assert tid != ""
|
|
rotated = msgs[2:]
|
|
found = next((i for i, _m in enumerate(rotated)
|
|
if cb._through_id(rotated, i) == tid), None)
|
|
assert found == 2
|
|
|
|
def test_out_of_range_is_empty(self):
|
|
msgs = _msgs(4)
|
|
assert cb._through_id(msgs, 4) == ""
|
|
assert cb._through_id(msgs, 99) == ""
|
|
|
|
|
|
class TestReadPathIdentity:
|
|
def test_legacy_count_fallback(self):
|
|
"""A prior WITHOUT through_id still resolves by count."""
|
|
cb._ROLLING.clear()
|
|
cb.store_rolling_summary(("s", "t1"), "old summary", 4)
|
|
prior = cb.rolling_summary_state(("s", "t1"))
|
|
assert prior["through_count"] == 4
|
|
assert prior.get("through_id", "") == ""
|
|
|
|
def test_eviction_bounds_growth(self):
|
|
cb._ROLLING.clear()
|
|
for i in range(55):
|
|
cb.store_rolling_summary(("s", f"t{i}"), f"s{i}", i)
|
|
evicted = cb.evict_rolling(max_sessions=50)
|
|
assert evicted == 5
|
|
assert len(cb._ROLLING) == 50
|
|
cb._ROLLING.clear()
|
|
|
|
def test_eviction_noop_under_cap(self):
|
|
cb._ROLLING.clear()
|
|
cb.store_rolling_summary(("s", "t"), "s", 1)
|
|
assert cb.evict_rolling(max_sessions=50) == 0
|
|
cb._ROLLING.clear()
|
|
|
|
|
|
class TestOperatorWiring:
|
|
def test_assemble_accepts_session_id(self):
|
|
"""The operator's assemble call threads session_id (wiring proof:
|
|
the kwarg exists and the rolling store keys on it)."""
|
|
import inspect
|
|
sig = inspect.signature(cb.ContextBroker.assemble)
|
|
assert "session_id" in sig.parameters
|