mirror of
http://100.103.83.12:3003/fegger/pv-agent.git
synced 2026-09-17 15:46:23 +00:00
92 lines
3.3 KiB
Python
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) |