95 lines
4.7 KiB
Python
95 lines
4.7 KiB
Python
"""Anker-Repair: unverankerte Zitate (start=-1) werden erst deterministisch
|
||
neu gematcht — ohne LLM-Call."""
|
||
|
||
import db
|
||
import inventar
|
||
import llm
|
||
from conftest import run_anlegen, topic_anlegen
|
||
|
||
|
||
def test_merge_kollision_und_kette():
|
||
topic = topic_anlegen("mergekante")
|
||
a = db.insert("atome", topic=topic, titel="A", typ="begriff", definition="lang genug",
|
||
status="neu", braucht=db.j([]))
|
||
b = db.insert("atome", topic=topic, titel="B", typ="begriff", definition="kurz",
|
||
status="neu", braucht=db.j([]))
|
||
c = db.insert("atome", topic=topic, titel="C", typ="begriff", definition="x",
|
||
status="neu", braucht=db.j([]))
|
||
# beide haben dieselbe Kante zu C → blindes Umhängen würde UNIQUE verletzen
|
||
db.execute("INSERT INTO kanten(topic, von_atom, zu_atom, art) VALUES(?,?,?,'braucht')",
|
||
(topic, a, c))
|
||
db.execute("INSERT INTO kanten(topic, von_atom, zu_atom, art) VALUES(?,?,?,'braucht')",
|
||
(topic, b, c))
|
||
# und eine Kante zwischen den Merge-Partnern → würde Selbstkante
|
||
db.execute("INSERT INTO kanten(topic, von_atom, zu_atom, art) VALUES(?,?,?,'braucht')",
|
||
(topic, b, a))
|
||
inventar._merge(topic, a, b)
|
||
kanten = db.query("SELECT * FROM kanten WHERE topic=?", (topic,))
|
||
assert len(kanten) == 1 and kanten[0]["von_atom"] == a and kanten[0]["zu_atom"] == c
|
||
# Merge-Kette: (B, C) — B ist schon gemerged, Wurzel A übernimmt
|
||
inventar._merge(topic, b, c)
|
||
assert db.one("SELECT merged_into FROM atome WHERE id=?", (c,))["merged_into"] == a
|
||
|
||
|
||
async def test_anker_rematch_ohne_llm(tmp_path):
|
||
topic = topic_anlegen("rematch")
|
||
run = run_anlegen(topic)
|
||
snap = tmp_path / "q.md"
|
||
snap.write_text("Vorher. Der Satz steht hier drin. Nachher.", encoding="utf-8")
|
||
q = db.insert("quellen", topic=topic, art="datei", titel="q",
|
||
snapshot=str(snap), hash="h", status="atome")
|
||
a = db.insert("atome", topic=topic, titel="T", typ="begriff", definition="d",
|
||
status="ohne_anker", braucht=db.j([]))
|
||
# Extraktion fand das Zitat nicht (Whitespace-Differenz), hat es aber gespeichert
|
||
db.insert("anker", atom_id=a, quelle_id=q, start=-1, ende=-1,
|
||
zitat="Der Satz steht hier drin.")
|
||
|
||
ctx = llm.Kontext(run, topic, "minimax")
|
||
ctx.ebene = "inventar"
|
||
assert await inventar._anker_fixen_batch(ctx, [a]) is True
|
||
row = db.one("SELECT * FROM anker WHERE atom_id=? AND start>=0", (a,))
|
||
assert row is not None
|
||
assert db.one("SELECT status FROM atome WHERE id=?", (a,))["status"] == "neu"
|
||
assert db.query("SELECT id FROM events WHERE run_id=?", (run,)) == [] # kein LLM-Call
|
||
assert inventar.messen(ctx) == [] or all(
|
||
b["art"] != "atom_ohne_anker" for b in inventar.messen(ctx))
|
||
|
||
|
||
def test_titel_kern_faltet_schreibvarianten():
|
||
import textkit
|
||
assert textkit.titel_kern("ΔTSP2 Tour‑Länge") == textkit.titel_kern("ΔTSP2 Tour-Länge")
|
||
assert textkit.titel_kern("FeedbackVertexSet ∈ NP") == textkit.titel_kern("Feedback Vertex Set ∈ NP")
|
||
assert textkit.titel_kern("!!!") == ""
|
||
|
||
|
||
def test_fallback_nutzt_jaccard_schwelle(monkeypatch):
|
||
import embedding
|
||
a = "Polynomialzeitreduktionen sind transitiv wenn L1 auf L2 und L2 auf L3 reduzierbar sind"
|
||
b = ("Polynomialzeitreduktionen sind transitiv wenn eine Sprache L1 auf L2"
|
||
" und L2 auf L3 in Polynomialzeit reduzierbar ist")
|
||
# Kosinus-Schwelle allein: Paraphrase rutscht durch (aak Lauf 8)
|
||
assert embedding.kandidaten_paare([a, b], 0.75) == []
|
||
paare = embedding.kandidaten_paare([a, b], 0.75, 0.3)
|
||
assert [(i, j) for i, j, _ in paare] == [(0, 1)]
|
||
|
||
|
||
async def test_titel_dublette_wird_gemerged_trotz_ferner_definition():
|
||
topic = topic_anlegen("titeldup")
|
||
run = run_anlegen(topic)
|
||
# identischer Titel, Definitionen lexikalisch fern (Jaccard < 0.3):
|
||
# nur der Titel-Kern-Generator bringt das Paar ans Panel
|
||
a = db.insert("atome", topic=topic, titel="VERTEX COVER Problem", typ="begriff",
|
||
definition="Entscheidungsproblem, ob ein Graph ein Vertex Cover"
|
||
" der Größe höchstens k enthält.",
|
||
status="neu", braucht=db.j([]))
|
||
b = db.insert("atome", topic=topic, titel="VERTEX COVER Problem", typ="begriff",
|
||
definition="Gefragt wird nach einer Knotenmenge, die jede Kante"
|
||
" abdeckt und maximal k Elemente hat.",
|
||
status="neu", braucht=db.j([]))
|
||
ctx = llm.Kontext(run, topic, "minimax")
|
||
ctx.ebene = "inventar"
|
||
await inventar._judge_dedup(ctx)
|
||
stati = {r["id"]: r["status"] for r in db.query(
|
||
"SELECT id, status FROM atome WHERE topic=?", (topic,))}
|
||
assert sorted(stati.values()) == ["gemerged", "neu"]
|