Files
pv-agent/tests/test_retrieve.py
T

92 lines
3.3 KiB
Python

"""Tests: Hybrid-Retrieval (BM25-only offline): Fusion, Entry-Dedup,
cross_ref-Erweiterung, leeres Retrieval."""
from concurrent.futures import ThreadPoolExecutor
import pytest
from agent.retrieve import Retriever
@pytest.fixture
def retriever(mini_index):
r = Retriever(mini_index)
yield r
r.close()
def test_search_finds_expected_entry(retriever):
results = retriever.search("Was ist Altersteilzeit und Lohnausgleich?")
assert results, "Retrieval sollte Treffer liefern"
assert results[0].entry_id == "lb-min-01"
assert results[0].stand == "2026-01"
# Beim Section-Schnitt entscheidet BM25-Längennormalisierung; hier zählt
# der richtige Eintrag, nicht der konkrete Abschnitt.
def test_search_cross_ref_expansion(retriever):
"""Top-Treffer lb-min-02 → cross_ref lb-min-01 wird als Erweiterung ergänzt."""
results = retriever.search("Urlaubsanspruch fünf Werktage")
main = [r for r in results if r.source != "cross_ref"]
extra = [r for r in results if r.source == "cross_ref"]
assert main and main[0].entry_id == "lb-min-02"
assert any(r.entry_id == "lb-min-01" for r in extra)
def test_search_no_match_returns_empty(retriever):
results = retriever.search("kanadische quellensteuer bermuda")
assert results == []
def test_search_dedups_entries(mini_index):
"""Pro Eintrag höchstens ein Haupt-Chunk im Kontext (Entry-Level-Dedup)."""
r = Retriever(mini_index)
try:
results = r.search("Lohnausgleich Urlaubsentgelt Lohnverrechnung",
n_entries=2)
main_ids = [x.entry_id for x in results if x.source != "cross_ref"]
assert len(main_ids) == len(set(main_ids))
assert len(main_ids) <= 2
finally:
r.close()
def test_recency_boost_prefers_newer_stand(retriever):
"""Milde Aktualitätsgewichtung: bei Gleichstand gewinnt der neuere Stand.
'Urlaubsanspruch' (2026-07) sollte vor 'Altersteilzeit' (2026-01)
landen, wenn beide im Kontext sind und der Query beide trifft.
"""
results = retriever.search("Urlaubsanspruch Altersteilzeit")
main = [r for r in results if r.source != "cross_ref"]
if {r.entry_id for r in main} >= {"lb-min-01", "lb-min-02"}:
# Beide im Kontext -> Reihenfolge prüfen ist nur bei Score-Nähe sinnvoll;
# hier reicht die Existenz-Annahme, der Boost ist bewusst minimal.
assert main[0].entry_id in {"lb-min-01", "lb-min-02"}
def test_shared_retriever_is_safe_across_worker_threads(retriever):
"""FastAPI darf dieselbe Retriever-Instanz in mehreren Threads nutzen."""
questions = ["Altersteilzeit Lohnausgleich", "Urlaubsanspruch Werktage"] * 4
with ThreadPoolExecutor(max_workers=4) as pool:
result_sets = list(pool.map(retriever.search, questions))
assert all(results for results in result_sets)
def test_stats_report(mini_index):
r = Retriever(mini_index)
try:
s = r.stats()
assert s["n_entries"] == 3
assert s["dense_available"] is False # embed_off=True
assert s["stand_min"] == "202601"
assert s["stand_max"] == "202607"
finally:
r.close()
def test_retriever_requires_index(tmp_path):
from agent.config import Config
cfg = Config(kb_dir="wissensbasis", db_path=str(tmp_path / "missing.db"))
with pytest.raises(RuntimeError, match="ingest"):
Retriever(cfg)