Newer
Older
navi-1 / tests / unit / memory / test_breadcrumbs.py
"""Breadcrumb keys — the index a later search asks by."""

from navi.memory.breadcrumbs import (
    MAX_BREADCRUMBS,
    extract_breadcrumbs,
    split_keys_line,
)


class TestSplitKeysLine:
    def test_the_trailing_keys_line_is_parsed_and_removed(self):
        text = "## Goal\nDo the thing.\n\nKEYS: navi/server.py, pytest, port 8099"
        clean, keys = split_keys_line(text)
        assert clean == "## Goal\nDo the thing."
        assert keys == ["navi/server.py", "pytest", "port 8099"]

    def test_bold_and_lowercase_spellings_are_recognised(self):
        for line in ("**KEYS**: a, b", "keys: a, b", "KEYS: a; b"):
            _, keys = split_keys_line(f"summary\n{line}")
            assert keys == ["a", "b"], line

    def test_none_yields_no_keys(self):
        clean, keys = split_keys_line("summary\nKEYS: NONE")
        assert keys == []
        assert clean == "summary"

    def test_backticks_and_bullets_are_stripped(self):
        _, keys = split_keys_line("s\nKEYS: `memory_search`, - `navi/memory/_chunks.py`, " + "x" * 200)
        assert keys == ["memory_search", "navi/memory/_chunks.py"]

    def test_a_summary_without_the_line_is_untouched(self):
        text = "## Goal\nNothing to index here."
        assert split_keys_line(text) == (text, [])

    def test_only_the_last_line_counts(self):
        # The model may mention KEYS in prose; the index is the line at the end.
        text = "KEYS: not-this-one\n## Goal\nbody\nKEYS: real_key"
        clean, keys = split_keys_line(text)
        assert keys == ["real_key"]
        assert "not-this-one" in clean


class TestExtractBreadcrumbs:
    def test_names_commands_paths_and_amounts_are_found(self):
        text = (
            "We ran `uv run pytest -q` in navi/core/compressor.py and it failed:\n"
            "AssertionError: memory_search returned 0 hits\n"
            "The budget is 4,5 GB and ticket #522 is closed."
        )
        found = extract_breadcrumbs([text])
        joined = " | ".join(found)
        assert "uv run pytest -q" in joined
        assert "navi/core/compressor.py" in joined
        assert "memory_search" in joined
        assert "#522" in joined
        assert any("4,5 GB" in item for item in found)

    def test_identifiers_and_urls(self):
        found = extract_breadcrumbs(
            ["See https://tasks.gnexus.space/tasks/522 and call MemorySearchTool.execute."]
        )
        assert "https://tasks.gnexus.space/tasks/522" in found
        assert "MemorySearchTool" in found

    def test_duplicates_collapse_case_insensitively(self):
        found = extract_breadcrumbs(["`navi/config.py` and NAVI/CONFIG.PY"])
        assert len([f for f in found if f.lower() == "navi/config.py"]) == 1

    def test_the_limit_is_respected(self):
        text = " ".join(f"`thing_{index}`" for index in range(60))
        assert len(extract_breadcrumbs([text], limit=5)) == 5
        assert extract_breadcrumbs([text], limit=0) == []

    def test_the_cap_is_not_exceeded_by_default(self):
        text = " ".join(f"`name_{index}`" for index in range(MAX_BREADCRUMBS + 20))
        assert len(extract_breadcrumbs([text])) == MAX_BREADCRUMBS

    def test_empty_text_produces_nothing(self):
        assert extract_breadcrumbs([]) == []
        assert extract_breadcrumbs(["", None]) == []  # type: ignore[list-item]

    def test_extraction_is_deterministic(self):
        text = "`alpha`, `/tmp/x.py`, snake_case_name, 12 ms"
        assert extract_breadcrumbs([text]) == extract_breadcrumbs([text])