update
This commit is contained in:
@@ -11,7 +11,7 @@ from backend import config, db, engine, llm
|
||||
|
||||
# Kleine Fake-Welt → kleine Schwellen
|
||||
config.SOLL_PUNKTE_MIN = 3
|
||||
config.RECHERCHE_RUNDEN_MAX = 2
|
||||
|
||||
config.KAPITEL_SOLL_PUNKTE = 2
|
||||
config.LUECKEN_RUNDEN_MAX = 1
|
||||
config.POLL_SEKUNDEN = 0.05
|
||||
@@ -20,8 +20,12 @@ config.DDGS_PAUSE_S = 0
|
||||
config.QUERIES_JE_LENS = 2
|
||||
config.VERDICHTUNG_ZIEL = 1 # zwingt den Verdichtungs-Pfad in der Fake-Welt
|
||||
config.TAKT_SEKUNDEN = 0 # kein 3s-Takt in Tests
|
||||
config.SEED_PFLICHT = False # Fake-Welt hat keine echten Startquellen
|
||||
config.QUELLEN_MIN = 1
|
||||
|
||||
engine.module_laden()
|
||||
from backend import graph as _graph
|
||||
_graph.get().knoten["gate_korpus"].runden_max = 2 # kleine Fake-Welt
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
||||
@@ -79,7 +79,7 @@ async def test_llm_pruefung_buendel_teilantwort(monkeypatch):
|
||||
|
||||
async def antwort(**kw):
|
||||
assert f"=== SECTION {b2} ===" in kw["werte"]["sections"]
|
||||
return f"===OK===\nSECTION: {b1}" # b2 fehlt in beiden Stimmen
|
||||
return f"===LEER===\nSECTION: {b1}" # b2 fehlt in beiden Stimmen
|
||||
|
||||
monkeypatch.setattr(llm_mod, "call", antwort)
|
||||
task = dict(db.one("SELECT * FROM tasks WHERE id=?", t1))
|
||||
|
||||
@@ -75,6 +75,7 @@ async def test_keiner_atome_nicht_erneut_gefragt():
|
||||
db.insert("soll", topic=topic, punkt="Punkt", status="bestaetigt")
|
||||
aid = _atom(topic, "Fremdes Atom", "Definition ohne Bezug zum Punkt hier.")
|
||||
task = {"run_id": run_id, "topic": topic, "runde": 1, "item": "z0",
|
||||
"knoten": "soll_zuordnung",
|
||||
"payload": db.j({"atom_ids": [aid]})}
|
||||
# Fake-Zuordnung antwortet KEINER (Titel unbekannt) → Atom bleibt ohne Soll
|
||||
erg = await inventar.soll_zuordnung(task)
|
||||
@@ -83,7 +84,8 @@ async def test_keiner_atome_nicht_erneut_gefragt():
|
||||
assert atom["soll_id"] is None and atom["soll_geprueft"] == 1
|
||||
# Dedup erzeugt für dieses Atom KEINE neue Zuordnungs-Aufgabe mehr
|
||||
erg2 = await dedup.dedup({"run_id": run_id, "topic": topic, "runde": 1,
|
||||
"item": "r1:dedup", "payload": "{}"})
|
||||
"knoten": "dedup", "item": "r1:dedup",
|
||||
"payload": "{}"})
|
||||
assert erg2.neue_tasks == []
|
||||
|
||||
|
||||
@@ -127,7 +129,7 @@ async def test_enthalten_merged_in_den_umfassenderen():
|
||||
"Deutlich längere Definition mit allen Sonderfällen und mehr "
|
||||
"Kontext für Lernende.", soll_id=sid, quelle=2, start=0, ende=10)
|
||||
await dedup.dedup({"run_id": run_id, "topic": topic, "runde": 1,
|
||||
"item": "r1:dedup", "payload": "{}"})
|
||||
"knoten": "dedup", "item": "r1:dedup", "payload": "{}"})
|
||||
gewinner = db.one("SELECT id FROM atome WHERE topic=? AND status='aktiv'",
|
||||
topic)
|
||||
assert gewinner["id"] == lang # längere Definition gewinnt
|
||||
@@ -199,7 +201,7 @@ async def test_fehlendes_urteil_nachcall_dann_befund(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(llm_mod, "call", ohne_urteil)
|
||||
await dedup.dedup({"run_id": run_id, "topic": topic, "runde": 1,
|
||||
"item": "r1:dedup", "payload": "{}"})
|
||||
"knoten": "dedup", "item": "r1:dedup", "payload": "{}"})
|
||||
assert calls["n"] == 4 # 2 Panel-Stimmen + 2 Nach-Call-Stimmen
|
||||
assert db.one("SELECT id FROM befunde WHERE run_id=? AND art='parse'",
|
||||
run_id)
|
||||
@@ -207,3 +209,42 @@ async def test_fehlendes_urteil_nachcall_dann_befund(monkeypatch):
|
||||
aktiv = db.query("SELECT id FROM atome WHERE topic=? AND status='aktiv'",
|
||||
topic)
|
||||
assert {r["id"] for r in aktiv} == {a, b} # nichts still verschmolzen
|
||||
|
||||
|
||||
def test_fakten_kanal_findet_studien_paraphrase():
|
||||
"""Audit: „Ariga & Lleras 2011" stand 3× im Guide, Wort-Jaccard nur 0.21 —
|
||||
geteilte Zahlen+Namen machen das Paar jetzt zum Kandidaten."""
|
||||
topic = topic_anlegen()
|
||||
_section(topic, 0, "Die Untersuchung von Ariga und Lleras aus dem Jahr "
|
||||
"2011 zeigte über lange Zeit stabile Aufmerksamkeit "
|
||||
"bei den Probanden im Labor.")
|
||||
_section(topic, 1, "Kurze bewusste Unterbrechungen halfen laut Ariga "
|
||||
"sowie Lleras 2011 den Teilnehmern messbar besser "
|
||||
"durch anstrengende Arbeitsphasen zu kommen.",
|
||||
ziel_suffix="2")
|
||||
paare = redundanz.kandidaten_paare(topic)
|
||||
assert len(paare) == 1
|
||||
|
||||
|
||||
async def test_wortgleiche_paare_ohne_freispruch(monkeypatch):
|
||||
"""Fast wörtliche Paare (gemeinsame 5-Wort-Folge) darf das Panel nicht
|
||||
als „didaktisch gewollt" freisprechen — immer Befund."""
|
||||
from backend import llm as llm_mod
|
||||
topic = topic_anlegen()
|
||||
run_id = db.insert("runs", topic=topic, status="running")
|
||||
satz = ("Die Pausen sollten proportional zur Arbeitszeit verlängert "
|
||||
"werden, sagt die Studie aus Maastricht deutlich.")
|
||||
_section(topic, 0, satz)
|
||||
_section(topic, 1, satz + " Dazu kommt hier noch ein Zusatz.",
|
||||
ziel_suffix="2")
|
||||
|
||||
async def gewollt(**kw):
|
||||
return "===URTEIL===\nPAAR: 1\nURTEIL: didaktisch_gewollt"
|
||||
|
||||
monkeypatch.setattr(llm_mod, "call", gewollt)
|
||||
erg = await redundanz.pruefen({"run_id": run_id, "topic": topic,
|
||||
"knoten": "redundanz", "runde": 1,
|
||||
"item": "r1:redundanz", "payload": "{}"})
|
||||
assert erg.daten["befunde"] == 1
|
||||
assert db.one("SELECT id FROM befunde WHERE run_id=? AND art='redundanz'",
|
||||
run_id)
|
||||
|
||||
@@ -48,7 +48,7 @@ def _pruefe_endzustand(topic: str):
|
||||
assert "TITEL:" not in guide and "DEFINITION:" not in guide
|
||||
assert "## Fehlerbehandlung" in guide
|
||||
# Lücken-Atom kam über Web-Nachrecherche
|
||||
assert db.one("SELECT id FROM quellen WHERE topic=? AND zweck LIKE 'luecke:%' "
|
||||
assert db.one("SELECT id FROM quellen WHERE topic=? AND zweck='luecke' "
|
||||
"AND status='geladen'", topic) is not None
|
||||
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ async def test_gate_guide_neuschreiben_bei_kaputten_markern():
|
||||
assert writer_tasks and writer_tasks[0]["payload"]["neu"] is True
|
||||
# Neuschreiben resettet den Fix-Cap und ersetzt den Text
|
||||
await guide.writer({"run_id": run_id, "topic": topic, "runde": 2,
|
||||
"knoten": "writer",
|
||||
"item": f"r2:baustein:{bid}",
|
||||
"payload": db.j({"baustein_id": bid, "neu": True})})
|
||||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", bid)
|
||||
@@ -201,8 +202,9 @@ async def test_vorwaerts_ignoriert_verlinkte_erwaehnung():
|
||||
if b["art"] == "vorwaerts"]
|
||||
|
||||
|
||||
async def test_gate_verwirft_soll_lose_atome():
|
||||
from backend import inventar
|
||||
async def test_gate_verwirft_soll_lose_atome(monkeypatch):
|
||||
from backend import config, inventar
|
||||
monkeypatch.setattr(config, "BEIFANG_MAX_QUOTE", 1.0) # Stop hier nicht Testziel
|
||||
topic = topic_anlegen()
|
||||
run_id = db.insert("runs", topic=topic, status="running")
|
||||
geprueft = db.insert("atome", topic=topic, titel="Beifang", status="aktiv",
|
||||
@@ -210,7 +212,8 @@ async def test_gate_verwirft_soll_lose_atome():
|
||||
frisch = db.insert("atome", topic=topic, titel="Frisch", status="aktiv",
|
||||
soll_geprueft=0)
|
||||
await inventar.gate({"run_id": run_id, "topic": topic, "runde": 1,
|
||||
"item": "runde:1", "payload": "{}"})
|
||||
"knoten": "gate_inventar", "item": "runde:1",
|
||||
"payload": "{}"})
|
||||
assert db.one("SELECT status FROM atome WHERE id=?", geprueft)[
|
||||
"status"] == "verworfen"
|
||||
assert db.one("SELECT status FROM atome WHERE id=?", frisch)[
|
||||
@@ -280,3 +283,66 @@ async def test_budget_stoppt_lauf():
|
||||
import pytest
|
||||
with pytest.raises(BudgetErschoepft):
|
||||
budget_pruefen(run["id"])
|
||||
|
||||
|
||||
async def test_stop_seed_fehlt(monkeypatch):
|
||||
"""Vollständigkeit ist DAS Kernziel: ohne nützlich geladene Startquelle
|
||||
PAUSE statt Mini-Guide (Lektion 103/106)."""
|
||||
import pytest
|
||||
from backend import config, korpus, llm
|
||||
monkeypatch.setattr(config, "SEED_PFLICHT", True)
|
||||
monkeypatch.setattr(config, "QUELLEN_MIN", 1)
|
||||
topic = topic_anlegen()
|
||||
run_id = db.insert("runs", topic=topic, status="running")
|
||||
db.insert("quellen", topic=topic, url="https://a.de", url_norm="a",
|
||||
backend="ddgs", status="geladen", zweck="korpus")
|
||||
for i in range(3):
|
||||
db.insert("soll", topic=topic, punkt=f"P{i}", status="bestaetigt")
|
||||
db.insert("tasks", run_id=run_id, topic=topic, knoten="gate_korpus",
|
||||
item="runde:1", runde=1, status="fertig",
|
||||
ergebnis=db.j({"bestaetigt": 3}))
|
||||
with pytest.raises(llm.LaufPause, match="seed_fehlt"):
|
||||
await korpus.gate({"run_id": run_id, "topic": topic, "runde": 2,
|
||||
"knoten": "gate_korpus", "item": "runde:2",
|
||||
"payload": "{}"})
|
||||
|
||||
|
||||
async def test_stop_zu_wenige_quellen(monkeypatch):
|
||||
import pytest
|
||||
from backend import config, korpus, llm
|
||||
monkeypatch.setattr(config, "SEED_PFLICHT", False)
|
||||
monkeypatch.setattr(config, "QUELLEN_MIN", 5)
|
||||
topic = topic_anlegen()
|
||||
run_id = db.insert("runs", topic=topic, status="running")
|
||||
db.insert("quellen", topic=topic, url="https://a.de", url_norm="a",
|
||||
backend="ddgs", status="geladen", zweck="korpus")
|
||||
for i in range(3):
|
||||
db.insert("soll", topic=topic, punkt=f"P{i}", status="bestaetigt")
|
||||
db.insert("tasks", run_id=run_id, topic=topic, knoten="gate_korpus",
|
||||
item="runde:1", runde=1, status="fertig",
|
||||
ergebnis=db.j({"bestaetigt": 3}))
|
||||
with pytest.raises(llm.LaufPause, match="quellen"):
|
||||
await korpus.gate({"run_id": run_id, "topic": topic, "runde": 2,
|
||||
"knoten": "gate_korpus", "item": "runde:2",
|
||||
"payload": "{}"})
|
||||
|
||||
|
||||
async def test_stop_beifang_quote():
|
||||
""">50 % der Atome als Beifang verworfen → Soll zu eng → PAUSE."""
|
||||
import pytest
|
||||
from backend import inventar, llm
|
||||
topic = topic_anlegen()
|
||||
run_id = db.insert("runs", topic=topic, status="running")
|
||||
s = db.insert("soll", topic=topic, punkt="P", status="bestaetigt",
|
||||
belege=db.j([]))
|
||||
behalten = db.insert("atome", topic=topic, titel="Gut", status="aktiv",
|
||||
soll_id=s, soll_geprueft=1)
|
||||
for i in range(2):
|
||||
db.insert("atome", topic=topic, titel=f"Beifang{i}", status="aktiv",
|
||||
soll_geprueft=1)
|
||||
with pytest.raises(llm.LaufPause, match="beifang"):
|
||||
await inventar.gate({"run_id": run_id, "topic": topic, "runde": 1,
|
||||
"knoten": "gate_inventar", "item": "runde:1",
|
||||
"payload": "{}"})
|
||||
assert db.one("SELECT status FROM atome WHERE id=?", behalten)[
|
||||
"status"] == "aktiv"
|
||||
|
||||
@@ -42,3 +42,19 @@ def test_layout_vollstaendig():
|
||||
lay = graph.get().layout()
|
||||
assert set(lay["knoten"]) == set(graph.get().knoten)
|
||||
assert all("spalte" in v and "zeile" in v for v in lay["knoten"].values())
|
||||
|
||||
|
||||
def test_skills_von_eine_wahrheit():
|
||||
"""Worker-Skills kommen NUR aus pipeline.yaml; nicht deklarierte
|
||||
extra_skills sind ein harter Fehler (Drift bricht laut, nicht still)."""
|
||||
assert graph.skills_von("writer")[-1] == "writer"
|
||||
assert graph.skills_von("fix", extra="klaerung")[-1] == "klaerung"
|
||||
with pytest.raises(graph.GraphFehler, match="nicht deklariert"):
|
||||
graph.skills_von("writer", extra="gibtsnicht")
|
||||
|
||||
|
||||
def test_timeout_und_runden_aus_dem_graph():
|
||||
assert graph.timeout_fuer("writer") == 420
|
||||
assert graph.timeout_fuer("dedup", n=10) == 120 + 8 * 10
|
||||
assert graph.timeout_fuer("unbekannter_knoten") == 180 # Default
|
||||
assert graph.get().knoten["gate_guide"].runden_max == 12
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
"""Konsens: 1 kuratierter Wikipedia-Beleg genügt, 1 Blog-Beleg nicht."""
|
||||
"""Konsens ohne 2-Quellen-Regel: 1 wörtlicher Beleg genügt (Zitat-Gate schützt);
|
||||
ohne Beleg bleibt der Punkt Kandidat."""
|
||||
from backend import db, korpus
|
||||
from .conftest import topic_anlegen
|
||||
|
||||
|
||||
async def test_wiki_einzelbeleg_bestaetigt():
|
||||
async def test_einzelbeleg_bestaetigt_ohne_beleg_nicht():
|
||||
topic = topic_anlegen()
|
||||
run_id = db.insert("runs", topic=topic, status="running")
|
||||
q_wiki = db.insert("quellen", topic=topic, url="https://de.wikipedia.org/w",
|
||||
url_norm="u1", backend="wikipedia_de", status="geladen")
|
||||
q_blog = db.insert("quellen", topic=topic, url="https://blog.de/x",
|
||||
url_norm="u2", backend="ddgs", status="geladen")
|
||||
db.insert("soll", topic=topic, punkt="Overflow-Pomodoros erklären",
|
||||
belege=db.j([{"quelle": q_wiki, "zitat": "z1"}]))
|
||||
db.insert("soll", topic=topic, punkt="Nur Blog-These",
|
||||
db.insert("soll", topic=topic, punkt="Blog-These mit Beleg",
|
||||
belege=db.j([{"quelle": q_blog, "zitat": "z2"}]))
|
||||
db.insert("soll", topic=topic, punkt="These ohne Beleg", belege=db.j([]))
|
||||
await korpus.soll_konsens({"run_id": run_id, "topic": topic, "runde": 1,
|
||||
"item": "r1:konsens", "payload": "{}"})
|
||||
wiki = db.one("SELECT status FROM soll WHERE punkt LIKE 'Overflow%'")
|
||||
blog = db.one("SELECT status FROM soll WHERE punkt LIKE 'Nur Blog%'")
|
||||
assert wiki["status"] == "bestaetigt" # kuratierte Quelle: 1 Beleg reicht
|
||||
assert blog["status"] == "kandidat" # Blog allein bestätigt nicht
|
||||
"knoten": "soll_konsens", "item": "r1:konsens",
|
||||
"payload": "{}"})
|
||||
mit = db.one("SELECT status FROM soll WHERE punkt LIKE 'Blog-These%'")
|
||||
ohne = db.one("SELECT status FROM soll WHERE punkt LIKE 'These ohne%'")
|
||||
assert mit["status"] == "bestaetigt" # 1 wörtlicher Beleg reicht
|
||||
assert ohne["status"] == "kandidat" # ohne Beleg keine Bestätigung
|
||||
|
||||
52
tests/test_lektorat.py
Normal file
52
tests/test_lektorat.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Lektorat: EIN Blick über den ganzen Guide, Befunde in die Fix-Maschine."""
|
||||
from backend import db, guide
|
||||
from .conftest import topic_anlegen
|
||||
|
||||
|
||||
def _guide_anlegen(topic):
|
||||
kid = db.insert("kapitel", topic=topic, titel="K1", ord=0)
|
||||
bids = []
|
||||
for i in range(2):
|
||||
zid = db.insert("lernziele", topic=topic, text=f"z{i}")
|
||||
bid = db.insert("bausteine", topic=topic, ziel_id=zid, titel=f"B{i}",
|
||||
kapitel_id=kid, ord=i)
|
||||
db.insert("sections", baustein_id=bid, text=f"Text der Section {i}.")
|
||||
bids.append(bid)
|
||||
return bids
|
||||
|
||||
|
||||
async def test_lektorat_meldet_befund(monkeypatch):
|
||||
from backend import llm as llm_mod
|
||||
topic = topic_anlegen()
|
||||
run_id = db.insert("runs", topic=topic, status="running")
|
||||
bids = _guide_anlegen(topic)
|
||||
tid = db.insert("tasks", run_id=run_id, topic=topic, knoten="lektorat",
|
||||
item="r1:lektorat", status="laufend")
|
||||
|
||||
async def antwort(**kw):
|
||||
assert f"[SECTION {bids[1]}]" in kw["werte"]["guide"]
|
||||
return (f"===BEFUND===\nSECTION: {bids[1]}\nART: redundanz\n"
|
||||
f"DETAIL: Fakt doppelt erklärt, kürzen.")
|
||||
|
||||
monkeypatch.setattr(llm_mod, "call", antwort)
|
||||
erg = await guide.lektorat({"id": tid, "run_id": run_id, "topic": topic,
|
||||
"knoten": "lektorat", "runde": 1,
|
||||
"item": "r1:lektorat", "payload": "{}"})
|
||||
assert erg.daten["befunde"] == 1
|
||||
b = db.one("SELECT * FROM befunde WHERE run_id=? AND knoten='lektorat'",
|
||||
run_id)
|
||||
assert b["art"] == "redundanz" and b["item"] == f"baustein:{bids[1]}"
|
||||
|
||||
|
||||
async def test_lektorat_laeuft_nur_einmal(monkeypatch):
|
||||
topic = topic_anlegen()
|
||||
run_id = db.insert("runs", topic=topic, status="running")
|
||||
_guide_anlegen(topic)
|
||||
db.insert("tasks", run_id=run_id, topic=topic, knoten="lektorat",
|
||||
item="r1:lektorat", status="fertig")
|
||||
t2 = db.insert("tasks", run_id=run_id, topic=topic, knoten="lektorat",
|
||||
item="r2:lektorat", status="laufend")
|
||||
erg = await guide.lektorat({"id": t2, "run_id": run_id, "topic": topic,
|
||||
"knoten": "lektorat", "runde": 2,
|
||||
"item": "r2:lektorat", "payload": "{}"})
|
||||
assert erg.daten.get("skip")
|
||||
@@ -59,7 +59,7 @@ async def test_cap_kein_retry(echt_modus, monkeypatch):
|
||||
async def cap(*a, **k):
|
||||
zaehler["n"] += 1
|
||||
return llm.ApiErgebnis(1, err="leere Antwort (stop=max_tokens)",
|
||||
tokens={"input": 10, "output": 5})
|
||||
cap=True, tokens={"input": 10, "output": 5})
|
||||
monkeypatch.setattr(llm, "_api", cap)
|
||||
assert await _call(run_id) is None
|
||||
assert zaehler["n"] == 1 # deterministisch — kein Neuversuch
|
||||
|
||||
@@ -32,3 +32,37 @@ def test_ziele_dedup_verschont_verschiedene():
|
||||
assert struktur._ziele_dedup(topic) == 0
|
||||
assert db.one("SELECT COUNT(*) c FROM lernziele WHERE topic=?",
|
||||
topic)["c"] == 2
|
||||
|
||||
|
||||
def test_ziele_dedup_facetten_paraphrase():
|
||||
"""Audit: Ganz-Satz-Jaccard kam nie über 0.36 — der Inhaltswort-Kern
|
||||
(Floskeln raus) merged Paraphrasen desselben Soll-Punkts."""
|
||||
topic = topic_anlegen()
|
||||
s = db.insert("soll", topic=topic, punkt="P", status="bestaetigt")
|
||||
db.insert("lernziele", topic=topic, titel="Wirkung verstehen", soll_id=s,
|
||||
text="Der Lernende kann die Wirkung der Technik verstehen.")
|
||||
db.insert("lernziele", topic=topic, titel="Wirkung einordnen", soll_id=s,
|
||||
text="Der Lernende kann Wirkung und Grenzen der Technik einordnen.")
|
||||
assert struktur._ziele_dedup(topic) == 1
|
||||
|
||||
|
||||
def test_ziele_dedup_nur_gleicher_soll():
|
||||
topic = topic_anlegen()
|
||||
s1 = db.insert("soll", topic=topic, punkt="P1", status="bestaetigt")
|
||||
s2 = db.insert("soll", topic=topic, punkt="P2", status="bestaetigt")
|
||||
db.insert("lernziele", topic=topic, titel="Wirkung verstehen", soll_id=s1,
|
||||
text="Der Lernende kann die Wirkung der Technik verstehen.")
|
||||
db.insert("lernziele", topic=topic, titel="Wirkung einordnen", soll_id=s2,
|
||||
text="Der Lernende kann Wirkung und Grenzen der Technik einordnen.")
|
||||
assert struktur._ziele_dedup(topic) == 0 # andere Soll-Punkte → kein Merge
|
||||
|
||||
|
||||
def test_baustein_min_wird_erzwungen():
|
||||
"""Audit: 10 Atome → 8 „Teil"-Gruppen mit 1-2 Atomen. MIN-Merge macht
|
||||
daraus Gruppen im Band, Quellreihenfolge bleibt."""
|
||||
from backend import config
|
||||
gruppen = [[{"id": i}] for i in range(8)]
|
||||
aus = struktur._min_erzwingen(gruppen)
|
||||
assert all(len(g) >= config.BAUSTEIN_MIN_ATOME for g in aus)
|
||||
assert all(len(g) <= config.BAUSTEIN_MAX_ATOME for g in aus)
|
||||
assert [a["id"] for g in aus for a in g] == list(range(8))
|
||||
|
||||
@@ -64,32 +64,20 @@ async def test_suche_dedupliziert_urls():
|
||||
assert erg2.daten["neu"] == 0 # gleiche URL → kein zweiter Laden-Task
|
||||
|
||||
|
||||
async def test_relevanz_filter_wirft_listen_raus(monkeypatch):
|
||||
"""Läuft OHNE Fake-Kurzschluss — genau der Pfad, der im Echtlauf brach."""
|
||||
monkeypatch.setattr(config, "FAKE", False)
|
||||
from backend import llm as llm_mod
|
||||
|
||||
async def urteil(**kw):
|
||||
assert "quellen-relevanz" in kw["skill_namen"]
|
||||
assert "Pasta" in kw["werte"]["treffer"] # Wiki läuft MIT durchs Urteil
|
||||
return "===RELEVANT===\nIDS: 0,1,3"
|
||||
monkeypatch.setattr(llm_mod, "call", urteil)
|
||||
treffer = [
|
||||
{"titel": "Pomodoro-Technik – Wikipedia",
|
||||
"url": "https://de.wikipedia.org/wiki/X", "backend": "wikipedia_de"},
|
||||
{"titel": "Guter Artikel", "url": "https://a.de/1", "backend": "ddgs"},
|
||||
{"titel": "Pasta", "url": "https://en.wikipedia.org/wiki/Pasta",
|
||||
"backend": "wikipedia_en"},
|
||||
{"titel": "Anleitung", "url": "https://a.de/3", "backend": "ddgs"},
|
||||
]
|
||||
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")
|
||||
task = {"run_id": run_id, "topic": topic, "item": "t", "runde": 1}
|
||||
ergebnis = await suche._relevanz_filter(task, "Thema", treffer)
|
||||
urls = [t["url"] for t in ergebnis]
|
||||
assert "https://de.wikipedia.org/wiki/X" in urls
|
||||
assert "https://a.de/1" in urls and "https://a.de/3" in urls
|
||||
assert "https://en.wikipedia.org/wiki/Pasta" not in urls # Off-Topic-Wiki raus
|
||||
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):
|
||||
@@ -144,7 +132,7 @@ async def test_laden_snapshot_stabil():
|
||||
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,
|
||||
"payload": db.j({"quelle_id": qid})}
|
||||
"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"]
|
||||
@@ -161,7 +149,30 @@ async def test_laden_leere_quelle_befund():
|
||||
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,
|
||||
"payload": db.j({"quelle_id": qid})})
|
||||
"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
|
||||
|
||||
@@ -21,6 +21,7 @@ def _welt(topic, n_vorteile=3):
|
||||
|
||||
def _task(run_id, topic, sid):
|
||||
return {"run_id": run_id, "topic": topic, "runde": 1,
|
||||
"knoten": "verdichtung",
|
||||
"item": f"r1:verd:soll:{sid}", "payload": db.j({"soll_id": sid})}
|
||||
|
||||
|
||||
@@ -90,6 +91,7 @@ async def test_facetten_check_erzeugt_fenster_task(tmp_path):
|
||||
soll_id=sid)
|
||||
db.insert("anker", atom_id=aid, quelle_id=qid, start=0, ende=20, zitat="z")
|
||||
erg = await inventar.gate({"run_id": run_id, "topic": topic, "runde": 1,
|
||||
"knoten": "gate_inventar",
|
||||
"item": "runde:1", "payload": "{}"})
|
||||
fac = [t for t in erg.neue_tasks if t["knoten"] == "luecke"
|
||||
and t["payload"].get("fenster")]
|
||||
@@ -102,6 +104,7 @@ async def test_facetten_check_erzeugt_fenster_task(tmp_path):
|
||||
db.insert("tasks", run_id=run_id, topic=topic, knoten="luecke",
|
||||
item=f"r{r}:{key}", status="fertig")
|
||||
erg2 = await inventar.gate({"run_id": run_id, "topic": topic, "runde": 2,
|
||||
"knoten": "gate_inventar",
|
||||
"item": "runde:2", "payload": "{}"})
|
||||
assert not any(b["art"] == "facette" for b in erg2.befunde)
|
||||
assert db.one("SELECT status FROM befunde WHERE art='facette'")[
|
||||
@@ -112,11 +115,13 @@ async def test_gate_erzeugt_verdichtungs_task():
|
||||
topic = topic_anlegen()
|
||||
run_id, sid, ids = _welt(topic)
|
||||
erg = await inventar.gate({"run_id": run_id, "topic": topic, "runde": 1,
|
||||
"knoten": "gate_inventar",
|
||||
"item": "runde:1", "payload": "{}"})
|
||||
verd = [t for t in erg.neue_tasks if t["knoten"] == "verdichtung"]
|
||||
assert verd and verd[0]["payload"]["soll_id"] == sid
|
||||
# nach Verdichtung: kein neuer Task mehr
|
||||
db.update("soll", "id=?", (sid,), verdichtet=1)
|
||||
erg2 = await inventar.gate({"run_id": run_id, "topic": topic, "runde": 2,
|
||||
"knoten": "gate_inventar",
|
||||
"item": "runde:2", "payload": "{}"})
|
||||
assert not [t for t in erg2.neue_tasks if t["knoten"] == "verdichtung"]
|
||||
|
||||
Reference in New Issue
Block a user