test(read-lifecycle): add ContentRouter regression test; pass explicit_hash

Address Copilot review (round 2):

- tests/test_transforms/test_read_lifecycle.py: new integration test that
  drives the fix through headroom.compress() and asserts the STALE Read
  marker's hash resolves in the global CCR store (tool=Read,
  strategy=read_lifecycle:stale). Guards against the original bug
  regressing.
- headroom/transforms/read_lifecycle.py: pass explicit_hash=ccr_hash to
  store.store() so the precomputed fallback hash is reused (no double
  SHA-256) and the marker/store key stays consistent if the store's
  default hash ever changes.

23 tests pass (was 22).
This commit is contained in:
Kiryu Tsukimiya 2026-06-27 15:31:37 +09:00
parent 55a0dfde67
commit 2e8c41a25f
No known key found for this signature in database
2 changed files with 86 additions and 0 deletions

View file

@ -485,6 +485,7 @@ class ReadLifecycleManager:
tool_name="Read",
tool_call_id=classification.tool_call_id,
compression_strategy=f"read_lifecycle:{classification.state.value}",
explicit_hash=ccr_hash,
)
except Exception as e: # noqa: BLE001 - storage failure must not break the request
logger.warning(

View file

@ -610,3 +610,88 @@ class TestNoFilePathHandling:
# Can't match file_path, so Read is not classified at all
assert result.reads_total == 0
assert result.messages[1]["content"] == LARGE_CONTENT
class TestContentRouterIntegration:
"""Regression: ContentRouter.transform must wire a real CCR store into
ReadLifecycleManager so STALE Read markers resolve via headroom_retrieve."""
def test_stale_read_marker_retrievable_via_compress(self, monkeypatch):
import re
# Force an in-memory backend so the test is hermetic.
monkeypatch.setenv("HEADROOM_CCR_BACKEND", "memory")
from headroom import compress
from headroom.cache.compression_store import (
get_compression_store,
reset_compression_store,
)
reset_compression_store()
large_content = "source line\n" * 500 # above read_lifecycle min_size_bytes
messages = [
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "t1",
"name": "Read",
"input": {"file_path": "/tmp/foo.txt"},
}
],
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "t1",
"content": large_content,
}
],
},
# Edit the same file -> the Read above becomes STALE.
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "t2",
"name": "Edit",
"input": {"file_path": "/tmp/foo.txt"},
}
],
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "t2",
"content": "edited",
}
],
},
]
result = compress(messages, model="claude-sonnet-4-5-20250929")
hashes: list[str] = []
for m in result.messages:
content = m.get("content")
if isinstance(content, list):
for b in content:
if isinstance(b, dict) and b.get("type") == "tool_result":
s = b.get("content", "")
if isinstance(s, str):
hashes.extend(re.findall(r"hash=([a-f0-9]+)", s))
assert hashes, "Expected a STALE Read marker with a hash"
store = get_compression_store()
entry = store.retrieve(hashes[0])
assert entry is not None, "STALE Read marker hash not in CCR store"
assert entry.tool_name == "Read"
assert entry.compression_strategy == "read_lifecycle:stale"