This commit is contained in:
team3
2026-07-12 04:20:07 +02:00
parent 14bcf1b8e9
commit a284e03225
29 changed files with 866 additions and 329 deletions

View File

@@ -178,7 +178,7 @@ def test_det_auftraege_blockquote_und_artefakt():
b_id = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu")
a = db.insert("atome", topic=topic, titel="A", typ="begriff", definition="d",
status="neu", baustein_id=b_id, braucht=db.j([]))
lang = (f"<!-- atom: {a} | A | E -->\n" + "Fließtext. " * 45
lang = (f"<!-- atom: {a} | A -->\n" + "Fließtext. " * 45
+ "\n> wörtliches Rohzitat aus der Quelle\nEs gilt u 6= v."
+ "\n\nDas Blank-Symbol $[. ist speziell und liegt in $[ \\in \\Gamma$.")
kompakt = "- Definition mit x_i und Laufzeit 2^(n/2) als Klartext."
@@ -189,6 +189,62 @@ def test_det_auftraege_blockquote_und_artefakt():
assert "Kompakt-Fassung" in text and "x_i" in text # Klartext-Formeln gefangen
def test_det_auftraege_neue_checks():
import db
import guide
from conftest import topic_anlegen
topic = topic_anlegen("detcheck2")
ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv")
b_id = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu")
a = db.insert("atome", topic=topic, titel="A", typ="begriff", definition="d",
status="neu", baustein_id=b_id, braucht=db.j([]))
wand = "Wort " * 130
lang = (f"<!-- atom: {a} | A -->\n<!-- atom: 99999 | Fremd -->\n"
"Der Beweis folgt aus Satz 6.25, dazu später mehr.\n\n"
+ wand + "\n\n#### Leere Überschrift\n\n#### Noch eine\nText dahinter.\n\n"
"The proof is left as an exercise for the reader and this paragraph"
" should be detected by the language check here.")
kompakt = f"<!-- atom: {a} | A -->\n- Punkt"
text = " ".join(guide._det_auftraege(topic, {"id": b_id, "ord": 0}, lang, kompakt))
assert "gehört nicht zu diesem Baustein" in text # marker_fremd
assert "Vertröstungs-Floskel" in text # „später mehr"
assert "Quellen-Referenz" in text # Satz 6.25
assert "Wörter (Regel: 4090)" in text # Absatz-Wand
assert "ohne folgenden Text" in text # leere Überschrift
assert "englische Passage" in text
assert "Marker gehören nur" in text # Marker in Kompakt-Fassung
# qa=True: Marker/Länge/Vorwärts haben eigene Befund-Arten, Stil-Checks bleiben
qa_text = " ".join(guide._det_auftraege(topic, {"id": b_id, "ord": 0},
lang, kompakt, qa=True))
assert "gehört nicht zu diesem Baustein" not in qa_text
assert "Vertröstungs-Floskel" in qa_text
async def test_fix_fehlschlag_behaelt_befunde(monkeypatch):
import db
import fake_agents
import guide
import llm
from conftest import run_anlegen, topic_anlegen
topic = topic_anlegen("fixfail")
run = run_anlegen(topic)
ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv")
b_id = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu")
db.insert("atome", topic=topic, titel="A", typ="begriff", definition="d",
status="neu", baustein_id=b_id, braucht=db.j([]))
db.insert("sections", baustein_id=b_id, stage="fix",
text_lang="alt", text_kompakt="", befunde=db.j(["Behebe (x): y"]))
# Fix liefert Text OHNE Marker → Marker-Invariante bricht → Fehlschlag-Pfad
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Fix",
lambda p: {"kompakt": "", "lang": "kein Marker"})
ctx = llm.Kontext(run, topic, "minimax")
ctx.ebene = "guide"
stage = await guide._stage_fix(ctx, db.one("SELECT * FROM bausteine WHERE id=?", (b_id,)))
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b_id,))
assert stage == "done" and sec["text_lang"] == "alt"
assert db.uj(sec["befunde"]) == ["Behebe (x): y"] # Aufträge bleiben sichtbar
def test_katex_gate_und_markdown_hygiene(monkeypatch):
import guide
monkeypatch.delenv("CREATOR_FAKE_AGENTS", raising=False) # Gate echt laufen lassen

View File

@@ -36,6 +36,7 @@ def _pruefe_endzustand(topic, run_id):
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:**" in md and "**Prüfe dich:**" in md
@@ -48,6 +49,17 @@ def _pruefe_endzustand(topic, run_id):
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,))
@@ -58,9 +70,9 @@ 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
# 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 len(bausteine) == 1
assert [b["level"] for b in bausteine] == ["E", "M", "S"]
async def test_e2e_uni(tmp_path):

View File

