208 lines
9.9 KiB
Python
208 lines
9.9 KiB
Python
"""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$.")
|
||
kompakt = "- Definition mit x_i und Laufzeit 2^(n/2) als Klartext."
|
||
auftraege = guide._det_auftraege(topic, {"id": b_id, "ord": 0}, lang, kompakt)
|
||
text = " ".join(auftraege)
|
||
assert "Blockquote" in text and "6=" in text
|
||
assert "ungerade Anzahl $-Zeichen" in text
|
||
assert "Kompakt-Fassung" in text and "x_i" in text # Klartext-Formeln gefangen
|
||
|
||
|
||
def test_katex_gate_und_markdown_hygiene(monkeypatch):
|
||
import guide
|
||
monkeypatch.delenv("CREATOR_FAKE_AGENTS", raising=False) # Gate echt laufen lassen
|
||
# Parse-Gate: echte KaTeX-Prüfung (nutzt node + frontend/node_modules)
|
||
fehler = guide._katex_fehler([{"tex": "\\undefinedmacro{x}", "display": False},
|
||
{"tex": "x^2", "display": False},
|
||
{"tex": "\\textsc{Subset Sum}", "display": False}])
|
||
assert len(fehler) == 1 and "undefinedmacro" in fehler[0] # \textsc ist per Makro ok
|
||
# Markdown-Hygiene: Beträge, Einzel-Stern, literales \n
|
||
auftraege = " ".join(guide._mathe_auftraege(
|
||
"Es gilt |C*| und *wichtig* laut Text.\\n- Punkt. Aber $\\neq$ bleibt ok.", "Test"))
|
||
assert "Beträge" in auftraege
|
||
assert "einzelnes „*“" in auftraege
|
||
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"))
|