This commit is contained in:
team3
2026-07-10 15:43:11 +02:00
commit 0a41166cca
72 changed files with 7772 additions and 0 deletions

187
tests/test_bausteine.py Normal file
View File

@@ -0,0 +1,187 @@
"""Deterministische Bausteine: textkit, jsonx, auto_loop, QA-Note."""
import auto_loop
import jsonx
import qa
import textkit
def test_finde_zitat_whitespace_tolerant():
text = "Der Automat\nbesteht aus Zuständen."
span = textkit.finde_zitat(text, "Der Automat besteht aus Zuständen.")
assert span is not None
start, ende = span
assert text[start:ende].split() == ["Der", "Automat", "besteht", "aus", "Zuständen."]
def test_finde_zitat_case_fallback_und_fehlschlag():
assert textkit.finde_zitat("DER AUTOMAT LÄUFT", "der Automat läuft") is not None
assert textkit.finde_zitat("völlig anderer Text", "der Automat läuft") is None
def test_finde_zitat_locker_interpunktion():
# PDF-Extrakte variieren Interpunktion/Dashes/Quotes — Wortlaut gleich → Treffer
text = "Es gilt: „Ein Automat mit Zuständen akzeptiert reguläre Sprachen."
zitat = 'Ein Automat, mit Zuständen, akzeptiert reguläre Sprachen'
span = textkit.finde_zitat(text, zitat)
assert span is not None
assert "Automat" in text[span[0]:span[1]]
# Paraphrase (andere Wörter) bleibt draußen
assert textkit.finde_zitat(text, "Ein DFA mit Zuständen akzeptiert alle Sprachen") is None
# zu kurze Zitate matchen locker nicht (Eindeutigkeit)
assert textkit.finde_zitat("abc def", "ab, c") is None
def test_finde_zitat_dekomponierte_umlaute():
"""Lektion 82: pdftotext liefert dekomponierte Umlaute (a+U+0308), das LLM-Zitat
präkomponierte (ä) — die Locker-Stufe faltet beide auf den Basisbuchstaben.
Vorher scheiterte fast jedes deutsche Zitat (aak: 59/96 Atome ohne Anker)."""
import unicodedata
text = unicodedata.normalize("NFD", "Satz 6.25. 3-SAT ist NP-vollständig. Beweis folgt später.")
span = textkit.finde_zitat(text, "Satz 6.25. 3-SAT ist NP-vollständig.")
assert span is not None
assert text[span[0]:span[1]].startswith("Satz 6.25")
# umgekehrt: präkomponierter Text, dekomponiertes Zitat
assert textkit.finde_zitat(
"Die Erfüllbarkeit boolescher Ausdrücke ist zentral.",
unicodedata.normalize("NFD", "Erfüllbarkeit boolescher Ausdrücke ist zentral")) is not None
def test_snapshot_schreiben_normalisiert_nfc():
"""Korpus-Snapshots werden am Import NFC-normalisiert — die exakte Zitat-Stufe
darf nicht an unsichtbar anderen Bytes scheitern."""
import unicodedata
import korpus
roh = unicodedata.normalize("NFD", "NP-vollständig und erfüllbar")
assert not unicodedata.is_normalized("NFC", roh)
pfad, _h = korpus._snapshot_schreiben("nfc-test", roh)
inhalt = open(pfad, encoding="utf-8").read()
assert unicodedata.is_normalized("NFC", inhalt)
assert inhalt == unicodedata.normalize("NFC", roh)
def test_snapshot_schreiben_ersetzt_kontrollzeichen():
"""PDF-Schriften mappen Sonderglyphen (ε) auf Steuerbytes — der Reader echot sie
als Müll und das Zitat wird unmatchbar. Import ersetzt sie durch Leerzeichen;
\\n und \\t bleiben (Layout)."""
import korpus
pfad, _h = korpus._snapshot_schreiben("ctrl-test", "Algorithmus (A\x0f )\nZeile\tzwei\x07!")
inhalt = open(pfad, encoding="utf-8").read()
assert inhalt == "Algorithmus (A )\nZeile\tzwei !"
def test_finde_zitat_fuzzy_listing_zeilennummern():
"""Stufe 4 (Lektion 83): pdftotext streut Listing-Zeilennummern in den Text,
das Reader-Zitat lässt sie weg — fuzzy mit harter Distanzschranke matcht trotzdem."""
text = ("Algorithmus ListScheduling(L=(J1 , . . . , Jn ),m)\n 1 for i=1 to m do\n"
" 2 Ei = 0; Bi = ∅;\n 3 od\n 4 for j=1 to n do\n"
" 5 wähle i mit Ei minimal;\n 6 od\n")
zitat = ("Algorithmus ListScheduling(L=(J1 , . . . , Jn ),m) for i=1 to m do Ei = 0; "
"Bi = ∅; od for j=1 to n do wähle i mit Ei minimal; od")
span = textkit.finde_zitat(text, zitat)
assert span is not None
assert text[span[0]:span[1]].startswith("Algorithmus ListScheduling")
def test_finde_zitat_fuzzy_rekonstruierte_glyphe():
"""PDF verlor die ε-Glyphe („(A )"), der Reader zitiert „(Aε)" — 1 Zeichen
Differenz auf langem Zitat → Treffer."""
text = ("Man nennt eine solche Familie von Algorithmen (A ) ein vollständiges "
"polynomielles Approximationsschema (FPTAS).")
zitat = ("Man nennt eine solche Familie von Algorithmen (Aε) ein vollständiges "
"polynomielles Approximationsschema (FPTAS).")
assert textkit.finde_zitat(text, zitat) is not None
def test_finde_zitat_fuzzy_mehrdeutig_bleibt_draussen():
"""Eindeutigkeits-Guard: zwei fast identische Definitionen (SubSetSum/Partition-
Muster) → kein Anker statt Falsch-Anker."""
a = "Gegeben: n ganze Zahlen c1 , . . . , cn und eine Zahl K. Entscheide: Gibt es eine Teilmenge S?"
b = "Gegeben: n ganze Zahlen c1 , . . . , cn und eine Zahl X. Entscheide: Gibt es eine Teilmenge T?"
zitat = "Gegeben: n ganze Zahlen c1 , . . . , cn und eine Zahl Q. Entscheide: Gibt es eine Teilmenge U?"
assert textkit.finde_zitat(a + "\n\nDazwischen steht anderer Text.\n\n" + b, zitat) is None
def test_finde_zitat_fuzzy_grenzen():
"""Paraphrase über der Distanzschranke bleibt draußen; Kurz-Zitate nie fuzzy."""
text = "Der List-Scheduling-Algorithmus verteilt Jobs der Reihe nach auf die am wenigsten belastete Maschine."
assert textkit.finde_zitat(text, "List Scheduling weist jeden Job der aktuell "
"günstigsten Maschine in Reihenfolge zu und stoppt") is None
assert textkit.finde_zitat("kurzer Text über ähm Dinge", "kurzer Test über ähm Dinge") is None
def test_snapshot_schreiben_ftfy_mojibake():
"""ftfy am Import: Mojibake und Ligaturen werden repariert (Lektion 83)."""
import importlib.util
import pytest
if importlib.util.find_spec("ftfy") is None:
pytest.skip("ftfy nicht installiert — NFC-Fallback deckt test_snapshot_schreiben_normalisiert_nfc")
import korpus
pfad, _h = korpus._snapshot_schreiben("ftfy-test", "effiziente NP-vollständige Suffixe")
inhalt = open(pfad, encoding="utf-8").read()
assert "vollständige" in inhalt # Mojibake ä → ä
assert "Suffixe" in inhalt # Ligatur ffi → ffi
def test_abschnitte_und_ueberlappung():
text = ("Absatz eins.\n\n" * 50) + ("Absatz zwei.\n\n" * 50)
teile = textkit.abschnitte(text, max_chars=300)
assert all(len(t) <= 300 for _, t in teile)
assert "".join(t for _, t in teile) == text
assert textkit.ueberlappung((0, 10), (5, 15)) == 0.5
assert textkit.ueberlappung((0, 10), (20, 30)) == 0.0
def test_negations_guard():
assert textkit.negations_menge("L ist nicht regulär") != textkit.negations_menge("L ist regulär")
def test_jsonx_fences_und_prosa():
assert jsonx.parse('```json\n{"a": 1}\n```') == {"a": 1}
assert jsonx.parse('Hier das Ergebnis: [{"b": "x — y"}] Danke!') == [{"b": "x — y"}]
assert jsonx.parse("kein json") is None
assert jsonx.parse('{"s": "mit \\"quote\\" und }"}') == {"s": 'mit "quote" und }'}
async def test_auto_loop_bedingungen():
note, grund = await auto_loop.auto_repair_loop("t", 10.0, None)
assert grund == "fertig"
async def stillstand():
return 8.0, False
note, grund = await auto_loop.auto_repair_loop("t", 8.0, stillstand)
assert grund == "stillstand"
zaehler = {"n": 0}
async def livelock():
zaehler["n"] += 1
return 8.0, True # bewegt, aber nie besser → hartes Limit greift
note, grund = await auto_loop.auto_repair_loop("t", 7.0, livelock, max_iter=4)
assert grund == "limit" and zaehler["n"] == 4
def test_qa_note_formel():
assert qa.note([], 10) == 10.0
n = qa.note([{"art": "marker_fehlend", "item": "1", "detail": ""}], 10)
assert n < 10.0
viele = [{"art": "atom_ohne_anker", "item": str(i), "detail": ""} for i in range(50)]
assert 0.0 <= qa.note(viele, 10) <= 9.9
def test_det_auftraege_blockquote_und_artefakt():
import db
import guide
from conftest import topic_anlegen
topic = topic_anlegen("detcheck")
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([]))
lang = (f"<!-- atom: {a} | A | E -->\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$.")
auftraege = guide._det_auftraege(topic, {"id": b_id, "ord": 0}, lang)
text = " ".join(auftraege)
assert "Blockquote" in text and "6=" in text
assert "ungerade Anzahl $-Zeichen" in text