183 lines
9.6 KiB
Python
183 lines
9.6 KiB
Python
"""Ebene 2: Quellen-Referenz-Guard — Karten müssen ohne den Quelltext
|
|
funktionieren (Lauf 8: 17 verifizierte Karten fragten „was steht im Beleg")."""
|
|
|
|
import artefakte
|
|
import db
|
|
import llm
|
|
from conftest import run_anlegen, topic_anlegen
|
|
|
|
|
|
def test_referenz_muster_trifft_nur_referenzen():
|
|
schlecht = [
|
|
{"frage": "Welchen Namen trägt die Technik, die im Beleg erwähnt wird?",
|
|
"antwort": "k-Enumeration.", "text": ""},
|
|
{"frage": "Was ist MAX-3-SAT?",
|
|
"antwort": "Aus dem Beleg geht nur hervor, dass es in Hausaufgabe 13.1 vorkommt.",
|
|
"text": ""},
|
|
{"frage": "Welche Aufgabe wird in Aufgabe 1 gestellt?", "antwort": "x", "text": ""},
|
|
{"frage": "Wie groß ist |V'|?", "antwort": "Laut Musterlösung genau |V|.", "text": ""},
|
|
]
|
|
gut = [ # Fachwörter dürfen NICHT matchen (generisch bleiben)
|
|
{"frage": "Was ist eine aussagenlogische Belegung?",
|
|
"antwort": "Eine Zuordnung von Wahrheitswerten zu Variablen.", "text": ""},
|
|
{"frage": "Was verbindet ein Fluss mit Quelle und Senke?",
|
|
"antwort": "Er belegt jede Kante mit einem Wert unter der Kapazität.", "text": ""},
|
|
]
|
|
assert all(artefakte._referenziert_quelle(k) for k in schlecht)
|
|
assert not any(artefakte._referenziert_quelle(k) for k in gut)
|
|
|
|
|
|
async def test_nur_ohne_sieht_fehlende_flashcard():
|
|
"""Ein lebendes Beispiel darf die Flashcard-Nachgenerierung nicht blockieren."""
|
|
topic = topic_anlegen("fcfehlt")
|
|
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
|
status="neu", soll_id=1, braucht=db.j([]))
|
|
b = db.insert("atome", topic=topic, titel="Y", typ="begriff", definition="d",
|
|
status="neu", soll_id=1, braucht=db.j([]))
|
|
# a: nur ein Beispiel, keine Flashcard → muss nachgeneriert werden
|
|
db.insert("artefakte", atom_id=a, typ="beispiel", status="verifiziert",
|
|
inhalt=db.j({"text": "Beispiel"}))
|
|
# b: verifizierte Flashcard → versorgt, nicht mehr in nur_ohne
|
|
db.insert("artefakte", atom_id=b, typ="flashcard", status="verifiziert",
|
|
inhalt=db.j({"frage": "F", "antwort": "A"}))
|
|
ids = [x["id"] for c in artefakte._chunks(topic, nur_ohne=True) for x in c]
|
|
assert ids == [a]
|
|
|
|
|
|
async def test_reverify_ausfall_laesst_kandidat(monkeypatch):
|
|
"""Re-Verify-Ausfall (kein Urteil zur id) → Kandidat bleibt, nicht verworfen."""
|
|
import fake_agents
|
|
topic = topic_anlegen("reverify")
|
|
run = run_anlegen(topic)
|
|
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
|
status="neu", braucht=db.j([]))
|
|
k = db.insert("artefakte", atom_id=a, typ="flashcard", status="kandidat",
|
|
inhalt=db.j({"frage": "Was ist X?", "antwort": "Y", "text": ""}))
|
|
# Fix liefert saubere Karte; Re-Verify antwortet ohne Eintrag zur id → kein Urteil
|
|
monkeypatch.setitem(fake_agents._HANDLER, "Artefakt-Fix",
|
|
lambda p: {"frage": "Was ist X?", "antwort": "Y", "text": ""})
|
|
monkeypatch.setitem(fake_agents._HANDLER, "Artefakt-Verify", lambda p: [])
|
|
ctx = llm.Kontext(run, topic, "minimax")
|
|
ctx.ebene = "artefakte"
|
|
await artefakte._fixen(ctx, db.one("SELECT * FROM artefakte WHERE id=?", (k,)),
|
|
["mangel"], {"id": a})
|
|
assert db.one("SELECT status FROM artefakte WHERE id=?", (k,))["status"] == "kandidat"
|
|
|
|
|
|
async def test_guard_verwirft_kandidat_und_repair_kann_nachlegen():
|
|
topic = topic_anlegen("guard")
|
|
run = run_anlegen(topic)
|
|
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
|
|
status="neu", braucht=db.j([]))
|
|
k = db.insert("artefakte", atom_id=a, typ="flashcard", status="kandidat",
|
|
inhalt=db.j({"frage": "Was steht im Beleg?", "antwort": "x", "text": ""}))
|
|
ctx = llm.Kontext(run, topic, "minimax")
|
|
ctx.ebene = "artefakte"
|
|
atom = db.one("SELECT * FROM atome WHERE id=?", (a,))
|
|
await artefakte._verifizieren(ctx, [atom]) # Guard greift VOR dem Panel
|
|
assert db.one("SELECT status FROM artefakte WHERE id=?", (k,))["status"] == "verworfen"
|
|
# 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)}
|