Files
creator2/tests/test_artefakte.py
2026-07-13 16:11:00 +02:00

363 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)
def _voll_versorgen(atom_id: int) -> None:
"""2 Flashcards + 1 Beispiel — Atom ohne Generate-Bedarf."""
for i in range(artefakte.FC_ZIEL):
db.insert("artefakte", atom_id=atom_id, typ="flashcard", status="verifiziert",
inhalt=db.j({"frage": f"F{i}?", "antwort": "A"}))
db.insert("artefakte", atom_id=atom_id, typ="beispiel", status="verifiziert",
inhalt=db.j({"form": "text", "text": "B"}))
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)
b = db.insert("atome", topic=topic, titel="Y", typ="begriff", definition="d",
status="neu", soll_id=1)
# 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: voll versorgt (2 Karten + Beispiel) → kein Bedarf
_voll_versorgen(b)
ids = [x["id"] for c in artefakte._chunks(topic, nur_ohne=True) for x in c]
assert ids == [a]
async def test_nachfuellen_bei_einer_lebenden_flashcard():
"""1 überlebende Karte blockierte früher die zweite für immer (Ziel: 2)."""
topic = topic_anlegen("fcauffuell")
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
status="neu", soll_id=1)
db.insert("artefakte", atom_id=a, typ="flashcard", status="verifiziert",
inhalt=db.j({"frage": "F?", "antwort": "A"}))
db.insert("artefakte", atom_id=a, typ="beispiel", status="verifiziert",
inhalt=db.j({"form": "text", "text": "B"}))
assert artefakte._gen_bedarf(a)
run = run_anlegen(topic)
ctx = llm.Kontext(run, topic, "minimax")
ctx.ebene = "artefakte"
atom = db.one("SELECT * FROM atome WHERE id=?", (a,))
await artefakte._generieren(ctx, [atom]) # Fake liefert 2 Karten + Beispiel
fc = artefakte._lebende(a, "flashcard")
assert len(fc) == artefakte.FC_ZIEL # Guard nimmt nur die EINE fehlende
assert len(artefakte._lebende(a, "beispiel")) == 1 # kein zweites Beispiel
async def test_beispiel_refill_nach_verwurf():
"""Ein verworfenes Beispiel wurde nie ersetzt — jetzt Pflicht je Atom."""
topic = topic_anlegen("bsprefill")
a = db.insert("atome", topic=topic, titel="X", typ="verfahren", definition="d",
status="neu", soll_id=1)
for i in range(artefakte.FC_ZIEL):
db.insert("artefakte", atom_id=a, typ="flashcard", status="verifiziert",
inhalt=db.j({"frage": f"F{i}?", "antwort": "A"}))
db.insert("artefakte", atom_id=a, typ="beispiel", status="verworfen",
inhalt=db.j({"form": "text", "text": "kaputt"}))
assert artefakte._gen_bedarf(a)
run = run_anlegen(topic)
ctx = llm.Kontext(run, topic, "minimax")
ctx.ebene = "artefakte"
await artefakte._generieren(ctx, [db.one("SELECT * FROM atome WHERE id=?", (a,))])
assert len(artefakte._lebende(a, "beispiel")) == 1
assert len(artefakte._lebende(a, "flashcard")) == artefakte.FC_ZIEL
def test_cap_stoppt_nachfuellen():
"""Ab NACHFUELL_CAP × Ziel Verwürfen gibt das Atom auf (Anti-Churn)."""
from config import NACHFUELL_CAP
topic = topic_anlegen("cap")
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
status="neu", soll_id=1)
_voll_versorgen(a)
for _ in range(NACHFUELL_CAP * 1): # Beispiel-Ziel = 1
db.insert("artefakte", atom_id=a, typ="beispiel", status="verworfen",
inhalt=db.j({"form": "text", "text": "x"}))
# lebendes Beispiel löschen → fehlt, aber aufgegeben
db.execute("DELETE FROM artefakte WHERE atom_id=? AND typ='beispiel'"
" AND status='verifiziert'", (a,))
assert artefakte._aufgegeben(a, "beispiel")
assert not artefakte._gen_bedarf(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")
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")
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 _paar_einfuegen(atom_id: int, i: int, status_wahr: str, status_falsch: str) -> tuple[int, int]:
paar = f"{atom_id}-{i}"
w = db.insert("artefakte", atom_id=atom_id, typ="aussage", status=status_wahr,
inhalt=db.j({"text": f"wahr {i}", "wahr": True, "paar": paar,
"erklaerung": "e"}))
f = db.insert("artefakte", atom_id=atom_id, typ="aussage", status=status_falsch,
inhalt=db.j({"text": f"falsch {i}", "wahr": False, "paar": paar,
"erklaerung": "e"}))
return w, f
def test_paare_bereinigen_verwirft_restseite():
topic = topic_anlegen("paarrest")
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
status="neu", soll_id=1)
w1, f1 = _paar_einfuegen(a, 0, "verifiziert", "verworfen") # einseitig → fällt
w2, f2 = _paar_einfuegen(a, 1, "verifiziert", "verifiziert") # komplett → bleibt
artefakte._paare_bereinigen([a])
assert db.one("SELECT status FROM artefakte WHERE id=?", (w1,))["status"] == "verworfen"
assert db.one("SELECT status FROM artefakte WHERE id=?", (w2,))["status"] == "verifiziert"
assert db.one("SELECT status FROM artefakte WHERE id=?", (f2,))["status"] == "verifiziert"
def test_paare_bereinigen_verwirft_deformierte_gruppe():
"""Gruppen ohne {wahr,falsch}-Seiten fallen KOMPLETT — auch 2 gleiche Seiten
ohne paar-Key (Altlast des fehlenden Typ-Filters in _verifizieren)."""
topic = topic_anlegen("paardeform")
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
status="neu", soll_id=1)
k1 = db.insert("artefakte", atom_id=a, typ="aussage", status="verifiziert",
inhalt=db.j({"text": "f1", "wahr": False})) # kein paar-Key
k2 = db.insert("artefakte", atom_id=a, typ="aussage", status="verifiziert",
inhalt=db.j({"text": "f2", "wahr": False}))
artefakte._paare_bereinigen([a])
for k in (k1, k2):
assert db.one("SELECT status FROM artefakte WHERE id=?", (k,))["status"] == "verworfen"
async def test_verifizieren_ignoriert_aussage_kandidaten():
"""Aussage-Kandidaten gehören NUR ins Aussagen-Panel — das Flashcard-Verify
überschrieb ihren Inhalt sonst mit dem frage/antwort-Schema (leere Hülle)."""
topic = topic_anlegen("typfilter")
run = run_anlegen(topic)
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
status="neu", soll_id=1)
k = db.insert("artefakte", atom_id=a, typ="aussage", status="kandidat",
inhalt=db.j({"text": "t", "wahr": True, "paar": f"{a}-0",
"erklaerung": "t"}))
ctx = llm.Kontext(run, topic, "minimax")
ctx.ebene = "artefakte"
atom = db.one("SELECT * FROM atome WHERE id=?", (a,))
await artefakte._verifizieren(ctx, [atom])
zeile = db.one("SELECT * FROM artefakte WHERE id=?", (k,))
assert zeile["status"] == "kandidat" # unangetastet
assert db.uj(zeile["inhalt"])["paar"] == f"{a}-0" # Inhalt intakt
async def test_aussagen_verify_verwirft_paarweise(monkeypatch):
"""Kippt eine Seite im Panel, fällt auch die frisch bestätigte Partnerseite."""
import fake_agents
topic = topic_anlegen("paarvote")
run = run_anlegen(topic)
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
status="neu", soll_id=1)
w, f = _paar_einfuegen(a, 0, "kandidat", "kandidat")
def verify(prompt):
import re
ids = [int(m) for m in re.findall(r"AUSSAGE (\d+) \[", prompt)]
return [{"aussage": i, "ok": i != f} for i in ids] # falsche Seite fällt
monkeypatch.setitem(fake_agents._HANDLER, "Aussagen-Verify", verify)
ctx = llm.Kontext(run, topic, "minimax")
ctx.ebene = "artefakte"
atom = db.one("SELECT * FROM atome WHERE id=?", (a,))
await artefakte._aussagen_verifizieren(ctx, [atom])
assert db.one("SELECT status FROM artefakte WHERE id=?", (w,))["status"] == "verworfen"
assert db.one("SELECT status FROM artefakte WHERE id=?", (f,))["status"] == "verworfen"
async def test_aussagen_nachfuellen_ohne_paar_kollision():
"""Neue Paare setzen den Index HINTER allen bestehenden Zeilen fort."""
from config import AUSSAGEN_JE_ATOM
topic = topic_anlegen("paaridx")
run = run_anlegen(topic)
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
status="neu", soll_id=1)
_paar_einfuegen(a, 0, "verworfen", "verworfen") # alter Index 0 bleibt belegt
_paar_einfuegen(a, 1, "verifiziert", "verifiziert")
ctx = llm.Kontext(run, topic, "minimax")
ctx.ebene = "artefakte"
atom = db.one("SELECT * FROM atome WHERE id=?", (a,))
await artefakte._aussagen_generieren(ctx, [atom]) # Bedarf: 1 Paar
lebend = artefakte._lebende(a, "aussage")
paare = {db.uj(k["inhalt"])["paar"] for k in lebend}
assert f"{a}-0" not in paare # kein Recycling des alten Keys
komplett, einseitig, _ = artefakte._paar_lage(a)
assert (komplett, einseitig) == (AUSSAGEN_JE_ATOM, 0)
def test_messen_meldet_vollstaendigkeit_und_cap():
from config import NACHFUELL_CAP
topic = topic_anlegen("messneu")
run = run_anlegen(topic)
a = db.insert("atome", topic=topic, titel="X", typ="begriff", definition="d",
status="neu", soll_id=1)
db.insert("artefakte", atom_id=a, typ="flashcard", status="verifiziert",
inhalt=db.j({"frage": "F?", "antwort": "A"})) # 1 von 2
_paar_einfuegen(a, 0, "verifiziert", "verworfen") # einseitig
ctx = llm.Kontext(run, topic, "minimax")
ctx.ebene = "artefakte"
arten = {b["art"] for b in artefakte.messen(ctx)}
assert {"flashcard_unvollstaendig", "atom_ohne_beispiel",
"paar_unvollstaendig"} <= arten
# Cap: genug Beispiel-Verwürfe → aufgegeben statt fehlt
for _ in range(NACHFUELL_CAP):
db.insert("artefakte", atom_id=a, typ="beispiel", status="verworfen",
inhalt=db.j({"form": "text", "text": "x"}))
arten = {b["art"] for b in artefakte.messen(ctx)}
assert "artefakt_aufgegeben" in arten and "atom_ohne_beispiel" not in arten
async def test_reparieren_false_bei_nur_aufgegeben():
topic = topic_anlegen("repfalse")
run = run_anlegen(topic)
ctx = llm.Kontext(run, topic, "minimax")
befunde = [{"art": "artefakt_aufgegeben", "item": "1-beispiel", "detail": "X"},
{"art": "artefakt_unentschieden", "item": "2", "detail": "Y"}]
assert await artefakte.reparieren(ctx, befunde) is False
befunde.append({"art": "atom_ohne_beispiel", "item": "1", "detail": "X"})
assert await artefakte.reparieren(ctx, befunde) is True
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": ""}
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")
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)
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")
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"
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)
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)}