Files
creator2/tests/test_e2e.py
2026-07-12 20:02:13 +02:00

184 lines
8.9 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
assert "| E -->" not in md and "| M -->" not in md # Marker tragen kein Level mehr
# Kapitel-Struktur: H2 = Kapitel (Struktur-Ebene, mit Intro), H3 = Baustein
assert "\n## " in "\n" + md and "### " in md
assert "**Kernpunkte:**" not in md and "**Prüfe dich:**" not in md # Blöcke entfernt
# Typisiertes Beispiel: die Fake-Welt liefert Tabellen-Beispiele → verbatim im Guide,
# Marker durch die Assembly ersetzt (kein toter Beispiel-Marker übrig)
assert "| Fall | Ergebnis |" in md
assert "<!-- beispiel:" not 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
# Stufen-Bausteine: jeder Baustein trägt genau ein Level, alle seine Atome
# dieses Level; Kapitel-Level monoton E→M→S entlang der globalen ord
rang = {"E": 0, "M": 1, "S": 2}
kap_level = {k["id"]: k["level"] for k in kaps}
for b in bausteine:
assert kap_level[b["kapitel_id"]] == b["level"]
for a in atome:
if a["baustein_id"] == b["id"]:
assert a["level"] == b["level"], (a["titel"], a["level"], b["level"])
level_folge = [rang[b["level"]] for b in bausteine]
assert level_folge == sorted(level_folge), "Durchgänge nicht E<M<S geordnet"
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)
# Fake-Welt: 3 Atome mit Level E/M/S → je Level ein eigener Baustein-Durchgang
bausteine = db.query("SELECT * FROM bausteine WHERE topic=? ORDER BY ord", (topic,))
assert [b["level"] for b in bausteine] == ["E", "M", "S"]
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", "diagramme", "guide"} # diagramme hängt an struktur
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