testsAndMisc/python_pkg/archwiki_rag/tests/test_reindex.py
Krzysztof kuhy Rudnicki 86565bdef6 archwiki_rag: offline semantic search over the Arch Wiki
Indexes the arch-wiki-docs dump that is already on disk and exposes it
to the knowledge-rag MCP server, so Arch questions can be asked by
symptom ("why doesn't my systemd user unit see DISPLAY under i3")
rather than by page title, with no network round-trip at query time.

The one Arch-specific MCP server in the wild fetches pages over the
network and bundles pacman operations that duplicate the local yay-mcp.
This does neither: it only searches, against a local corpus.

install.sh installs its own dependencies, wires the MCP config and
registers a pacman hook so the index follows package updates. The
generated index is machine-specific and regenerable, so it is
gitignored rather than committed.

_reindex_entry runs under the knowledge-rag interpreter, where
mcp_server is importable and this repo's packages are not. Resolving
that import with importlib.import_module states the deferral outright
instead of a function-level import statement that reads as an oversight,
and the backend failure path now re-raises as SystemExit, so the
exit-code translation is explicit rather than a swallowed exception.
Both replace lint suppressions; the package carries none.

pylint runs in its own venv, so beautifulsoup4 and markdownify are
declared there too — being in meta/requirements.txt does not make them
resolvable to the hook.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
2026-08-10 15:33:18 +02:00

158 lines
5.7 KiB
Python

"""Tests for archwiki_rag.reindex."""
from __future__ import annotations
from pathlib import Path
import subprocess
from unittest.mock import MagicMock, patch
from python_pkg.archwiki_rag import reindex
from python_pkg.archwiki_rag.constants import DATA_SUBDIR, LOCK_FILENAME
MOD = "python_pkg.archwiki_rag.reindex"
def _write_lock(store: Path, contents: str) -> None:
"""Create a knowledge-rag instance lock file.
Parameters:
store (Path): Store root.
contents (str): Raw lock file body.
"""
data = store / DATA_SUBDIR
data.mkdir(parents=True, exist_ok=True)
(data / LOCK_FILENAME).write_text(contents, encoding="utf-8")
class TestReadLockPid:
def test_missing_lock(self, tmp_path: Path) -> None:
assert reindex.read_lock_pid(tmp_path) is None
def test_reads_pid(self, tmp_path: Path) -> None:
_write_lock(tmp_path, "4321\n")
assert reindex.read_lock_pid(tmp_path) == 4321
def test_non_numeric_lock(self, tmp_path: Path) -> None:
_write_lock(tmp_path, "not-a-pid\n")
assert reindex.read_lock_pid(tmp_path) is None
def test_empty_lock(self, tmp_path: Path) -> None:
_write_lock(tmp_path, "")
assert reindex.read_lock_pid(tmp_path) is None
class TestIsServerRunning:
def test_no_lock(self, tmp_path: Path) -> None:
assert reindex.is_server_running(tmp_path) is False
def test_live_process(self, tmp_path: Path) -> None:
_write_lock(tmp_path, "1\n") # pid 1 always exists
assert reindex.is_server_running(tmp_path) is True
def test_stale_lock_treated_as_free(self, tmp_path: Path) -> None:
_write_lock(tmp_path, "999999999\n")
assert reindex.is_server_running(tmp_path) is False
class TestLoadPerCore:
def test_divides_load_by_cpu_count(self, tmp_path: Path) -> None:
loadavg = tmp_path / "loadavg"
loadavg.write_text("8.00 4.00 2.00 3/1234 5678\n", encoding="utf-8")
with (
patch(f"{MOD}.LOADAVG_PATH", loadavg),
patch(f"{MOD}.os.cpu_count", return_value=4),
):
assert reindex.load_per_core() == 2.0
def test_missing_file_is_not_busy(self, tmp_path: Path) -> None:
with patch(f"{MOD}.LOADAVG_PATH", tmp_path / "absent"):
assert reindex.load_per_core() == 0.0
def test_unparseable_content_is_not_busy(self, tmp_path: Path) -> None:
loadavg = tmp_path / "loadavg"
loadavg.write_text("garbage\n", encoding="utf-8")
with patch(f"{MOD}.LOADAVG_PATH", loadavg):
assert reindex.load_per_core() == 0.0
def test_empty_file_is_not_busy(self, tmp_path: Path) -> None:
loadavg = tmp_path / "loadavg"
loadavg.write_text("", encoding="utf-8")
with patch(f"{MOD}.LOADAVG_PATH", loadavg):
assert reindex.load_per_core() == 0.0
def test_unknown_cpu_count_falls_back_to_one(self, tmp_path: Path) -> None:
loadavg = tmp_path / "loadavg"
loadavg.write_text("3.00 1.00 1.00 1/1 1\n", encoding="utf-8")
with (
patch(f"{MOD}.LOADAVG_PATH", loadavg),
patch(f"{MOD}.os.cpu_count", return_value=None),
):
assert reindex.load_per_core() == 3.0
class TestBlockingReason:
@patch(f"{MOD}.is_server_running", return_value=True)
def test_server_running_blocks(self, mock_running: MagicMock) -> None:
reason = reindex.blocking_reason(Path("/store"))
assert reason is not None
assert "already using this store" in reason
@patch(f"{MOD}.load_per_core", return_value=4.0)
@patch(f"{MOD}.is_server_running", return_value=False)
def test_busy_machine_blocks(
self,
mock_running: MagicMock,
mock_load: MagicMock,
) -> None:
reason = reindex.blocking_reason(Path("/store"))
assert reason is not None
assert "machine is busy" in reason
@patch(f"{MOD}.load_per_core", return_value=4.0)
@patch(f"{MOD}.is_server_running", return_value=False)
def test_ignore_load_overrides(
self,
mock_running: MagicMock,
mock_load: MagicMock,
) -> None:
assert reindex.blocking_reason(Path("/store"), ignore_load=True) is None
@patch(f"{MOD}.load_per_core", return_value=0.2)
@patch(f"{MOD}.is_server_running", return_value=False)
def test_idle_machine_is_clear(
self,
mock_running: MagicMock,
mock_load: MagicMock,
) -> None:
assert reindex.blocking_reason(Path("/store")) is None
class TestRunReindex:
def test_missing_interpreter(self) -> None:
with patch(f"{MOD}.KNOWLEDGE_RAG_PYTHON", Path("/nonexistent/python")):
assert reindex.run_reindex(Path("/store")) == 127
@patch(f"{MOD}.subprocess.run")
def test_passes_store_and_repo_root(self, mock_run: MagicMock) -> None:
mock_run.return_value = subprocess.CompletedProcess([], returncode=0)
with (
patch.object(Path, "exists", return_value=True),
patch.dict(f"{MOD}.os.environ", {}, clear=True),
):
assert reindex.run_reindex(Path("/store")) == 0
env = mock_run.call_args.kwargs["env"]
assert env["KNOWLEDGE_RAG_DIR"] == "/store"
assert env["PYTHONPATH"].endswith("testsAndMisc")
@patch(f"{MOD}.subprocess.run")
def test_prepends_to_existing_pythonpath(self, mock_run: MagicMock) -> None:
mock_run.return_value = subprocess.CompletedProcess([], returncode=3)
with (
patch.object(Path, "exists", return_value=True),
patch.dict(f"{MOD}.os.environ", {"PYTHONPATH": "/pre"}, clear=True),
):
assert reindex.run_reindex(Path("/store")) == 3
assert mock_run.call_args.kwargs["env"]["PYTHONPATH"].endswith(":/pre")