update
This commit is contained in:
@@ -79,3 +79,104 @@ async def test_guard_verwirft_kandidat_und_repair_kann_nachlegen():
|
||||
# verworfene zählen nicht als versorgt: Repair generiert für das Atom nach
|
||||
chunks = artefakte._chunks(topic, nur_ohne=True)
|
||||
assert [x["id"] for c in chunks for x in c] == [a]
|
||||
|
||||
|
||||
def test_tabelle_struktur_check():
|
||||
# wohlgeformt
|
||||
ok = "| A | B |\n|---|---|\n| 1 | 2 |"
|
||||
assert artefakte._tabelle_ok(ok)
|
||||
# keine Datenzeile / kein Trenner / inkonsistente Spalten
|
||||
assert not artefakte._tabelle_ok("| A | B |\n|---|---|")
|
||||
assert not artefakte._tabelle_ok("A B\n1 2\n3 4")
|
||||
assert not artefakte._tabelle_ok("| A | B |\n|---|---|\n| 1 |")
|
||||
|
||||
|
||||
def test_referenziert_quelle_ignoriert_code():
|
||||
# „aufgabe 3" in Code/Kommentar darf NICHT als Quellen-Referenz matchen
|
||||
inhalt = {"form": "code", "text": "", "code": "# aufgabe 3: sum\nprint(sum([1,2]))",
|
||||
"sprache": "python", "tabelle": "", "mermaid": ""}
|
||||
assert not artefakte._referenziert_quelle(inhalt)
|
||||
# in der Prosa (text) matcht es weiterhin
|
||||
assert artefakte._referenziert_quelle({"text": "siehe aufgabe 3", "frage": "", "antwort": ""})
|
||||
|
||||
|
||||
def test_formen_gate_verwirft_kaputte_tabelle():
|
||||
topic = topic_anlegen("formgate")
|
||||
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||
status="neu", braucht=db.j([]))
|
||||
gut = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||
inhalt=db.j({"form": "tabelle", "tabelle": "| A | B |\n|---|---|\n| 1 | 2 |"}))
|
||||
schlecht = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||
inhalt=db.j({"form": "tabelle", "tabelle": "kaputt ohne pipes"}))
|
||||
artefakte._formen_gate(topic)
|
||||
assert db.one("SELECT status FROM artefakte WHERE id=?", (gut,))["status"] == "kandidat"
|
||||
assert db.one("SELECT status FROM artefakte WHERE id=?", (schlecht,))["status"] == "verworfen"
|
||||
|
||||
|
||||
def test_beispiel_einsetzen_tabelle():
|
||||
import guide
|
||||
topic = topic_anlegen("bsp-tab")
|
||||
b = db.insert("bausteine", topic=topic, ziel_id=1, titel="B", ord=0, status="neu")
|
||||
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||
status="neu", baustein_id=b, ord=0, braucht=db.j([]))
|
||||
md = "| A | B |\n|---|---|\n| 1 | 2 |"
|
||||
art = db.insert("artefakte", atom_id=a, typ="beispiel", status="verifiziert",
|
||||
inhalt=db.j({"form": "tabelle", "tabelle": md}))
|
||||
lang = f"Text.\n\n<!-- beispiel: {art} -->\n\nMehr."
|
||||
out = guide._beispiel_einsetzen(b, lang)
|
||||
assert md in out and "<!-- beispiel:" not in out
|
||||
# toter Marker verschwindet
|
||||
assert "<!-- beispiel:" not in guide._beispiel_einsetzen(b, "x\n<!-- beispiel: 999 -->\ny")
|
||||
|
||||
|
||||
def test_code_syntax_gate():
|
||||
assert artefakte._code_ok("python", "x = sum([1, 2])\nprint(x)")
|
||||
assert not artefakte._code_ok("python", "def f(: pass") # SyntaxError
|
||||
assert artefakte._code_ok("javascript", "const x = [1,2].map(v => v*2)")
|
||||
assert not artefakte._code_ok("javascript", "const x = ;;;(")
|
||||
assert not artefakte._code_ok("python", "") # leer
|
||||
assert artefakte._code_ok("ruby", "puts 1") # unbekannt → kein Gate
|
||||
|
||||
|
||||
def test_formen_gate_verwirft_kaputten_code():
|
||||
topic = topic_anlegen("codegate")
|
||||
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||
status="neu", braucht=db.j([]))
|
||||
gut = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||
inhalt=db.j({"form": "code", "sprache": "python", "code": "print(1+1)"}))
|
||||
schlecht = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||
inhalt=db.j({"form": "code", "sprache": "python", "code": "def f(: pass"}))
|
||||
artefakte._formen_gate(topic)
|
||||
assert db.one("SELECT status FROM artefakte WHERE id=?", (gut,))["status"] == "kandidat"
|
||||
assert db.one("SELECT status FROM artefakte WHERE id=?", (schlecht,))["status"] == "verworfen"
|
||||
|
||||
|
||||
def test_formen_gate_diagramm_beispiel():
|
||||
topic = topic_anlegen("diagbsp")
|
||||
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||
status="neu", braucht=db.j([]))
|
||||
spec = {"knoten": [{"id": "q0", "label": "Start"}, {"id": "q1", "label": "Ende"}],
|
||||
"kanten": [{"von": "q0", "zu": "q1", "label": "a"}]}
|
||||
d = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||
inhalt=db.j({"form": "diagramm", "spec": spec}))
|
||||
leer = db.insert("artefakte", atom_id=a, typ="beispiel", status="kandidat",
|
||||
inhalt=db.j({"form": "diagramm", "spec": {"knoten": []}}))
|
||||
artefakte._formen_gate(topic)
|
||||
d_row = db.one("SELECT status, inhalt FROM artefakte WHERE id=?", (d,))
|
||||
assert d_row["status"] == "kandidat"
|
||||
assert db.uj(d_row["inhalt"])["mermaid"].startswith("flowchart") # kompiliert
|
||||
assert db.one("SELECT status FROM artefakte WHERE id=?", (leer,))["status"] == "verworfen"
|
||||
|
||||
|
||||
async def test_guide_messen_toter_beispiel_marker():
|
||||
import guide
|
||||
topic = topic_anlegen("bspqa")
|
||||
b = db.insert("bausteine", topic=topic, ziel_id=1, titel="B", ord=0, status="neu")
|
||||
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||
status="neu", baustein_id=b, ord=0, braucht=db.j([]))
|
||||
db.insert("lernziele", topic=topic, id=1, text="Z", status="aktiv")
|
||||
db.insert("sections", baustein_id=b, stage="done", text_kompakt="- k",
|
||||
text_lang=f"<!-- atom: {a} | X -->\n<!-- beispiel: 777 -->\n" + "Wort " * 60)
|
||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||
ctx.ebene = "guide"
|
||||
assert "beispiel_marker_tot" in {x["art"] for x in guide.messen(ctx)}
|
||||
|
||||
@@ -333,22 +333,6 @@ def test_stil_kein_fehlalarm_deutsch():
|
||||
assert not any("englische Passage" in x for x in guide._stil_auftraege(text, "Langtext"))
|
||||
|
||||
|
||||
def test_pruefe_dich_norm_match():
|
||||
"""Frage-Matching ist Case/Whitespace-tolerant (Fix darf umformatieren)."""
|
||||
import db
|
||||
import guide
|
||||
from conftest import topic_anlegen
|
||||
topic = topic_anlegen("pruefe")
|
||||
ziel = db.insert("lernziele", topic=topic, text="Z", 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([]))
|
||||
db.insert("artefakte", atom_id=a, typ="flashcard", status="verifiziert",
|
||||
inhalt=db.j({"frage": "Was ist ein Graph?", "antwort": "Knoten und Kanten."}))
|
||||
fragen = guide._pruefe_dich(b_id, "Prüfe dich: Was ist ein Graph?")
|
||||
assert len(fragen) == 1 and fragen[0]["antwort"] == "Knoten und Kanten."
|
||||
|
||||
|
||||
def test_katex_gate_und_markdown_hygiene(monkeypatch):
|
||||
import guide
|
||||
monkeypatch.delenv("CREATOR_FAKE_AGENTS", raising=False) # Gate echt laufen lassen
|
||||
@@ -365,3 +349,42 @@ def test_katex_gate_und_markdown_hygiene(monkeypatch):
|
||||
assert "literales" in auftraege
|
||||
lang_formel = "Es gilt $" + "x + ".join(["y"] * 30) + "$ hier."
|
||||
assert "überlange Inline-Formel" in " ".join(guide._mathe_auftraege(lang_formel, "T"))
|
||||
|
||||
|
||||
def test_split_writer_delimited_und_fallback():
|
||||
import guide
|
||||
# delimited: roher LaTeX-Backslash bleibt (kein JSON-Escaping)
|
||||
k, l = guide._split_writer("===LANG===\nText $a \\leq b$\n===KOMPAKT===\n- p")
|
||||
assert l == "Text $a \\leq b$" and k == "- p"
|
||||
# umgekehrte Reihenfolge der Blöcke
|
||||
k2, l2 = guide._split_writer("===KOMPAKT===\n- x\n===LANG===\nY")
|
||||
assert l2 == "Y" and k2 == "- x"
|
||||
# Fallback: altes JSON
|
||||
k3, l3 = guide._split_writer('{"kompakt": "- a", "lang": "b"}')
|
||||
assert l3 == "b" and k3 == "- a"
|
||||
# Fallback: nackter Text → alles lang
|
||||
assert guide._split_writer("nur text")[1] == "nur text"
|
||||
|
||||
|
||||
async def test_fix_cap_friert_section_ein(monkeypatch):
|
||||
"""Nach FIX_MAX_VERSUCHE erfolglosen Fixes (Text unverändert) wird die Section
|
||||
für Stil-Befunde eingefroren → reparieren meldet bewegt=False (Stillstand)."""
|
||||
import db
|
||||
import fake_agents
|
||||
import guide
|
||||
import llm
|
||||
from conftest import run_anlegen, topic_anlegen
|
||||
topic = topic_anlegen("fixcap")
|
||||
run = run_anlegen(topic)
|
||||
ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv")
|
||||
b = 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, braucht=db.j([]))
|
||||
lang = f"<!-- atom: {a} | A -->\n" + "Wort " * 60
|
||||
db.insert("sections", baustein_id=b, stage="fix", text_lang=lang, text_kompakt="- p",
|
||||
befunde=db.j([]), fix_versuche=guide.FIX_MAX_VERSUCHE) # schon am Cap
|
||||
ctx = llm.Kontext(run, topic, "minimax")
|
||||
ctx.ebene = "guide"
|
||||
bewegt = await guide.reparieren(
|
||||
ctx, [{"art": "det_check", "item": str(b), "detail": "Kürzen"}])
|
||||
assert bewegt is False # eingefroren → nicht geroutet → kein Fortschritt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Ebene 3.5 Diagramme: deterministische DAG-Diagramme (kein LLM), Spec→Mermaid."""
|
||||
"""Ebene 3.5 Diagramme: LLM-Diagramme (Automat/Ablauf/Baum), Spec→Mermaid, Assembly."""
|
||||
|
||||
import db
|
||||
import diagramme
|
||||
@@ -21,7 +21,7 @@ def _baustein_mit_kette(topic, n=4):
|
||||
|
||||
|
||||
def test_spec_zu_mermaid_flowchart():
|
||||
spec = {"typ": "dependency",
|
||||
spec = {"typ": "flow",
|
||||
"knoten": [{"id": "1", "label": "A"}, {"id": "2", "label": 'B "x" [y]'}],
|
||||
"kanten": [{"von": "1", "zu": "2", "label": ""}]}
|
||||
out = diagramme._spec_zu_mermaid(spec)
|
||||
@@ -30,25 +30,11 @@ def test_spec_zu_mermaid_flowchart():
|
||||
assert '"' not in out.split("\n", 1)[1].replace('["', "").replace('"]', "") # Label entschärft
|
||||
|
||||
|
||||
async def test_dag_diagramm_wird_gebaut_und_verifiziert():
|
||||
topic = topic_anlegen("diag-dag")
|
||||
b, atome = _baustein_mit_kette(topic, n=4) # 3 Kanten ≥ DIAGRAMM_MIN_KANTEN
|
||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||
await diagramme.bauen(ctx)
|
||||
d = db.one("SELECT * FROM diagramme WHERE baustein_id=?", (b,))
|
||||
assert d and d["status"] == "verifiziert" and d["quelle"] == "dag"
|
||||
assert d["mermaid"].startswith("flowchart TD")
|
||||
assert all(f"n{a}" in d["mermaid"] for a in atome) # jeder Knoten drin
|
||||
assert diagramme.messen(ctx) == [] # keine offenen Kandidaten
|
||||
assert diagramme.gate(ctx) is None
|
||||
|
||||
|
||||
async def test_wenig_kanten_kein_diagramm():
|
||||
async def test_fake_judge_nein_keine_diagramme():
|
||||
topic = topic_anlegen("diag-leer")
|
||||
b, _ = _baustein_mit_kette(topic, n=2) # nur 1 Kante < Schwelle
|
||||
b, _ = _baustein_mit_kette(topic, n=2)
|
||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||
await diagramme.bauen(ctx)
|
||||
# kein GEBAUTES Diagramm (Fake-Judge sagt noetig=False → nur eine 'kein'-Merkzeile)
|
||||
await diagramme.bauen(ctx) # Fake-Judge sagt noetig=False → nur 'kein'-Merkzeile
|
||||
assert db.query("SELECT id FROM diagramme WHERE topic=? AND status='verifiziert'",
|
||||
(topic,)) == []
|
||||
assert db.one("SELECT status FROM diagramme WHERE baustein_id=?", (b,))["status"] == "kein"
|
||||
@@ -97,12 +83,12 @@ async def test_marker_wird_zu_fence():
|
||||
"""Assembly: platzierter <!-- diagramm: id --> → ```mermaid-Fence."""
|
||||
import guide
|
||||
topic = topic_anlegen("diag-place")
|
||||
b, _ = _baustein_mit_kette(topic, n=4)
|
||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||
await diagramme.bauen(ctx)
|
||||
d = db.one("SELECT id FROM diagramme WHERE baustein_id=?", (b,))
|
||||
b, _ = _baustein_mit_kette(topic, n=2)
|
||||
mermaid = "flowchart TD\n n1[\"A\"]\n n2[\"B\"]\n n1 --> n2"
|
||||
d = db.insert("diagramme", topic=topic, baustein_id=b, typ="flow", quelle="llm",
|
||||
spec=db.j({}), mermaid=mermaid, status="verifiziert")
|
||||
db.insert("sections", baustein_id=b, stage="done", text_kompakt="- k",
|
||||
text_lang=f"Text.\n\n<!-- diagramm: {d['id']} -->\n\nMehr Text.")
|
||||
text_lang=f"Text.\n\n<!-- diagramm: {d} -->\n\nMehr Text.")
|
||||
lang = guide.kapitel_struktur(topic)[0]["sections"][0]["lang"]
|
||||
assert "```mermaid" in lang and "flowchart TD" in lang
|
||||
assert "<!-- diagramm:" not in lang # Marker ersetzt
|
||||
@@ -128,3 +114,23 @@ async def test_guide_messen_meldet_toten_marker():
|
||||
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
|
||||
ctx.ebene = "guide"
|
||||
assert "diagramm_marker_tot" in {x["art"] for x in guide.messen(ctx)}
|
||||
|
||||
|
||||
async def test_marker_platzieren_deterministisch():
|
||||
"""Der Writer setzt keine Beispiel-Marker mehr — _marker_platzieren fügt sie
|
||||
hinter den Atom-Absatz ein (atom-genau)."""
|
||||
import guide
|
||||
topic = topic_anlegen("platz")
|
||||
b = db.insert("bausteine", topic=topic, ziel_id=1, titel="B", ord=0, status="neu")
|
||||
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
||||
status="neu", baustein_id=b, ord=0, braucht=db.j([]))
|
||||
art = db.insert("artefakte", atom_id=a, typ="beispiel", status="verifiziert",
|
||||
inhalt=db.j({"form": "tabelle", "tabelle": "| A | B |\n|---|---|\n| 1 | 2 |"}))
|
||||
lang = f"#### X\n<!-- atom: {a} | X -->\nErklärung von X.\n\nWeiterer Absatz zum Thema."
|
||||
out = guide._marker_platzieren(db.one("SELECT * FROM bausteine WHERE id=?", (b,)), lang)
|
||||
assert f"<!-- beispiel: {art} -->" in out
|
||||
# der Marker steht NACH dem Atom-Absatz
|
||||
assert out.index("<!-- beispiel:") < out.index("Weiterer Absatz")
|
||||
# idempotent: zweiter Lauf dupliziert nicht
|
||||
assert guide._marker_platzieren(db.one("SELECT * FROM bausteine WHERE id=?", (b,)),
|
||||
out).count("<!-- beispiel:") == 1
|
||||
|
||||
@@ -39,7 +39,11 @@ def _pruefe_endzustand(topic, run_id):
|
||||
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
|
||||
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:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Struktur-Ebene: Topo-Sortierung, Zyklenbruch, Band-Schnitt — mit synthetischen Atomen."""
|
||||
|
||||
import db
|
||||
import inventar
|
||||
import llm
|
||||
import struktur
|
||||
from conftest import run_anlegen, topic_anlegen
|
||||
@@ -224,6 +225,30 @@ def test_level_konflikt_absenkung():
|
||||
assert not [b for b in struktur.messen(ctx) if b["art"] == "level_konflikt"]
|
||||
|
||||
|
||||
async def test_braucht_fallback_kompositum():
|
||||
# braucht-Titel "Approximationsalgorithmus" (Kompositum) trifft weder norm noch
|
||||
# titel_kern das Atom "Approximativer Algorithmus" (Phrase). Der Fallback-Judge
|
||||
# legt die Kante; _level_konflikte_loesen senkt die Voraussetzung von M auf E.
|
||||
topic = topic_anlegen("brfall")
|
||||
run = run_anlegen(topic)
|
||||
ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv")
|
||||
a = db.insert("atome", topic=topic, titel="Absolute Güte", typ="begriff",
|
||||
definition="d", level="E", status="neu", ziel_id=ziel,
|
||||
braucht=db.j(["Approximationsalgorithmus"]))
|
||||
b = db.insert("atome", topic=topic, titel="Approximativer Algorithmus", typ="begriff",
|
||||
definition="d", level="M", status="neu", ziel_id=ziel, braucht=db.j([]))
|
||||
ctx = llm.Kontext(run, topic, "minimax")
|
||||
ctx.ebene = "struktur"
|
||||
inventar._kanten_aufloesen(topic) # deterministisch: verfehlt das Kompositum
|
||||
assert db.query("SELECT * FROM kanten WHERE topic=? AND von_atom=?", (topic, a)) == []
|
||||
await inventar._braucht_fallback(ctx)
|
||||
kante = db.one("SELECT * FROM kanten WHERE topic=? AND von_atom=? AND zu_atom=?",
|
||||
(topic, a, b))
|
||||
assert kante and kante["art"] == "braucht"
|
||||
struktur._level_konflikte_loesen(topic)
|
||||
assert db.one("SELECT level FROM atome WHERE id=?", (b,))["level"] == "E"
|
||||
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user