195 lines
9.6 KiB
Python
195 lines
9.6 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
|
|
# Level je Baustein = dominantes Atom-Level; Themenreinheit: die Soll-Punkte
|
|
# aller Atome gehören zum Baustein-Thema; Kapitel-Level monoton E→M→S
|
|
rang = {"E": 0, "M": 1, "S": 2}
|
|
kap_level = {k["id"]: k["level"] for k in kaps}
|
|
soll_thema = {p["id"]: p["thema_id"] for p in
|
|
db.query("SELECT id, thema_id FROM soll WHERE topic=?", (topic,))}
|
|
from collections import Counter
|
|
for b in bausteine:
|
|
assert kap_level[b["kapitel_id"]] == b["level"]
|
|
eigene = [a for a in atome if a["baustein_id"] == b["id"]]
|
|
zaehler = Counter(a["level"] for a in eigene)
|
|
dominant = min(zaehler, key=lambda lv: (-zaehler[lv], rang.get(lv, 9)))
|
|
assert b["level"] == dominant, (b["titel"], b["level"], dict(zaehler))
|
|
for a in eigene:
|
|
assert soll_thema.get(a["soll_id"]) == b["thema_id"], (a["titel"], b["titel"])
|
|
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 E/M/S im selben Thema → EIN Baustein, Level = E
|
|
# (Gleichstand 1/1/1 → niedrigstes; kein Level-Split mehr)
|
|
bausteine = db.query("SELECT * FROM bausteine WHERE topic=? ORDER BY ord", (topic,))
|
|
assert [b["level"] for b in bausteine] == ["E"]
|
|
assert all(b["thema_id"] for b in bausteine)
|
|
|
|
|
|
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.query("SELECT * FROM themen WHERE topic=?", (topic,)) == []
|
|
assert all(p["thema_id"] is None for p in
|
|
db.query("SELECT thema_id FROM soll 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"} # guide 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", "themen", "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
|