Skip to content
10 changes: 8 additions & 2 deletions olive-mcp-server/olive_mcp_server/tools/docs_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@

logger = logging.getLogger(__name__)

# Files that are generated by update_kb.py and should not be searched as KB.
_EXCLUDED_KB_FILES = {"update_report.json", "candidate_quirks.json"}
# Files that are generated by update_kb.py / expand_kb.py and must not be
# searched as KB. Including them would drift the content hash from the shipped
# index on every refresh run (refresh_metadata.json carries run timestamps).
_EXCLUDED_KB_FILES = {
"update_report.json",
"candidate_quirks.json",
"refresh_metadata.json",
}

# Cache live docs for one hour so repeated searches do not hammer the network.
_LIVE_CACHE: dict[str, str] = {}
Expand Down
39 changes: 38 additions & 1 deletion olive-mcp-server/tests/test_docs_search_semantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from olive_mcp_server.tools import docs_search
from olive_mcp_server.tools.docs_search import search_olive_documentation
from olive_mcp_server.tools.index_store import content_hash_pairs


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -397,7 +398,43 @@ def test_load_kb_text_skips_invalid_utf8_and_keeps_valid_files(tmp_path, monkeyp
assert "bad_utf8" not in sources


def test_iter_kb_json_files_rejects_symlinks_outside_kb_dir(tmp_path, monkeypatch: pytest.MonkeyPatch):
def test_generator_sidecars_excluded_from_kb_hash(tmp_path, monkeypatch: pytest.MonkeyPatch):
"""Generator-written sidecars must not change the searchable KB content.

update_kb.py / expand_kb.py write ``refresh_metadata.json`` (plus the
already-excluded report files) into the KB directory at refresh time.
If any of them leak into ``_load_kb_text()``, the content hash drifts
from the shipped index and semantic search rebuilds on every run.
"""
(tmp_path / "passes.json").write_text('{"pass": "OnnxQuantization"}', encoding="utf-8")

monkeypatch.setattr(docs_search, "KB_DIR", tmp_path)
baseline_pairs = docs_search._load_kb_text()
baseline_hash = content_hash_pairs(baseline_pairs)

# Simulate a KB refresh run dropping its sidecars next to the KB files.
# String leaves mirror the real refresh_metadata.json (generator, timestamps,
# fingerprints) so the content hash would drift if the file were indexed.
(tmp_path / "refresh_metadata.json").write_text(
'{"schema_version": 1, "generator_version": "0.5.0",'
' "source_timestamp": "2026-08-12T21:00:51Z",'
' "runs": {"update_kb": {"generator": "update_kb", "success": true}}}',
encoding="utf-8",
)
(tmp_path / "update_report.json").write_text('{"generator": "update_kb"}', encoding="utf-8")
(tmp_path / "candidate_quirks.json").write_text('[{"title": "candidate"}]', encoding="utf-8")

after_pairs = docs_search._load_kb_text()
assert content_hash_pairs(after_pairs) == baseline_hash
sources = " ".join(path for path, _ in after_pairs)
assert "refresh_metadata" not in sources
assert "update_report" not in sources
assert "candidate_quirks" not in sources


def test_iter_kb_json_files_rejects_symlinks_outside_kb_dir(
tmp_path, monkeypatch: pytest.MonkeyPatch
):
"""Path traversal guard: symlinks resolving outside KB_DIR are rejected."""
import os

Expand Down
Loading