@@ -132,11 +132,10 @@ async def test_level_kalibrierung(monkeypatch):
topic = topic_anlegen("level")
run = run_anlegen(topic)
ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv")
b_id = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu")
a1 = db.insert("atome", topic=topic, titel="Kern", typ="begriff", definition="d",
level="M", status="neu", baustein_id=b_id, braucht=db.j([]))
level="M", status="neu", ziel_id=ziel, braucht=db.j([]))
a2 = db.insert("atome", topic=topic, titel="Detail", typ="aussage", definition="d",
level="M", status="neu", baustein_id=b_id, braucht=db.j([]))
level="M", status="neu", ziel_id=ziel, braucht=db.j([]))
def judge(prompt):
return [{"atom": a1, "level": "E"}, {"atom": a2, "level": "S"},
@@ -147,3 +146,67 @@ async def test_level_kalibrierung(monkeypatch):
await struktur._level_kalibrieren(ctx)
assert db.one("SELECT level FROM atome WHERE id=?", (a1,))["level"] == "E"
assert db.one("SELECT level FROM atome WHERE id=?", (a2,))["level"] == "S"
def test_level_konflikt_absenkung():
# v(E) braucht z(S), z braucht w(M) → Kaskade senkt z UND w auf E
topic = topic_anlegen("konflikt")
run = run_anlegen(topic)
ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv")
def atom(titel, level):
return db.insert("atome", topic=topic, titel=titel, typ="begriff",
definition="d", level=level, status="neu", ziel_id=ziel,
braucht=db.j([]))
v, z, w = atom("V", "E"), atom("Z", "S"), atom("W", "M")
for von, zu in ((v, z), (z, w)):
db.execute("INSERT INTO kanten(topic, von_atom, zu_atom, art)"
" VALUES(?,?,?,'braucht')", (topic, von, zu))
struktur._level_konflikte_loesen(topic)
assert db.one("SELECT level FROM atome WHERE id=?", (z,))["level"] == "E"
assert db.one("SELECT level FROM atome WHERE id=?", (w,))["level"] == "E"
ctx = llm.Kontext(run, topic, "minimax")
assert not [b for b in struktur.messen(ctx) if b["art"] == "level_konflikt"]
async def test_level_split_ordnung_kapitel():
# Ein Ziel mit 4×E + 4×M → zwei Bausteine mit level, E-ord < M-ord,
# Kapitel je Durchgang, kein level_mix/kapitel_level
topic = topic_anlegen("split")
run = run_anlegen(topic)
soll = db.insert("soll", topic=topic, punkt="P", status="bestaetigt", belege=db.j([]))
ziel = db.insert("lernziele", topic=topic, text="Kann P", soll_id=soll, status="aktiv")
for i in range(4):
db.insert("atome", topic=topic, titel=f"E{i}", typ="begriff", definition="d",
level="E", status="neu", soll_id=soll, ziel_id=ziel, braucht=db.j([]))
db.insert("atome", topic=topic, titel=f"M{i}", typ="begriff", definition="d",
level="M", status="neu", soll_id=soll, ziel_id=ziel, braucht=db.j([]))
ctx = llm.Kontext(run, topic, "minimax")
ctx.ebene = "struktur"
struktur._bausteine_schneiden(ctx)
bausteine = db.query("SELECT * FROM bausteine WHERE topic=? ORDER BY ord", (topic,))
assert [b["level"] for b in bausteine] == ["E", "M"]
await struktur._kapitel_bilden(ctx)
kaps = db.query("SELECT * FROM kapitel WHERE topic=? ORDER BY ord", (topic,))
assert [k["level"] for k in kaps] == ["E", "M"]
arten = {b["art"] for b in struktur.messen(ctx)}
assert not arten & {"level_mix", "kapitel_level", "band", "partition"}
def test_kein_merge_ueber_level():
# kleine E-Gruppe darf NICHT mit kleiner M-Gruppe mergen
topic = topic_anlegen("levelmerge")
run = run_anlegen(topic)
soll = db.insert("soll", topic=topic, punkt="P", status="bestaetigt", belege=db.j([]))
z1 = db.insert("lernziele", topic=topic, text="Kann P1", soll_id=soll, status="aktiv")
z2 = db.insert("lernziele", topic=topic, text="Kann P2", soll_id=soll, status="aktiv")
for i in range(2):
db.insert("atome", topic=topic, titel=f"E{i}", typ="begriff", definition="d",
level="E", status="neu", soll_id=soll, ziel_id=z1, braucht=db.j([]))
db.insert("atome", topic=topic, titel=f"M{i}", typ="begriff", definition="d",
level="M", status="neu", soll_id=soll, ziel_id=z2, braucht=db.j([]))
ctx = llm.Kontext(run, topic, "minimax")
struktur._bausteine_schneiden(ctx)
for b in db.query("SELECT * FROM bausteine WHERE topic=?", (topic,)):
levels = {a["level"] for a in struktur._atome(topic)
if a["baustein_id"] == b["id"]}
assert levels == {b["level"]}

View File

@@ -42,3 +42,10 @@ async def test_export_import_roundtrip():
for b in db.query("SELECT * FROM bausteine WHERE topic=?", (topic,)):
assert db.one("SELECT id FROM kapitel WHERE id=?", (b["kapitel_id"],))
assert db.one("SELECT baustein_id FROM sections WHERE baustein_id=?", (b["id"],))
# Marker-Remap: jede Marker-id im importierten Text existiert (aak: 421/421 tot)
atom_ids = {a["id"] for a in db.query("SELECT id FROM atome WHERE topic=?", (topic,))}
for s in db.query("SELECT s.* FROM sections s JOIN bausteine b ON b.id=s.baustein_id"
" WHERE b.topic=?", (topic,)):
for feld in ("text_lang", "text_kompakt"):
for m in re.findall(r"<!--\s*atom:\s*(\d+)\s*\|", s[feld] or ""):
assert int(m) in atom_ids, f"toter Marker {m}"