Files
creator3/tests/test_suche_laden.py
2026-07-24 11:23:18 +02:00

179 lines
7.8 KiB
Python

"""Suche: Fallback-Kette, Circuit Breaker, URL-Dedupe. Laden: Snapshot stabil."""
import pytest
from backend import config, db, laden, suche
from .conftest import topic_anlegen
def test_url_normieren():
assert suche.url_normieren("HTTPS://Beispiel.de/Pfad/?utm_source=x&a=1") == \
"https://beispiel.de/Pfad?a=1"
assert suche.url_normieren("https://beispiel.de/seite/") == \
suche.url_normieren("https://beispiel.de/seite")
async def test_fallback_kette(monkeypatch):
monkeypatch.setattr(config, "FAKE", False)
suche._ausfaelle.clear()
async def leer_wiki(q):
return []
async def kaputt(q):
raise RuntimeError("down")
async def liefert(q):
return [{"titel": "T", "url": "https://x.de/a", "backend": "b2"}]
monkeypatch.setattr(suche, "_wikipedia", leer_wiki)
monkeypatch.setattr(suche, "_KETTE", (("b1", kaputt), ("b2", liefert)))
monkeypatch.setattr(config, "SUCHE_RETRIES", 0)
treffer, fehler = await suche.web_suchen("query")
assert [t["backend"] for t in treffer] == ["b2"]
assert fehler == ["b1: RuntimeError"]
async def test_circuit_breaker(monkeypatch):
monkeypatch.setattr(config, "FAKE", False)
monkeypatch.setattr(config, "SUCHE_RETRIES", 0)
suche._ausfaelle.clear()
aufrufe = {"n": 0}
async def leer_wiki(q):
return []
async def kaputt(q):
aufrufe["n"] += 1
raise RuntimeError("down")
monkeypatch.setattr(suche, "_wikipedia", leer_wiki)
monkeypatch.setattr(suche, "_KETTE", (("b1", kaputt),))
for _ in range(config.CIRCUIT_BREAKER_N + 3):
await suche.web_suchen("q")
assert aufrufe["n"] == config.CIRCUIT_BREAKER_N # danach übersprungen
async def test_suche_dedupliziert_urls():
topic = topic_anlegen()
run_id = db.insert("runs", topic=topic, status="running")
t = {"run_id": run_id, "topic": topic, "runde": 1,
"payload": db.j({"query": "thema ueberblick r1"})}
erg1 = await suche.suchen({**t})
erg2 = await suche.suchen({**t})
assert erg1.daten["neu"] == 1
assert erg2.daten["neu"] == 0 # gleiche URL → kein zweiter Laden-Task
async def test_inhalts_urteil_sortiert_nach_dem_laden_aus():
"""Kein Titel-Filter mehr — das Urteil fällt auf dem geladenen Inhalt
(Lektion 102). Fake-Handler: Titel „Werbeliste" → nein."""
topic = topic_anlegen()
run_id = db.insert("runs", topic=topic, status="running")
qid = db.insert("quellen", topic=topic, titel="Werbeliste Tools 2026",
url="https://beispiel.de/grundlagen",
url_norm="w1", backend="ddgs", zweck="korpus")
erg = await laden.laden({"run_id": run_id, "topic": topic, "runde": 1,
"knoten": "laden", "payload": db.j({"quelle_id": qid})})
assert erg.daten.get("aussortiert")
q = db.one("SELECT status, grund FROM quellen WHERE id=?", qid)
assert q["status"] == "aussortiert" and q["grund"]
assert erg.neue_tasks == [] # keine Soll-Extraktion für Aussortierte
async def test_wikipedia_seed_umgeht_deckel(monkeypatch):
monkeypatch.setattr(config, "QUELLEN_MAX", 1)
topic = topic_anlegen(name="seedtest", titel="Pomodoro Technik")
run_id = db.insert("runs", topic=topic, status="running")
erg = await suche.suchen({"run_id": run_id, "topic": topic, "runde": 1,
"item": "r1:wikiseed",
"payload": db.j({"seed": True})})
assert erg.daten["neu"] == 2 # de+en trotz Deckel 1
urls = [q["url"] for q in db.query(
"SELECT url FROM quellen WHERE topic=?", topic)]
assert any("de.wikipedia.org/wiki/Pomodoro_Technik" in u for u in urls)
assert any("en.wikipedia.org" in u for u in urls)
# idempotent: zweiter Lauf legt nichts Neues an
erg2 = await suche.suchen({"run_id": run_id, "topic": topic, "runde": 1,
"item": "r1:wikiseed",
"payload": db.j({"seed": True})})
assert erg2.daten["neu"] == 0
async def test_quellen_deckel(monkeypatch):
monkeypatch.setattr(config, "QUELLEN_MAX", 1)
topic = topic_anlegen()
run_id = db.insert("runs", topic=topic, status="running")
basis = {"run_id": run_id, "topic": topic, "runde": 1, "item": "t"}
erg1 = await suche.suchen({**basis,
"payload": db.j({"query": "thema ueberblick r1"})})
erg2 = await suche.suchen({**basis,
"payload": db.j({"query": "thema tutorial r1"})})
assert erg1.daten["neu"] == 1
assert erg2.daten["neu"] == 0 # Deckel erreicht — keine neue Quelle
async def test_quellen_deckel_pro_topic(monkeypatch):
monkeypatch.setattr(config, "QUELLEN_MAX", 1)
topic = topic_anlegen()
db.update("topics", "name=?", (topic,), quellen_max=0) # 0 = unbegrenzt
run_id = db.insert("runs", topic=topic, status="running")
basis = {"run_id": run_id, "topic": topic, "runde": 1, "item": "t"}
erg1 = await suche.suchen({**basis,
"payload": db.j({"query": "thema ueberblick r1"})})
erg2 = await suche.suchen({**basis,
"payload": db.j({"query": "thema tutorial r1"})})
assert erg1.daten["neu"] == 1
assert erg2.daten["neu"] == 1 # Topic-Einstellung schlägt globalen Deckel
async def test_laden_snapshot_stabil():
topic = topic_anlegen()
run_id = db.insert("runs", topic=topic, status="running")
qid = db.insert("quellen", topic=topic, url="https://beispiel.de/grundlagen",
url_norm="https://beispiel.de/grundlagen", zweck="korpus")
t = {"run_id": run_id, "topic": topic, "runde": 1,
"knoten": "laden", "payload": db.j({"quelle_id": qid})}
erg1 = await laden.laden({**t})
q = db.one("SELECT * FROM quellen WHERE id=?", qid)
assert q["status"] == "geladen" and q["snapshot"]
inhalt1 = laden.snapshot_lesen(q)
erg2 = await laden.laden({**t}) # Resume: kein Neuschreiben
assert erg2.daten.get("skip")
assert laden.snapshot_lesen(q) == inhalt1
assert len(erg1.neue_tasks) >= 1 # Soll-Extraktions-Chunks
async def test_laden_leere_quelle_befund():
topic = topic_anlegen()
run_id = db.insert("runs", topic=topic, status="running")
qid = db.insert("quellen", topic=topic, url="https://unbekannt.example/x",
url_norm="https://unbekannt.example/x")
erg = await laden.laden({"run_id": run_id, "topic": topic, "runde": 1,
"knoten": "laden", "payload": db.j({"quelle_id": qid})})
assert erg.neue_tasks == []
assert db.one("SELECT status FROM quellen WHERE id=?", qid)["status"] == "leer"
assert db.one("SELECT id FROM befunde WHERE run_id=? AND art='leer'", run_id)
async def test_seed_loest_wikipedia_titel_auf(monkeypatch):
"""Audit: Seed-Titel „AlpineJS" existiert nicht — Artikel heißt
„Alpine.js". Erst Wiki-Suche, direkter URL-Bau nur als Fallback."""
monkeypatch.setattr(config, "FAKE", False)
async def wiki(q):
assert q == "AlpineJS"
return [{"titel": "Alpine.js", "backend": "wikipedia_de",
"url": "https://de.wikipedia.org/wiki/Alpine.js"},
{"titel": "Nebentreffer", "backend": "wikipedia_de",
"url": "https://de.wikipedia.org/wiki/Nebentreffer"}]
monkeypatch.setattr(suche, "_wikipedia", wiki)
topic = topic_anlegen(name="seedaufl", titel="AlpineJS")
run_id = db.insert("runs", topic=topic, status="running")
await suche.suchen({"run_id": run_id, "topic": topic, "runde": 1,
"item": "r1:wikiseed", "payload": db.j({"seed": True})})
urls = [q["url"] for q in db.query("SELECT url FROM quellen WHERE topic=?",
topic)]
assert "https://de.wikipedia.org/wiki/Alpine.js" in urls # 1. Treffer zählt
assert any("en.wikipedia.org/wiki/AlpineJS" in u for u in urls) # Fallback