Files
creator2/tests/test_auftraege.py
2026-07-14 00:15:03 +02:00

315 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Auftrags-Lebenszyklus (Guide): eingefrorener Wortlaut, Urteil je Runde,
Fehlalarm-Tod (einstimmig), Runden-Cap → eskaliert, Fund nur bei leerer Liste."""
import db
import fake_agents
import guide
import llm
import pytest
from config import AUFTRAG_RUNDEN_MAX
from conftest import run_anlegen, topic_anlegen
def _baustein(topic, lang_extra=""):
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)
lang = f"<!-- atom: {a} | A -->\n" + ("Wort " * 60).strip() + lang_extra
db.insert("sections", baustein_id=b_id, stage="pruefer", text_lang=lang,
text_kompakt="- p")
return db.one("SELECT * FROM bausteine WHERE id=?", (b_id,))
def _ctx(topic):
ctx = llm.Kontext(run_anlegen(topic), topic, "minimax")
ctx.ebene = "guide"
return ctx
def test_migration_befunde_json_zu_auftraegen():
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE sections(baustein_id INTEGER PRIMARY KEY,"
" stage TEXT NOT NULL DEFAULT 'writer', text_kompakt TEXT DEFAULT '',"
" text_lang TEXT DEFAULT '', befunde TEXT DEFAULT '[]',"
" qa_hash TEXT DEFAULT '', fix_versuche INTEGER NOT NULL DEFAULT 0)")
con.execute("INSERT INTO sections(baustein_id, befunde) VALUES(7, ?)", (db.j([
"KRITISCH (falsch): Zahl stimmt nicht",
"KRITISCH (luecke): Atom 5 fehlt",
"KRITISCH (fachlich_falsch): Claim X",
"KRITISCH (marker_fehlend): Marker 5 fehlt", # messen-Art → verwerfen
"KRITISCH: Marker für Atom 5 fehlt — exakt einfügen.", # det → verwerfen
"Langtext, Absatz 2 hat 130 Wörter (Regel: 4090): teilen."]),))
db._init_schema(con)
zeilen = con.execute("SELECT art, detail, quelle FROM auftraege").fetchall()
assert sorted(z[0] for z in zeilen) == ["falsch", "falsch", "luecke"]
assert ("falsch", "Claim X", "qa") in zeilen # fachlich_falsch → falsch, quelle qa
# Spalte weg + idempotent (zweiter Lauf legt nichts doppelt an)
assert not any(s[1] == "befunde" for s in con.execute("PRAGMA table_info(sections)"))
db._init_schema(con)
assert con.execute("SELECT COUNT(*) FROM auftraege").fetchone()[0] == 3
async def test_urteil_statusuebergaenge_und_keine_neuen_funde(monkeypatch):
topic = topic_anlegen("urteil")
b = _baustein(topic)
a1 = db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="D1")
a2 = db.insert("auftraege", baustein_id=b["id"], art="stil", detail="D2")
a3 = db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="D3")
urteile = {1: "behoben", 2: "kein_mangel", 3: "faktenbasis"}
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Pruefer-Urteil",
lambda p: [{"auftrag": i, "urteil": u} for i, u in urteile.items()])
# Fund-Modus darf bei offenen Aufträgen NIE laufen
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Pruefer",
lambda p: pytest.fail("Fund-Modus trotz offener Aufträge"))
await guide._auftraege_urteilen(_ctx(topic), b,
db.one("SELECT * FROM sections WHERE baustein_id=?",
(b["id"],)),
guide._offene_auftraege(b["id"]))
stat = {r["id"]: r["status"] for r in db.query("SELECT * FROM auftraege")}
assert stat == {a1: "behoben", a2: "kein_mangel", a3: "eskaliert"}
async def test_kein_mangel_nur_einstimmig(monkeypatch):
topic = topic_anlegen("split")
b = _baustein(topic)
au = db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="D")
votes = iter(["kein_mangel", "offen", "offen"]) # Stimme 1 ≠ Stimme 2 (+Ersatz)
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Pruefer-Urteil",
lambda p: [{"auftrag": 1, "urteil": next(votes)}])
await guide._auftraege_urteilen(_ctx(topic), b,
db.one("SELECT * FROM sections WHERE baustein_id=?",
(b["id"],)),
guide._offene_auftraege(b["id"]))
r = db.one("SELECT * FROM auftraege WHERE id=?", (au,))
assert r["status"] == "offen" and r["runden"] == 1 # uneins → offen, Runde zählt
async def test_runden_cap_eskaliert_und_wird_nicht_geroutet(monkeypatch):
topic = topic_anlegen("cap")
b = _baustein(topic)
au = db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="zäh",
runden=AUFTRAG_RUNDEN_MAX - 1)
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Pruefer-Urteil",
lambda p: [{"auftrag": 1, "urteil": "offen"}])
ctx = _ctx(topic)
await guide._auftraege_urteilen(ctx, b,
db.one("SELECT * FROM sections WHERE baustein_id=?",
(b["id"],)),
guide._offene_auftraege(b["id"]))
assert db.one("SELECT status FROM auftraege WHERE id=?", (au,))["status"] == "eskaliert"
# fakten_konflikt wird in reparieren nicht geroutet → kein Fortschritt = False
db.update("sections", "baustein_id", b["id"], stage="done")
bewegt = await guide.reparieren(ctx, [{"art": "fakten_konflikt",
"item": str(b["id"]), "detail": "zäh"}])
assert bewegt is False
assert db.one("SELECT stage FROM sections WHERE baustein_id=?",
(b["id"],))["stage"] == "done"
async def test_fund_dedup_gegen_kein_mangel(monkeypatch):
"""Ein totgestimmter Fehlalarm darf nicht als frische Zeile auferstehen."""
topic = topic_anlegen("wiedergaenger")
b = _baustein(topic)
db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="Fehlalarm X",
status="kein_mangel")
gesehen = {}
def finder(prompt):
gesehen["negativ"] = "Fehlalarm X" in prompt
return {"befunde": [{"art": "falsch", "detail": "Fehlalarm X"},
{"art": "luecke", "detail": "Neu Y"}]}
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Pruefer", finder)
await guide._auftraege_finden(_ctx(topic), b,
db.one("SELECT * FROM sections WHERE baustein_id=?",
(b["id"],)))
assert gesehen["negativ"] # Negativ-Liste stand im Prompt
offen = guide._offene_auftraege(b["id"])
assert [a["detail"] for a in offen] == ["Neu Y"]
async def test_fix_stil_optimistisch_falsch_via_urteil(monkeypatch):
topic = topic_anlegen("fixarten")
b = _baustein(topic)
stil = db.insert("auftraege", baustein_id=b["id"], art="stil", detail="glätten")
falsch = db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="Zahl prüfen")
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Fix",
lambda p: {"kompakt": "- p", "lang": sec["text_lang"] + " neu"})
# Re-Check-Urteil lässt den falsch-Auftrag OFFEN → nicht optimistisch geschlossen
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Pruefer-Urteil",
lambda p: [{"auftrag": 1, "urteil": "offen"}])
db.update("sections", "baustein_id", b["id"], stage="fix")
assert await guide._stage_fix(_ctx(topic), b) == "done"
assert db.one("SELECT status FROM auftraege WHERE id=?", (stil,))["status"] == "behoben"
assert db.one("SELECT status FROM auftraege WHERE id=?", (falsch,))["status"] == "offen"
def test_qa_auftrag_dedup_und_eskalation():
topic = topic_anlegen("qapfad")
b = _baustein(topic)
guide._qa_auftrag(b["id"], "Claim A")
guide._qa_auftrag(b["id"], "Claim A") # exakter Dedup
zeilen = db.query("SELECT * FROM auftraege WHERE baustein_id=?", (b["id"],))
assert len(zeilen) == 1 and zeilen[0]["quelle"] == "qa"
# Re-Bestätigung auf kein_mangel-Zeile: Richter uneins → eskaliert
db.update("auftraege", "id", zeilen[0]["id"], status="kein_mangel")
guide._qa_auftrag(b["id"], "Claim A")
assert db.one("SELECT status FROM auftraege WHERE id=?",
(zeilen[0]["id"],))["status"] == "eskaliert"
async def test_urteilsrunde_ohne_text_ist_bewegt(monkeypatch):
"""Statusübergänge zählen als Fortschritt — sonst bricht auto_loop reine
Urteilsrunden als Stillstand ab, obwohl Aufträge geschlossen wurden."""
topic = topic_anlegen("bewegt2")
b = _baustein(topic)
db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="D")
db.update("sections", "baustein_id", b["id"], stage="done")
# Fake-Urteil: behoben (Default-Handler); Text bleibt unverändert
bewegt = await guide.reparieren(_ctx(topic), [
{"art": "auftrag_offen", "item": str(b["id"]), "detail": "(falsch) D"}])
assert bewegt is True
assert db.query("SELECT * FROM auftraege WHERE status='offen'") == []
async def test_freeze_urteilt_weiter(monkeypatch):
"""Fix-Cap friert nur Stil-Fixes ein — Aufträge werden trotzdem geurteilt."""
topic = topic_anlegen("freeze")
b = _baustein(topic)
db.update("sections", "baustein_id", b["id"],
stage="done", fix_versuche=guide.FIX_MAX_VERSUCHE)
db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="D")
bewegt = await guide.reparieren(_ctx(topic), [
{"art": "auftrag_offen", "item": str(b["id"]), "detail": "(falsch) D"}])
assert bewegt is True # geurteilt (behoben) trotz Freeze
assert db.one("SELECT COUNT(*) n FROM auftraege WHERE status='behoben'")["n"] == 1
# ── Klärungs-Stichentscheid (eskalierte falsch/luecke) ────────────────────────
async def test_klaerung_text_falsch_wiedereroeffnet(monkeypatch):
"""Mehrheit text_falsch → Auftrag wieder offen (runden=0, geklaert=1); der
Fix bekommt das Streich-Mandat als [KLÄRUNG]-Vermerk."""
topic = topic_anlegen("klaer1")
b = _baustein(topic)
au = db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="Zahl falsch",
status="eskaliert", runden=AUFTRAG_RUNDEN_MAX)
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Konflikt-Klaerung",
lambda p: [{"auftrag": 1, "urteil": "text_falsch"}])
await guide._klaeren(_ctx(topic), b)
r = db.one("SELECT * FROM auftraege WHERE id=?", (au,))
assert (r["status"], r["runden"], r["geklaert"]) == ("offen", 0, 1)
# der Fix-Prompt trägt das Streich-Mandat
gesehen = {}
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
def fix(prompt):
gesehen["klaerung"] = "[KLÄRUNG" in prompt and "STREICHEN" in prompt
return {"kompakt": "- p", "lang": sec["text_lang"] + " korrigiert"}
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Fix", fix)
db.update("sections", "baustein_id", b["id"], stage="fix")
await guide._stage_fix(_ctx(topic), b)
assert gesehen["klaerung"]
async def test_klaerung_unbegruendet_schliesst():
"""Default-Fake stimmt unbegruendet → kein_mangel, geklaert=1, bewegt True."""
topic = topic_anlegen("klaer2")
b = _baustein(topic)
au = db.insert("auftraege", baustein_id=b["id"], art="luecke", detail="Fehlalarm",
status="eskaliert")
db.update("sections", "baustein_id", b["id"], stage="done")
bewegt = await guide.reparieren(_ctx(topic), [
{"art": "klaerung_offen", "item": str(b["id"]), "detail": "Fehlalarm"}])
r = db.one("SELECT * FROM auftraege WHERE id=?", (au,))
assert (r["status"], r["geklaert"]) == ("kein_mangel", 1)
assert bewegt is True
assert db.one("SELECT stage FROM sections WHERE baustein_id=?",
(b["id"],))["stage"] == "done" # keine Stage-Route nötig
async def test_klaerung_patt_terminal(monkeypatch):
"""Voll-Patt (1/1/1) → geklaert=1, bleibt eskaliert; messen zeigt
fakten_konflikt; zweiter reparieren-Lauf ruft die Klärung nie wieder."""
topic = topic_anlegen("klaer3")
b = _baustein(topic)
au = db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="strittig",
status="eskaliert")
votes = iter(["text_falsch", "unbegruendet", "quelle_unklar"])
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Konflikt-Klaerung",
lambda p: [{"auftrag": 1, "urteil": next(votes)}])
await guide._klaeren(_ctx(topic), b)
r = db.one("SELECT * FROM auftraege WHERE id=?", (au,))
assert (r["status"], r["geklaert"]) == ("eskaliert", 1)
ctx = _ctx(topic)
arten = [x["art"] for x in guide.messen(ctx)]
assert "fakten_konflikt" in arten and "klaerung_offen" not in arten
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Konflikt-Klaerung",
lambda p: pytest.fail("geklärter Auftrag erneut geklärt"))
assert await guide.reparieren(ctx, [{"art": "fakten_konflikt",
"item": str(b["id"]), "detail": "strittig"}]) is False
async def test_klaerung_ohne_gueltige_stimmen_fail_closed(monkeypatch):
"""Liefern die Richter keine gültigen Urteile, bleibt geklaert=0 —
nächste Runde klärt erneut (fail-closed)."""
topic = topic_anlegen("klaer4")
b = _baustein(topic)
au = db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="D",
status="eskaliert")
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Konflikt-Klaerung",
lambda p: [{"auftrag": 1, "urteil": "quatsch"}])
await guide._klaeren(_ctx(topic), b)
r = db.one("SELECT * FROM auftraege WHERE id=?", (au,))
assert (r["status"], r["geklaert"]) == ("eskaliert", 0)
async def test_runden_inkrement_ist_nicht_bewegt(monkeypatch):
"""Identisches Panel-Patt auf identischem Text = Leer-Churn → reparieren
meldet ehrlich False (runden zählt nicht mehr als Bewegung)."""
topic = topic_anlegen("leerchurn")
b = _baustein(topic)
db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="Patt")
db.update("sections", "baustein_id", b["id"], stage="done")
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Pruefer-Urteil",
lambda p: [{"auftrag": 1, "urteil": "offen"}])
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Fix",
lambda p: {"kompakt": sec["text_kompakt"], "lang": sec["text_lang"]})
bewegt = await guide.reparieren(_ctx(topic), [
{"art": "auftrag_offen", "item": str(b["id"]), "detail": "(falsch) Patt"}])
assert bewegt is False # nur runden+1, kein Status-/Text-Fortschritt
async def test_stil_rest_keine_route():
topic = topic_anlegen("stilrest")
b = _baustein(topic)
db.insert("auftraege", baustein_id=b["id"], art="stil", detail="zäh",
status="eskaliert")
db.update("sections", "baustein_id", b["id"], stage="done")
assert await guide.reparieren(_ctx(topic), [
{"art": "stil_rest", "item": str(b["id"]), "detail": "zäh"}]) is False
assert db.one("SELECT stage FROM sections WHERE baustein_id=?",
(b["id"],))["stage"] == "done"
async def test_reeskalation_nach_klaerung_terminal(monkeypatch):
"""Wiedereröffneter (geklärter) Auftrag, der erneut am Cap eskaliert, ist
terminal unauflösbar — keine zweite Klärung."""
topic = topic_anlegen("reesk")
b = _baustein(topic)
au = db.insert("auftraege", baustein_id=b["id"], art="falsch", detail="D",
geklaert=1, runden=AUFTRAG_RUNDEN_MAX - 1)
monkeypatch.setitem(fake_agents._HANDLER, "Guide-Pruefer-Urteil",
lambda p: [{"auftrag": 1, "urteil": "offen"}])
await guide._auftraege_urteilen(_ctx(topic), b,
db.one("SELECT * FROM sections WHERE baustein_id=?",
(b["id"],)),
guide._offene_auftraege(b["id"]))
r = db.one("SELECT * FROM auftraege WHERE id=?", (au,))
assert (r["status"], r["geklaert"]) == ("eskaliert", 1)
ctx = _ctx(topic)
arten = [x["art"] for x in guide.messen(ctx)]
assert "fakten_konflikt" in arten and "klaerung_offen" not in arten
assert guide._klaerung_kandidaten(b["id"]) == []