433 lines
21 KiB
Python
433 lines
21 KiB
Python
"""Befund-Repair: gezielte Aktionen aus dem QA-Report (repair.py) — ohne Flow, gegen Test-DB."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
import repair
|
|
import qa as qa_mod
|
|
|
|
TOPIC = "reparatur"
|
|
|
|
|
|
def _report(**over):
|
|
r = {"topic": TOPIC, "note": 9.0, "quoten": {}, "hygiene": [], "dubletten": [],
|
|
"fremd": [], "unecht": [], "luecken": [], "artefakte": {"verwaiste": []}}
|
|
r.update(over)
|
|
return r
|
|
|
|
|
|
@pytest.fixture
|
|
async def env(testdb, tmp_path, monkeypatch):
|
|
"""Zwei fertige Blöcke auf beiden Boards + Subs/Artefakte + Sidecar-Dateien."""
|
|
db = testdb
|
|
monkeypatch.setattr(qa_mod, "QA_DIR", tmp_path / "qa")
|
|
files = {"sidecar": tmp_path / "sidecar.json", "facts": tmp_path / "facts.json",
|
|
"question_pattern": tmp_path / "qp.json", "sub_roh": tmp_path / "roh.json",
|
|
"artefakte": tmp_path / "artefakte.json"}
|
|
monkeypatch.setattr(repair, "_blocks_files", lambda t: files)
|
|
# frisches Abschluss-QA im Repair stumm schalten (eigener Test deckt qa_report ab)
|
|
async def _no_qa(topic, llm=False):
|
|
return None
|
|
monkeypatch.setattr(qa_mod, "qa_report", _no_qa)
|
|
|
|
async def _seed(title, desc, subs=1):
|
|
norm = repair._norm_title(title)
|
|
cid = "b-" + norm.replace(" ", "")[:10]
|
|
await db.kanban_upsert_card(TOPIC, "inventory", cid, "block", "done_block",
|
|
{"title": title, "description": desc, "sources": [f"{title}.txt"],
|
|
"readers": ["r1"], "mirrored_norm": norm})
|
|
await db.kanban_upsert_card(TOPIC, "artefacts", norm, "ablock", "done_artefact", {"title": title})
|
|
await db.upsert_block(TOPIC, norm, title, desc, [f"{title}.txt"])
|
|
await db.set_block_status(TOPIC, norm, "consensus")
|
|
for i in range(subs):
|
|
await db.put_subblock(TOPIC, norm, f"sub{i}", title, f"Sub {i}")
|
|
await db.put_sub_artifact(TOPIC, norm, "sub0", "flashcard", "{}", title, "Sub 0")
|
|
return cid
|
|
|
|
for p in files.values():
|
|
p.write_text("{}", encoding="utf-8")
|
|
(tmp_path / "qa" / TOPIC).mkdir(parents=True)
|
|
|
|
def write_report(r):
|
|
(tmp_path / "qa" / TOPIC / "r.json").write_text(json.dumps(r), encoding="utf-8")
|
|
|
|
return db, _seed, files, write_report
|
|
|
|
|
|
async def test_merge_confirmed_duplicate(env, monkeypatch):
|
|
db, seed, files, write_report = env
|
|
cid_a = await seed("Alpha", "kurz")
|
|
cid_b = await seed("Alpha Problem", "deutlich längere Beschreibung — Gewinner")
|
|
write_report(_report(dubletten=[{"a": "Alpha", "b": "Alpha Problem", "llm": "ja"},
|
|
{"a": "Alpha", "b": "Beta", "llm": "nein"}]))
|
|
calls = []
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
calls.append(prompt)
|
|
return 0, '{"relevant": {"1": "ja"}}', ""
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["merges"] == ["Alpha → Alpha Problem"]
|
|
assert len(calls) == 1 and "Beta" not in calls[0] # nur das llm=ja-Paar zum Judge
|
|
verlierer = await db.kanban_get_card(TOPIC, "inventory", cid_a)
|
|
assert verlierer["stage"] == "grouped" and verlierer["payload"]["merged_into"] == "Alpha Problem"
|
|
gewinner = await db.kanban_get_card(TOPIC, "inventory", cid_b)
|
|
assert "Alpha.txt" in gewinner["payload"]["sources"] # Union
|
|
assert await db.kanban_get_card(TOPIC, "artefacts", "alpha") is None
|
|
assert not [r for r in await db.list_subblocks(TOPIC, "alpha")]
|
|
|
|
|
|
async def test_fremd_removed_only_on_nein(env, monkeypatch):
|
|
db, seed, files, write_report = env
|
|
cid_f = await seed("Fremdling", "gehört nicht rein")
|
|
cid_e = await seed("Echter", "belegt")
|
|
write_report(_report(fremd=["Fremdling", "Echter"]))
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
if "-st-" in key: # Stichentscheid über den strittigen „Echter": behalten
|
|
return 0, '{"relevant": {"1": "ja"}}', ""
|
|
return 0, '{"relevant": {"1": "nein", "2": "ja"}}', ""
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
monkeypatch.setattr(repair, "source_folder", lambda t: None)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["entfernt"] == ["Fremdling"]
|
|
weg = await db.kanban_get_card(TOPIC, "inventory", cid_f)
|
|
assert weg["stage"] == "rejected" and weg["payload"]["reason"] == "qa-fremd"
|
|
bleibt = await db.kanban_get_card(TOPIC, "inventory", cid_e)
|
|
assert bleibt["stage"] == "done_block"
|
|
|
|
|
|
async def test_judge_failure_keeps_everything(env, monkeypatch):
|
|
db, seed, files, write_report = env
|
|
cid = await seed("Wackelig", "unsicher")
|
|
write_report(_report(unecht=["Wackelig"]))
|
|
|
|
async def broken_agent(key, prompt, timeout, **kw):
|
|
raise RuntimeError("boom")
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", broken_agent)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["entfernt"] == []
|
|
card = await db.kanban_get_card(TOPIC, "inventory", cid)
|
|
assert card["stage"] == "done_block" # fail-open
|
|
|
|
|
|
async def test_hygiene_cleans_title_norm_invariant(env, monkeypatch):
|
|
db, seed, files, write_report = env
|
|
cid = await seed("**Fetter Titel**", "beschreibung")
|
|
files["sidecar"].write_text(json.dumps({"**Fetter Titel**": ["s"]}), encoding="utf-8")
|
|
write_report(_report(hygiene=[{"titel": "**Fetter Titel**", "probleme": ["markdown"]}]))
|
|
|
|
async def no_agent(*a, **kw):
|
|
raise AssertionError("Hygiene braucht keinen Agenten")
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", no_agent)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["hygiene"] == ["**Fetter Titel** → Fetter Titel"]
|
|
card = await db.kanban_get_card(TOPIC, "inventory", cid)
|
|
assert card["payload"]["title"] == "Fetter Titel"
|
|
assert json.loads(files["sidecar"].read_text()) == {"Fetter Titel": ["s"]}
|
|
rows = await db.list_blocks(TOPIC)
|
|
assert any(r["title"] == "Fetter Titel" and r["status"] == "consensus" for r in rows)
|
|
|
|
|
|
async def test_no_report_is_clean_error(env):
|
|
db, seed, files, write_report = env
|
|
res = await repair.repair_befunde("gibtsnicht")
|
|
assert "fehler" in res
|
|
|
|
|
|
async def test_abschluss_qa_misst_mit_llm(env, monkeypatch):
|
|
"""Repair-Abschlussreport misst mit LLM — der llm=False-Report blendete
|
|
sub_dubletten aus und ließ die Note zwischen 10.0 und ~9 pendeln."""
|
|
db, seed, files, write_report = env
|
|
write_report(_report())
|
|
import qa as qa_mod
|
|
seen = {}
|
|
|
|
async def spy(topic, llm=False):
|
|
seen["llm"] = llm
|
|
return None
|
|
|
|
monkeypatch.setattr(qa_mod, "qa_report", spy)
|
|
await repair.repair_befunde(TOPIC)
|
|
assert seen["llm"] is True
|
|
|
|
|
|
async def test_sub_dubletten_merge(env, monkeypatch):
|
|
"""Bestätigtes Sub-Paar + Zweitmeinung ja → Verlierer variant, Frage/Artefakt
|
|
wandern zum Gewinner (bzw. fallen weg, wenn er den Typ schon hat)."""
|
|
db, seed, files, write_report = env
|
|
await seed("Alpha", "beschr")
|
|
norm = repair._norm_title("Alpha")
|
|
await db.put_subblock(TOPIC, norm, "gewinner sub", "Alpha", "Gewinner Sub",
|
|
facts='{"key_points": ["a", "b"]}', status="consensus")
|
|
await db.put_subblock(TOPIC, norm, "verlierer sub", "Alpha", "Verlierer Sub",
|
|
facts='{"key_points": ["x"]}', status="consensus")
|
|
await db.put_sub_artifact(TOPIC, norm, "verlierer sub", "example", "{}", "Alpha", "Verlierer Sub")
|
|
await db.upsert_question_pattern(TOPIC, norm, "verlierer sub", "Alpha", "Verlierer Sub", "Frage V?")
|
|
write_report(_report(sub_dubletten=[
|
|
{"a": "[Alpha] Gewinner Sub", "b": "[Alpha] Verlierer Sub", "llm": "ja"},
|
|
{"a": "[Alpha] Gibtsnicht", "b": "[Alpha] Verlierer Sub", "llm": "ja"}])) # tote Zeile → skip
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
assert "Gibtsnicht" not in prompt
|
|
return 0, '{"relevant": {"1": "ja"}}', ""
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["sub_merges"] == ["Verlierer Sub → Gewinner Sub"]
|
|
rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, norm)}
|
|
assert rows["verlierer sub"] == "variant" and rows["gewinner sub"] == "consensus"
|
|
arts = {(r["sub_norm"], r["type"]) for r in await db.get_sub_artefakte(TOPIC)}
|
|
assert ("gewinner sub", "example") in arts and ("verlierer sub", "example") not in arts
|
|
fragen = {r["sub_norm"]: r["question"] for r in await db.list_question_pattern(TOPIC)}
|
|
assert fragen.get("gewinner sub") == "Frage V?" and "verlierer sub" not in fragen
|
|
|
|
|
|
async def test_sub_dubletten_zweitmeinung_nein(env, monkeypatch):
|
|
db, seed, files, write_report = env
|
|
await seed("Alpha", "beschr")
|
|
norm = repair._norm_title("Alpha")
|
|
await db.put_subblock(TOPIC, norm, "sub a", "Alpha", "Sub A", status="consensus")
|
|
await db.put_subblock(TOPIC, norm, "sub b", "Alpha", "Sub B", status="consensus")
|
|
write_report(_report(sub_dubletten=[{"a": "[Alpha] Sub A", "b": "[Alpha] Sub B", "llm": "ja"}]))
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
return 0, '{"relevant": {"1": "nein"}}', ""
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["sub_merges"] == []
|
|
rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, norm)}
|
|
assert rows["sub a"] == rows["sub b"] == "consensus"
|
|
|
|
|
|
async def test_sub_dubletten_stichentscheid_faltet(env, monkeypatch):
|
|
"""Dissens QA (Befund) vs. Zweitmeinung (behalten) → Stichentscheid-Judge (Key -st)
|
|
entscheidet mit 2:1 für den Befund → Merge. Vorher pendelte die Note dauerhaft
|
|
unter 10 ohne Fix-Pfad („keine behebbaren Befunde" trotz Befund)."""
|
|
db, seed, files, write_report = env
|
|
await seed("Alpha", "beschr")
|
|
norm = repair._norm_title("Alpha")
|
|
await db.put_subblock(TOPIC, norm, "sub a", "Alpha", "Sub A",
|
|
facts='{"key_points": ["a"]}', status="consensus")
|
|
await db.put_subblock(TOPIC, norm, "sub b", "Alpha", "Sub B", status="consensus")
|
|
write_report(_report(sub_dubletten=[{"a": "[Alpha] Sub A", "b": "[Alpha] Sub B", "llm": "ja"}]))
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
if "-st-" in key: # Stichentscheid bestätigt den QA-Befund
|
|
return 0, '{"relevant": {"1": "ja"}}', ""
|
|
return 0, '{"relevant": {"1": "nein"}}', "" # Zweitmeinung widerspricht
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["sub_merges"] == ["Sub B → Sub A"]
|
|
rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, norm)}
|
|
assert rows["sub b"] == "variant" and rows["sub a"] == "consensus"
|
|
|
|
|
|
async def test_stichentscheid_behalten_persistiert_freispruch(env, monkeypatch):
|
|
"""2:1 „behalten" (Zweitmeinung + j3 einig gegen den QA-Befund) → Freispruch wird
|
|
persistiert und der Report des nächsten qa_report zählt das Paar nicht mehr —
|
|
vorher pendelte die Note dauerhaft unter 10 ohne Fix-Pfad."""
|
|
import qa as qa_mod
|
|
db, seed, files, write_report = env
|
|
await seed("Alpha", "beschr")
|
|
norm = repair._norm_title("Alpha")
|
|
await db.put_subblock(TOPIC, norm, "sub a", "Alpha", "Sub A", status="consensus")
|
|
await db.put_subblock(TOPIC, norm, "sub b", "Alpha", "Sub B", status="consensus")
|
|
write_report(_report(sub_dubletten=[{"a": "[Alpha] Sub A", "b": "[Alpha] Sub B", "llm": "ja"}]))
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
return 0, '{"relevant": {"1": "nein"}}', "" # beide Repair-Judges: behalten
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["sub_merges"] == [] and len(res["freigesprochen"]) == 1
|
|
frei = qa_mod.lade_freispruch(TOPIC)
|
|
key = qa_mod._paar_key("[Alpha] Sub A", "[Alpha] Sub B")
|
|
assert key in set(frei.get("sub_dubletten") or [])
|
|
# QA-Seite: bestätigtes, aber freigesprochenes Paar zählt nicht in die Quote
|
|
sd = [{"a": "[Alpha] Sub A", "b": "[Alpha] Sub B", "llm": "ja"}]
|
|
frei_sub = set(frei["sub_dubletten"])
|
|
zaehlt = sum(1 for p in sd if p.get("llm") == "ja"
|
|
and qa_mod._paar_key(p["a"], p["b"]) not in frei_sub)
|
|
assert zaehlt == 0
|
|
|
|
|
|
async def test_fremd_stichentscheid_behalten(env, monkeypatch):
|
|
"""Dissens bei fremd, Stichentscheid sagt ebenfalls behalten (ja) → Block bleibt
|
|
(fail-open bei 1:2 gegen den Befund)."""
|
|
db, seed, files, write_report = env
|
|
await seed("Alpha", "beschr")
|
|
write_report(_report(fremd=["Alpha"]))
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
return 0, '{"relevant": {"1": "ja"}}', "" # beide: belegt/behalten
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["entfernt"] == []
|
|
|
|
|
|
async def test_waisen_cleanup(env, monkeypatch):
|
|
"""Artefakte/Fragen auf verworfene oder fehlende Subs fliegen; lebende und
|
|
mehrdeutig-präfixige bleiben."""
|
|
db, seed, files, write_report = env
|
|
await seed("Alpha", "beschreibung") # legt sub0 (consensus) + flashcard auf sub0 an
|
|
norm = repair._norm_title("Alpha")
|
|
await db.put_subblock(TOPIC, norm, "weg", "Alpha", "Weg", status="discarded")
|
|
await db.put_subblock(TOPIC, norm, "doppel: eins", "Alpha", "Doppel eins")
|
|
await db.put_subblock(TOPIC, norm, "doppel: zwei", "Alpha", "Doppel zwei")
|
|
await db.put_sub_artifact(TOPIC, norm, "weg", "flashcard", "{}", "Alpha", "Weg") # tot
|
|
await db.put_sub_artifact(TOPIC, norm, "fehlt", "example", "{}", "Alpha", "Fehlt") # tot
|
|
await db.put_sub_artifact(TOPIC, norm, "doppel", "example", "{}", "Alpha", "Doppel") # mehrdeutig → bleibt
|
|
await db.upsert_question_pattern(TOPIC, norm, "fehlt", "Alpha", "Fehlt", "Frage?") # tot
|
|
write_report(_report())
|
|
|
|
async def no_agent(*a, **kw):
|
|
raise AssertionError("Aufräumen braucht keinen Agenten")
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", no_agent)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["aufgeraeumt"] == 3
|
|
rest = {(r["sub_norm"], r["type"]) for r in await db.get_sub_artefakte(TOPIC)}
|
|
assert rest == {("sub0", "flashcard"), ("doppel", "example")}
|
|
assert not [r for r in await db.list_question_pattern(TOPIC)]
|
|
|
|
|
|
async def test_dubletten_verdacht_freispruch(env, monkeypatch):
|
|
"""Widerlegtes/unbeurteiltes Verdachtspaar bekommt den Klärungskanal: Judge sagt
|
|
behalten → Freispruch persistiert; der nächste qa_report zählt das Paar nicht mehr
|
|
(vorher zählte widerlegter Verdacht ewig in die Quote — Mess-Rauschen ohne Ausweg)."""
|
|
import qa as qa_mod
|
|
db, seed, files, write_report = env
|
|
await seed("Alpha", "beschr")
|
|
await seed("Alpha Kreis", "beschr")
|
|
write_report(_report(dubletten=[{"a": "Alpha", "b": "Alpha Kreis", "llm": "nein"}]))
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
return 0, '{"relevant": {"1": "nein"}}', "" # Judge: keine Dublette
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["merges"] == [] and len(res["freigesprochen"]) == 1 and res["aktionen"] == 1
|
|
frei = qa_mod.lade_freispruch(TOPIC)
|
|
assert qa_mod._paar_key("Alpha", "Alpha Kreis") in set(frei.get("dubletten") or [])
|
|
|
|
|
|
async def test_dubletten_verdacht_stichentscheid_merged(env, monkeypatch):
|
|
"""Verdachtspaar (llm=nein), aber Judge+Stichentscheid sagen beide Dublette →
|
|
Merge mit 2:1 gegen das QA-Urteil."""
|
|
db, seed, files, write_report = env
|
|
await seed("Alpha", "kurz")
|
|
await seed("Alpha Kreis", "deutlich längere Beschreibung — Gewinner")
|
|
write_report(_report(dubletten=[{"a": "Alpha", "b": "Alpha Kreis", "llm": "nein"}]))
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
return 0, '{"relevant": {"1": "ja"}}', ""
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
res = await repair.repair_befunde(TOPIC)
|
|
assert res["merges"] == ["Alpha → Alpha Kreis"] and res["freigesprochen"] == []
|
|
|
|
|
|
async def test_beleg_nachfass_discard_und_datenfix(env, monkeypatch):
|
|
"""mentions=0-Subs: Judge bestätigt „kein Beleg" → Sub verworfen; Dissens mit
|
|
Stichentscheid „belegt" → mentions=1 nachgetragen (Datenfix, kein Freispruch)."""
|
|
db, seed, files, write_report = env
|
|
await seed("Alpha", "beschr")
|
|
norm = repair._norm_title("Alpha")
|
|
await db.put_subblock(TOPIC, norm, "ohne beleg", "Alpha", "Ohne Beleg", status="consensus")
|
|
await db.put_subblock(TOPIC, norm, "doch belegt", "Alpha", "Doch Belegt", status="consensus")
|
|
await db.set_subblock_fields(TOPIC, norm, "ohne beleg", mentions=0)
|
|
await db.set_subblock_fields(TOPIC, norm, "doch belegt", mentions=0)
|
|
write_report(_report(beleg={"subs_ohne_beleg": ["Alpha · Ohne Beleg", "Alpha · Doch Belegt"]}))
|
|
monkeypatch.setattr(repair, "source_folder", lambda t: files["sidecar"].parent)
|
|
monkeypatch.setattr(repair, "_evidence_pack", lambda *a, **kw: "AUSZUG")
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
if "-beleg-st-" in key:
|
|
return 0, '{"relevant": {"1": "ja"}}', "" # Stichentscheid: belegt
|
|
return 0, '{"relevant": {"1": "nein", "2": "ja"}}', ""
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
res = await repair.repair_befunde(TOPIC, "artefacts")
|
|
assert sorted(res["beleg_fix"]) == ["belegt: Alpha · Doch Belegt", "entfernt: Alpha · Ohne Beleg"]
|
|
rows = {r["sub_norm"]: r for r in await db.list_subblocks(TOPIC, norm)}
|
|
assert rows["ohne beleg"]["status"] == "discarded"
|
|
assert rows["doch belegt"]["status"] == "consensus" and rows["doch belegt"]["mentions"] == 1
|
|
|
|
|
|
async def test_fix_beschreibung_fuellt_leere(env, monkeypatch):
|
|
"""Hygiene „leere-beschreibung" → Sanierung schreibt EINE Beschreibung aus den
|
|
Auszügen; Titel bleibt unverändert."""
|
|
db, seed, files, write_report = env
|
|
cid = await seed("Leerling", "")
|
|
write_report(_report(hygiene=[{"titel": "Leerling", "probleme": ["leere-beschreibung"]}]))
|
|
monkeypatch.setattr(repair, "source_folder", lambda t: files["sidecar"].parent)
|
|
monkeypatch.setattr(repair, "_evidence_pack", lambda *a, **kw: "AUSZUG")
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
assert "-sanierung-" in key
|
|
return 0, '{"title": "Leerling", "description": "Aus dem Material."}', ""
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
res = await repair.repair_befunde(TOPIC, "inventory")
|
|
assert "beschrieben: Leerling" in res["hygiene"]
|
|
card = await db.kanban_get_card(TOPIC, "inventory", cid)
|
|
assert card["payload"]["description"] == "Aus dem Material."
|
|
|
|
|
|
async def test_fix_suffix_rename_rekeyt_alles(env, monkeypatch):
|
|
"""Kollisions-Suffix, Basis-Norm frei → Rename inkl. Re-Key von Subblocks/Fragen/
|
|
Artefakten + Board-2-Karte; danach keine Invarianten-Waisen."""
|
|
from invarianten import pruefe_invarianten
|
|
db, seed, files, write_report = env
|
|
cid = await seed("Gamma (2)", "beschr")
|
|
norm_alt = repair._norm_title("Gamma (2)")
|
|
await db.upsert_question_pattern(TOPIC, norm_alt, "sub0", "Gamma (2)", "Sub 0", "F?")
|
|
write_report(_report(hygiene=[{"titel": "Gamma (2)", "probleme": ["kollisions-suffix"]}]))
|
|
|
|
async def no_agent(*a, **kw):
|
|
raise AssertionError("Rename bei freier Basis braucht keinen Agenten")
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", no_agent)
|
|
res = await repair.repair_befunde(TOPIC, "inventory")
|
|
assert "suffix: Gamma (2) → Gamma" in res["hygiene"]
|
|
card = await db.kanban_get_card(TOPIC, "inventory", cid)
|
|
assert card["payload"]["title"] == "Gamma" and card["payload"]["mirrored_norm"] == "gamma"
|
|
assert await db.kanban_get_card(TOPIC, "artefacts", norm_alt) is None
|
|
assert (await db.kanban_get_card(TOPIC, "artefacts", "gamma"))["payload"]["title"] == "Gamma"
|
|
assert {r["block_norm"] for r in await db.list_subblocks(TOPIC, "gamma")} == {"gamma"}
|
|
assert not await db.list_subblocks(TOPIC, norm_alt)
|
|
assert {r["block_norm"] for r in await db.list_question_pattern(TOPIC)} == {"gamma"}
|
|
befunde = await pruefe_invarianten(TOPIC) # DB-Sicht: keine Waisen nach dem Re-Key
|
|
assert not [b for b in befunde if "Waise" in b]
|
|
|
|
|
|
async def test_pruefe_luecken_freispruch(env, monkeypatch):
|
|
"""Offene Lücken + Konzept-Lücken: 2:1 „keine echte Lücke" → Freispruch persistiert;
|
|
_zaehlbare_luecken zählt freigesprochene Fundstellen nicht mehr."""
|
|
import qa as qa_mod
|
|
db, seed, files, write_report = env
|
|
await seed("Alpha", "beschr")
|
|
lk = [{"datei": "f.txt", "abschnitt": 1, "vorschau": "verwaister Abschnitt", "llm": "ja"}]
|
|
write_report(_report(luecken=lk, konzept_luecken=["Satz von Foo"]))
|
|
|
|
async def fake_agent(key, prompt, timeout, **kw):
|
|
return 0, '{"relevant": {"1": "nein"}}', "" # beide Judges: keine echte Lücke
|
|
|
|
import agents; monkeypatch.setattr(agents, "run_agent", fake_agent)
|
|
res = await repair.repair_befunde(TOPIC, "inventory")
|
|
assert len(res["freigesprochen"]) == 2
|
|
frei = qa_mod.lade_freispruch(TOPIC)
|
|
assert qa_mod.luecken_key(lk[0]) in set(frei.get("luecken") or [])
|
|
assert "satz von foo" in {s.casefold() for s in frei.get("konzept_luecken") or []}
|
|
lk[0]["freispruch"] = True
|
|
assert qa_mod._zaehlbare_luecken(lk, llm=True) == []
|