"""Automatic recall of raw excerpts — the read side of the compression index."""
from datetime import datetime, timezone
from types import SimpleNamespace
import navi.config as _config
import navi.core.context_builder.builder as _cb
from navi.config import Settings
from navi.core.context_builder import ContextBuilder, _recall_query
from navi.llm.base import Message
from navi.memory.search import MemoryHit
from tests.conftest_factory import make_profile_registry
NOW = datetime(2026, 11, 9, 12, 0, tzinfo=timezone.utc)
class FakeSearch:
def __init__(self, hits=None):
self.hits = hits or []
self.calls = []
async def search(self, query, **kwargs):
self.calls.append((query, kwargs))
return self.hits
async def latest_summary_at(self, session_id):
return NOW
def _session(*, with_summary: bool, context=None):
messages = context or [
Message(role="user", content="we discussed the deploy port"),
Message(role="assistant", content="the deploy port is 8099"),
]
if with_summary:
messages = [Message(role="user", content="[Context Summary]", is_summary=True), *messages]
return SimpleNamespace(id="s1", context=messages, session_metadata={})
def _hit(text="the deploy port is 8099", **kwargs):
defaults = dict(
source_type="raw", text=text, session_id="s1",
start_seq=3, end_seq=4, timestamp=NOW, chunk_id="c1", score=0.9,
)
defaults.update(kwargs)
return MemoryHit(**defaults)
class TestQueryWindow:
def test_the_query_uses_the_window_not_only_the_last_message(self):
context = [
Message(role="user", content="we discussed the deploy port"),
Message(role="assistant", content="the deploy port is 8099"),
]
query = _recall_query(context, "and what about it?")
assert "deploy port" in query
assert "8099" in query
assert "and what about it?" in query
def test_summaries_are_kept_out_of_the_query(self):
context = [
Message(role="user", content="a giant old summary", is_summary=True),
Message(role="user", content="fresh question"),
]
assert "giant old summary" not in _recall_query(context, "next")
def test_the_query_is_bounded(self):
context = [Message(role="user", content="x" * 10_000)]
assert len(_recall_query(context, "y" * 10_000)) <= 1200
class TestRecallsOnlyAfterACompression:
async def test_no_summary_means_no_search(self):
search = FakeSearch([_hit()])
builder = ContextBuilder(make_profile_registry(), memory_search=search)
msg = await builder._memory_recall_msg(_session(with_summary=False), "the port")
assert msg is None
assert search.calls == []
async def test_with_a_summary_the_session_is_searched(self):
search = FakeSearch([_hit()])
builder = ContextBuilder(make_profile_registry(), memory_search=search)
msg = await builder._memory_recall_msg(_session(with_summary=True), "the port", user_id="u1")
assert msg is not None
_, kwargs = search.calls[0]
assert kwargs["scope"] == "raw"
assert kwargs["session_id"] == "s1"
assert kwargs["user_id"] == "u1"
async def test_recall_can_be_switched_off(self, monkeypatch):
new = Settings(_env_file=None, memory_recall_enabled=False)
monkeypatch.setattr(_config, "settings", new)
monkeypatch.setattr(_cb, "_config", SimpleNamespace(settings=new))
search = FakeSearch([_hit()])
builder = ContextBuilder(make_profile_registry(), memory_search=search)
assert await builder._memory_recall_msg(_session(with_summary=True), "port") is None
assert search.calls == []
class TestSelection:
async def test_an_excerpt_is_rendered_with_its_source(self):
builder = ContextBuilder(make_profile_registry(), memory_search=FakeSearch([_hit()]))
msg = await builder._memory_recall_msg(_session(with_summary=True), "port")
assert msg.role == "system"
assert "session s1, turns 3–4" in msg.content
assert "the deploy port is 8099" in msg.content
assert "verbatim" in msg.content
async def test_the_same_chunk_is_not_injected_twice(self):
injected: list[str] = []
builder = ContextBuilder(make_profile_registry(), memory_search=FakeSearch([_hit()]))
first = await builder._memory_recall_msg(_session(with_summary=True), "port", injected_ids=injected)
second = await builder._memory_recall_msg(_session(with_summary=True), "port", injected_ids=injected)
assert first is not None
assert second is None
assert injected == ["c1"]
async def test_a_weak_vector_match_is_dropped(self, monkeypatch):
new = Settings(_env_file=None, memory_recall_min_similarity=0.6)
monkeypatch.setattr(_cb, "_config", SimpleNamespace(settings=new))
builder = ContextBuilder(
make_profile_registry(), memory_search=FakeSearch([_hit(score=0.4)])
)
assert await builder._memory_recall_msg(_session(with_summary=True), "port") is None
async def test_the_lexical_fallback_needs_two_terms(self):
weak = _hit(score=None, terms_hit=1, chunk_id="c1")
strong = _hit(score=None, terms_hit=2, chunk_id="c2", text="the port is in the deploy notes")
builder = ContextBuilder(make_profile_registry(), memory_search=FakeSearch([weak, strong]))
msg = await builder._memory_recall_msg(_session(with_summary=True), "port")
assert "deploy notes" in msg.content
assert msg.content.count("•") == 1
async def test_an_outdated_excerpt_is_not_injected(self):
builder = ContextBuilder(make_profile_registry(), memory_search=FakeSearch(
[_hit(superseded_by="server_port (updated later)")]
))
assert await builder._memory_recall_msg(_session(with_summary=True), "port") is None
async def test_at_most_k_excerpts(self, monkeypatch):
new = Settings(_env_file=None, memory_recall_k=1)
monkeypatch.setattr(_cb, "_config", SimpleNamespace(settings=new))
hits = [_hit(chunk_id="c1"), _hit("second", chunk_id="c2")]
builder = ContextBuilder(make_profile_registry(), memory_search=FakeSearch(hits))
msg = await builder._memory_recall_msg(_session(with_summary=True), "port")
assert "second" not in msg.content
async def test_a_search_failure_is_swallowed(self):
class Broken(FakeSearch):
async def search(self, query, **kwargs):
raise RuntimeError("db down")
builder = ContextBuilder(make_profile_registry(), memory_search=Broken())
assert await builder._memory_recall_msg(_session(with_summary=True), "port") is None