"""Tests: Hybrid-Retrieval (BM25-only offline): Fusion, Entry-Dedup, cross_ref-Erweiterung, leeres Retrieval.""" 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_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)