"""Compression breadcrumbs: the KEYS line and the raw-chunk index it feeds."""
from types import SimpleNamespace
import navi.config as _config
import navi.core.compressor as _cm
from navi.config import Settings
from navi.core.compressor import ContextCompressor, compress_context
from navi.llm.base import Message
from tests.conftest_factory import FakeLLMBackend
def _conversation() -> list[Message]:
return [
Message(role="system", content="sys"),
Message(role="user", content="use `uv run pytest` in navi/core/compressor.py", sequence_number=1),
Message(role="assistant", content="ok, port 8099", sequence_number=2),
Message(role="user", content="next", sequence_number=3),
Message(role="assistant", content="next answer", sequence_number=4),
Message(role="user", content="latest", sequence_number=5),
Message(role="assistant", content="latest answer", sequence_number=6),
]
class TestKeysLine:
async def test_the_keys_line_is_stripped_and_stored_on_the_summary(self):
backend = FakeLLMBackend(responses=[
"## Goal\ndo it\n\nKEYS: `uv run pytest`, navi/core/compressor.py, port 8099"
])
new_context, summary = await compress_context(
context=_conversation(), llm=backend, model="test", temperature=0.3, keep_recent=2,
)
assert "KEYS" not in summary
assert summary.startswith("## Goal")
summary_msg = next(m for m in new_context if m.is_summary)
assert summary_msg.metadata["breadcrumbs"] == [
"uv run pytest", "navi/core/compressor.py", "port 8099",
]
async def test_a_summary_without_keys_keeps_empty_metadata(self):
backend = FakeLLMBackend(responses=["plain summary"])
new_context, summary = await compress_context(
context=_conversation(), llm=backend, model="test", temperature=0.3, keep_recent=2,
)
assert summary == "plain summary"
assert next(m for m in new_context if m.is_summary).metadata == {}
async def test_a_keys_only_response_does_not_become_an_empty_summary(self):
backend = FakeLLMBackend(responses=["KEYS: alpha, beta"])
_, summary = await compress_context(
context=_conversation(), llm=backend, model="test", temperature=0.3, keep_recent=2,
)
assert summary == "(summary unavailable)"
class FakeChunkStore:
def __init__(self):
self.calls = []
async def add_chunks(self, chunks, user_id=None, session_id=None):
self.calls.append((chunks, user_id, session_id))
return len(chunks)
class TestIndexDemoted:
def _demoted(self) -> list[Message]:
return [
Message(role="user", content="run `navi_search --port 8099`", sequence_number=1),
Message(role="assistant", content="done, see navi/memory/_chunks.py", sequence_number=2),
Message(role="user", content="old summary text", sequence_number=3, is_summary=True),
]
async def test_chunks_are_written_with_the_summary_keys_and_extracted_names(self):
store = FakeChunkStore()
summary_msg = Message(
role="user", content="summary", is_summary=True,
metadata={"breadcrumbs": ["from_summary_key"]},
)
session = SimpleNamespace(user_id="u1")
compressor = ContextCompressor()
await compressor._index_demoted(store, session, "s1", self._demoted(), summary_msg)
chunks, user_id, session_id = store.calls[0]
assert (user_id, session_id) == ("u1", "s1")
assert len(chunks) == 1
keys = chunks[0]["breadcrumbs"]
assert "from_summary_key" in keys
assert "navi/memory/_chunks.py" in keys
assert "navi_search --port 8099" in keys
assert "old summary text" not in chunks[0]["content"]
async def test_the_hard_truncate_path_falls_back_to_the_extractor(self):
store = FakeChunkStore()
await ContextCompressor()._index_demoted(
store, SimpleNamespace(user_id=None), "s1", self._demoted(), None,
)
keys = store.calls[0][0][0]["breadcrumbs"]
assert "navi/memory/_chunks.py" in keys
async def test_indexing_can_be_switched_off(self, monkeypatch):
new = Settings(_env_file=None, memory_search_enabled=False)
monkeypatch.setattr(_config, "settings", new)
monkeypatch.setattr(_cm, "settings", new)
store = FakeChunkStore()
await ContextCompressor()._index_demoted(
store, SimpleNamespace(user_id="u1"), "s1", self._demoted(), None,
)
assert store.calls == []
async def test_a_broken_store_never_breaks_compression(self):
class Broken(FakeChunkStore):
async def add_chunks(self, chunks, user_id=None, session_id=None):
raise RuntimeError("db down")
# No exception escapes: losing an index entry is cheaper than losing a turn.
await ContextCompressor()._index_demoted(
Broken(), SimpleNamespace(user_id="u1"), "s1", self._demoted(), None,
)
async def test_no_messages_means_no_call(self):
store = FakeChunkStore()
await ContextCompressor()._index_demoted(store, SimpleNamespace(user_id="u1"), "s1", [], None)
assert store.calls == []