This commit is contained in:
team3
2026-07-12 16:13:50 +02:00
parent a284e03225
commit 31a42bbf1d
48 changed files with 3625 additions and 253 deletions

View File

@@ -55,6 +55,99 @@ async def test_anker_rematch_ohne_llm(tmp_path):
b["art"] != "atom_ohne_anker" for b in inventar.messen(ctx))
async def test_resume_liest_teilextrahierte_quelle_weiter(tmp_path):
"""Abbruch mitten in der Extraktion (atome_stand leer): der Resume liest die
Quelle komplett neu, statt sie als fertig zu überspringen."""
topic = topic_anlegen("resume-ext")
run = run_anlegen(topic)
snap = tmp_path / "q.md"
snap.write_text("Absatz eins über Kompaktheit.\n\nAbsatz zwei über Vollständigkeit.",
encoding="utf-8")
# Quelle hat schon EIN verankertes Atom, aber atome_stand ist leer (Abbruch)
q = db.insert("quellen", topic=topic, art="datei", titel="q",
snapshot=str(snap), hash="h", status="extrahiert", atome_stand="")
a = db.insert("atome", topic=topic, titel="Alt", typ="begriff", definition="d",
status="neu", braucht=db.j([]))
db.insert("anker", atom_id=a, quelle_id=q, start=0, ende=5, zitat="Absatz")
ctx = llm.Kontext(run, topic, "minimax")
ctx.ebene = "inventar"
await inventar._extrahiere_quelle(ctx, db.one("SELECT * FROM quellen WHERE id=?", (q,)))
# neu gelesen → Extraktions-Events da, atome_stand jetzt gesetzt
assert db.query("SELECT id FROM events WHERE run_id=? AND stage='extraktion'", (run,))
assert db.one("SELECT atome_stand FROM quellen WHERE id=?", (q,))["atome_stand"] == "atome"
# Gegenprobe: mit gesetztem Merker wird NICHT neu gelesen
run2 = run_anlegen(topic)
ctx2 = llm.Kontext(run2, topic, "minimax")
ctx2.ebene = "inventar"
await inventar._extrahiere_quelle(ctx2, db.one("SELECT * FROM quellen WHERE id=?", (q,)))
assert db.query("SELECT id FROM events WHERE run_id=?", (run2,)) == []
async def test_stichentscheid_braucht_zwei_stimmen(monkeypatch):
"""Verwerfen (destruktiv) nur bei vollzähligem einstimmigem „fremd"-Panel."""
topic = topic_anlegen("stich")
run = run_anlegen(topic)
db.insert("soll", topic=topic, punkt="P1", status="bestaetigt", belege=db.j([]))
ctx = llm.Kontext(run, topic, "minimax")
ctx.ebene = "inventar"
def neu_atom():
return db.insert("atome", topic=topic, titel="X", typ="begriff",
definition="d", status="neu", braucht=db.j([]))
def panel_mit(votes):
async def _p(ctx, groesse, **kw):
return list(votes)
return _p
a1 = neu_atom() # 1× fremd → nicht vollzählig → Atom bleibt
monkeypatch.setattr(llm, "panel", panel_mit([{"soll": "fremd"}]))
await inventar._soll_stichentscheid(ctx, a1)
assert db.one("SELECT status FROM atome WHERE id=?", (a1,))["status"] == "neu"
a2 = neu_atom() # 2× fremd → verworfen
monkeypatch.setattr(llm, "panel", panel_mit([{"soll": "fremd"}, {"soll": "fremd"}]))
await inventar._soll_stichentscheid(ctx, a2)
assert db.one("SELECT status FROM atome WHERE id=?", (a2,))["status"] == "verworfen"
a3 = neu_atom() # 2× ohne soll-Feld → kein fremd-Votum → bleibt
monkeypatch.setattr(llm, "panel", panel_mit([{}, {}]))
await inventar._soll_stichentscheid(ctx, a3)
assert db.one("SELECT status FROM atome WHERE id=?", (a3,))["status"] == "neu"
async def test_anker_batch_ausfall_verwirft_nicht(monkeypatch, tmp_path):
"""Call-Ausfall (None) ≠ „keine Quellstelle" → Atom bleibt ohne_anker, bewegt=False."""
import fake_agents
topic = topic_anlegen("ankerfail")
run = run_anlegen(topic)
snap = tmp_path / "q.md"
snap.write_text("Ganz anderer Text ohne die gesuchte Stelle.", 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([]))
db.insert("anker", atom_id=a, quelle_id=q, start=-1, ende=-1,
zitat="Ein Zitat das nirgends im Quelltext steht und lang genug ist.")
monkeypatch.setitem(fake_agents._HANDLER, "Atom-Anker-Fix-Batch", lambda p: {})
ctx = llm.Kontext(run, topic, "minimax")
ctx.ebene = "inventar"
bewegt = await inventar._anker_fixen_batch(ctx, [a])
assert db.one("SELECT status FROM atome WHERE id=?", (a,))["status"] == "ohne_anker"
assert bewegt is False
def test_finde_zitat_casefold_sz_offsets():
"""casefold-Stufe: ß→ss-Expansionen vor dem Treffer dürfen den Span nicht
verschieben; Treffer am Textende darf nicht in einen IndexError laufen."""
import textkit
text = "Straße und Fußweg. " * 30 + "DER KERNSATZ STEHT AM ENDE."
span = textkit.finde_zitat(text, "der kernsatz steht am ende.")
assert span is not None
assert text[span[0]:span[1]] == "DER KERNSATZ STEHT AM ENDE."
def test_titel_kern_faltet_schreibvarianten():
import textkit
assert textkit.titel_kern("ΔTSP2 TourLänge") == textkit.titel_kern("ΔTSP2 Tour-Länge")