168 lines
7.8 KiB
Python
168 lines
7.8 KiB
Python
"""Fake-E2E: kompletter Generierungspfad in Sekunden. Thema (Web-Recherche) und
|
|
Uni (Dateien), plus Resume-Idempotenz."""
|
|
|
|
import asyncio
|
|
import itertools
|
|
|
|
import db
|
|
import guide
|
|
import korpus
|
|
import pipeline
|
|
from conftest import topic_anlegen
|
|
from fake_agents import FAKE_TEXT
|
|
|
|
|
|
async def _lauf_komplett(topic):
|
|
run_id = pipeline.lauf_starten(topic)
|
|
await pipeline._laeufe[topic]
|
|
return run_id
|
|
|
|
|
|
def _pruefe_endzustand(topic, run_id):
|
|
assert db.one("SELECT status FROM topics WHERE name=?", (topic,))["status"] == "fertig"
|
|
run = db.one("SELECT * FROM runs WHERE id=?", (run_id,))
|
|
assert run["status"] == "done"
|
|
offen = db.query("SELECT * FROM befunde WHERE run_id=? AND status='offen'", (run_id,))
|
|
assert offen == [], f"offene Befunde: {offen}"
|
|
atome = db.query("SELECT * FROM atome WHERE topic=? AND status NOT IN"
|
|
" ('gemerged','verworfen')", (topic,))
|
|
assert len(atome) == 3 # die Fake-Welt hat exakt drei Atome
|
|
assert all(a["soll_id"] and a["ziel_id"] and a["baustein_id"] for a in atome)
|
|
for a in atome:
|
|
assert db.one("SELECT id FROM anker WHERE atom_id=?", (a["id"],))
|
|
karten = db.query("SELECT * FROM artefakte WHERE atom_id=? AND typ='flashcard'"
|
|
" AND status='verifiziert'", (a["id"],))
|
|
assert karten
|
|
md = guide.guide_markdown(topic)
|
|
for a in atome:
|
|
assert f"<!-- atom: {a['id']} |" in md
|
|
# Kapitel-Struktur: H2 = Kapitel (Struktur-Ebene, mit Intro), H3 = Baustein
|
|
assert "\n## " in "\n" + md and "### " in md
|
|
assert "**Kernpunkte:**" in md and "**Prüfe dich:**" in md
|
|
kaps = db.query("SELECT * FROM kapitel WHERE topic=? ORDER BY ord", (topic,))
|
|
assert kaps, "keine Kapitel gebildet"
|
|
for k in kaps:
|
|
assert k["intro"], f"Kapitel-Intro fehlt: {k['titel']}"
|
|
bausteine = db.query("SELECT * FROM bausteine WHERE topic=? ORDER BY ord", (topic,))
|
|
assert all(b["kapitel_id"] for b in bausteine)
|
|
folge = [b["kapitel_id"] for b in bausteine]
|
|
segmente = [k for k, _ in itertools.groupby(folge)]
|
|
assert len(segmente) == len(set(segmente)) == len(kaps) # kontiguierlich, lückenlos
|
|
for b in db.query("SELECT titel FROM bausteine WHERE topic=?", (topic,)):
|
|
assert not b["titel"].startswith("Kann"), b["titel"] # Kurztitel, kein Ziel-Satz
|
|
events = db.query("SELECT * FROM events WHERE run_id=?", (run_id,))
|
|
assert events and all(e["template_hash"] for e in events if e["template"])
|
|
|
|
|
|
async def test_e2e_thema():
|
|
topic = topic_anlegen("fake-thema", art="thema")
|
|
run_id = await _lauf_komplett(topic)
|
|
_pruefe_endzustand(topic, run_id)
|
|
# zwei kleine Ziele → Merge über Soll-Grenzen (Entkopplung): EIN Baustein
|
|
bausteine = db.query("SELECT * FROM bausteine WHERE topic=? ORDER BY ord", (topic,))
|
|
assert len(bausteine) == 1
|
|
|
|
|
|
async def test_e2e_uni(tmp_path):
|
|
ordner = korpus.TOPICS_DIR / "fake-uni"
|
|
ordner.mkdir(parents=True)
|
|
(ordner / "skript.txt").write_text(FAKE_TEXT, encoding="utf-8")
|
|
(ordner / "uebung.txt").write_text(FAKE_TEXT + "\nSerie 1.\n", encoding="utf-8")
|
|
topic = topic_anlegen("fake-uni", art="uni")
|
|
run_id = await _lauf_komplett(topic)
|
|
_pruefe_endzustand(topic, run_id)
|
|
quellen = db.query("SELECT * FROM quellen WHERE topic=?", (topic,))
|
|
assert len(quellen) == 2 and all(q["art"] == "datei" for q in quellen)
|
|
rollen = {q["titel"]: q["rolle"] for q in quellen}
|
|
assert rollen["skript.txt"] == "stoff" and rollen["uebung.txt"] == "aufgaben"
|
|
|
|
|
|
async def test_voll_reset_extrahiert_neu():
|
|
topic = topic_anlegen("fake-vollreset", art="thema")
|
|
run1 = await _lauf_komplett(topic)
|
|
_pruefe_endzustand(topic, run1)
|
|
pipeline.voll_reset(topic)
|
|
assert db.query("SELECT * FROM atome WHERE topic=?", (topic,)) == []
|
|
assert db.query("SELECT * FROM artefakte WHERE atom_id IN"
|
|
" (SELECT id FROM atome WHERE topic=?)", (topic,)) == []
|
|
run2 = await _lauf_komplett(topic)
|
|
_pruefe_endzustand(topic, run2) # kompletter Neuaufbau inkl. neuer Atome
|
|
|
|
|
|
async def test_soll_reset_kein_doppel():
|
|
topic = topic_anlegen("fake-reset", art="thema")
|
|
run1 = await _lauf_komplett(topic)
|
|
_pruefe_endzustand(topic, run1)
|
|
atome_vorher = {a["id"] for a in db.query(
|
|
"SELECT id FROM atome WHERE topic=? AND status NOT IN ('gemerged','verworfen')", (topic,))}
|
|
|
|
pipeline.soll_reset(topic)
|
|
assert db.query("SELECT * FROM soll WHERE topic=?", (topic,)) == []
|
|
run2 = await _lauf_komplett(topic)
|
|
_pruefe_endzustand(topic, run2)
|
|
atome_nachher = {a["id"] for a in db.query(
|
|
"SELECT id FROM atome WHERE topic=? AND status NOT IN ('gemerged','verworfen')", (topic,))}
|
|
assert atome_nachher == atome_vorher # Extraktions-Guard: keine Doppel-Atome
|
|
|
|
|
|
async def test_auto_stopp_und_fortsetzen():
|
|
topic = topic_anlegen("fake-auto", art="thema")
|
|
db.update("topics", "name", topic, auto=db.j({"inventar": False}))
|
|
run1 = await _lauf_komplett(topic)
|
|
assert db.one("SELECT status FROM topics WHERE name=?", (topic,))["status"] == "inventar_fertig"
|
|
r = db.one("SELECT * FROM runs WHERE id=?", (run1,))
|
|
assert r["status"] == "done" and "Auto-Stopp" in r["grund"]
|
|
db.update("topics", "name", topic, auto=db.j({})) # Auto wieder an → durchlaufen
|
|
run2 = await _lauf_komplett(topic)
|
|
_pruefe_endzustand(topic, run2)
|
|
|
|
|
|
async def test_ebenen_entfernen_kaskade():
|
|
topic = topic_anlegen("fake-entfernen", art="thema")
|
|
run1 = await _lauf_komplett(topic)
|
|
_pruefe_endzustand(topic, run1)
|
|
atome_vorher = {a["id"] for a in db.query(
|
|
"SELECT id FROM atome WHERE topic=? AND status NOT IN ('gemerged','verworfen')", (topic,))}
|
|
pipeline.ebenen_entfernen(topic, "struktur")
|
|
assert db.query("SELECT * FROM bausteine WHERE topic=?", (topic,)) == []
|
|
assert db.query("SELECT * FROM kapitel WHERE topic=?", (topic,)) == []
|
|
assert db.query("SELECT * FROM lernziele WHERE topic=?", (topic,)) == []
|
|
assert db.one("SELECT status FROM topics WHERE name=?", (topic,))["status"] == "artefakte_fertig"
|
|
# Anzeige-Reset: Token-Zählung der entfernten Ebenen beginnt neu (Events bleiben)
|
|
resets = db.uj(db.one("SELECT resets FROM topics WHERE name=?", (topic,))["resets"], {})
|
|
assert set(resets) == {"struktur", "guide"}
|
|
assert resets["struktur"] == db.one("SELECT MAX(id) AS m FROM events")["m"]
|
|
run2 = await _lauf_komplett(topic)
|
|
_pruefe_endzustand(topic, run2)
|
|
atome_nachher = {a["id"] for a in db.query(
|
|
"SELECT id FROM atome WHERE topic=? AND status NOT IN ('gemerged','verworfen')", (topic,))}
|
|
assert atome_nachher == atome_vorher # Atome blieben erhalten
|
|
|
|
|
|
async def test_topic_loeschen():
|
|
topic = topic_anlegen("fake-weg", art="thema")
|
|
run1 = await _lauf_komplett(topic)
|
|
_pruefe_endzustand(topic, run1)
|
|
pipeline.topic_loeschen(topic)
|
|
for tabelle in ("topics", "quellen", "soll", "atome", "lernziele", "bausteine",
|
|
"kapitel", "runs"):
|
|
feld = "name" if tabelle == "topics" else "topic"
|
|
assert db.query(f"SELECT * FROM {tabelle} WHERE {feld}=?", (topic,)) == [], tabelle
|
|
|
|
|
|
async def test_resume_idempotent():
|
|
topic = topic_anlegen("fake-resume", art="thema")
|
|
run1 = await _lauf_komplett(topic)
|
|
_pruefe_endzustand(topic, run1)
|
|
atome_vorher = {a["id"] for a in db.query(
|
|
"SELECT id FROM atome WHERE topic=? AND status NOT IN ('gemerged','verworfen')", (topic,))}
|
|
calls_vorher = len(db.query("SELECT id FROM events", ()))
|
|
|
|
run2 = pipeline.lauf_starten(topic) # zweiter Lauf auf fertigem Topic
|
|
await pipeline._laeufe[topic]
|
|
assert db.one("SELECT status FROM runs WHERE id=?", (run2,))["status"] == "done"
|
|
atome_nachher = {a["id"] for a in db.query(
|
|
"SELECT id FROM atome WHERE topic=? AND status NOT IN ('gemerged','verworfen')", (topic,))}
|
|
assert atome_nachher == atome_vorher # nichts dupliziert
|
|
assert len(db.query("SELECT id FROM events", ())) == calls_vorher # keine neuen LLM-Calls
|