"""DSE Embedding-Recall — deterministische semantische Schicht (gecacht). Testet die reine Logik OHNE Embedding-Service: Cache-Treffer-Pfad, Schwellen-Filter, Kandidaten-Schnitt, Reachability-Guard. Das Einbetten selbst (Embedding-Service) ist Integration und wird auf macmini/Prod validiert. """ from __future__ import annotations import asyncio import json import compliance.services.specialist_agents.dse._embedding_recall as er _TEXT = ("Datenschutzerklaerung der Muster GmbH. " * 20) # > 100 Zeichen def _seed_cache(tmp_path, scores: dict[str, float]) -> str: p = tmp_path / "dse_embed_cache.json" p.write_text(json.dumps({er._doc_hash(_TEXT): scores})) return str(p) def test_doc_hash_deterministic(): # feste Funktion: gleicher Text → gleicher Hash (Reproduzierbarkeit) assert er._doc_hash(_TEXT) == er._doc_hash(_TEXT) assert er._doc_hash("a") != er._doc_hash("b") def test_cache_hit_threshold_filter(tmp_path, monkeypatch): # Cache-Treffer: kein Embedding-Service nötig. Nur Scores >= Schwelle UND # in den Kandidaten werden zurückgegeben. scores = {"DATA-1": 0.71, "DATA-2": 0.60, "AUTH-3": 0.68, "SEC-4": 0.50} monkeypatch.setenv("DSE_EMBED_CACHE", _seed_cache(tmp_path, scores)) monkeypatch.setattr(er, "_CACHE_PATH", str(tmp_path / "dse_embed_cache.json")) cands = ["DATA-1", "DATA-2", "AUTH-3", "SEC-4"] out = asyncio.run(er.embedding_recall(_TEXT, cands, threshold=0.65)) # >=0.65: DATA-1 (0.71), AUTH-3 (0.68). NICHT DATA-2 (0.60), SEC-4 (0.50). assert out == {"DATA-1", "AUTH-3"} def test_cache_hit_candidate_intersection(tmp_path, monkeypatch): # Nur Kandidaten (durchgefallene Controls) zählen — andere ignoriert. scores = {"DATA-1": 0.90, "DATA-2": 0.90} monkeypatch.setattr(er, "_CACHE_PATH", str(tmp_path / "c.json")) (tmp_path / "c.json").write_text(json.dumps({er._doc_hash(_TEXT): scores})) out = asyncio.run(er.embedding_recall(_TEXT, ["DATA-1"], threshold=0.65)) assert out == {"DATA-1"} # DATA-2 nicht in Kandidaten def test_empty_inputs(): assert asyncio.run(er.embedding_recall("zu kurz", ["X"])) == set() assert asyncio.run(er.embedding_recall(_TEXT, [])) == set() def test_service_down_returns_empty(tmp_path, monkeypatch): # Kein Cache + Service nicht erreichbar → leer (deterministischer Layer trägt), # KEIN Hang. monkeypatch.setattr(er, "_CACHE_PATH", str(tmp_path / "none.json")) async def _unreachable(timeout=2.0): return False monkeypatch.setattr(er, "_embedding_reachable", _unreachable) out = asyncio.run(er.embedding_recall(_TEXT, ["DATA-1"])) assert out == set()