"""Bündeln: Claim mehrerer Tasks je Call, Teil-Antwort-Retry statt Verwerfen.""" import asyncio from backend import db, engine, graph, guide from .conftest import topic_anlegen def _tasks_anlegen(topic, run_id, n=5): return [db.insert("tasks", run_id=run_id, topic=topic, knoten="llm_pruefung", item=f"p{i}", payload=db.j({"baustein_id": i})) for i in range(n)] def test_buendel_claim_mittel_nimmt_drei(): topic = topic_anlegen() run_id = db.insert("runs", topic=topic, status="running") ids = _tasks_anlegen(topic, run_id) g = graph.get() db.execute("UPDATE tasks SET status='laufend' WHERE id=?", ids[0]) t = dict(db.one("SELECT * FROM tasks WHERE id=?", ids[0])) mitglieder = engine._buendel_claimen(g, t, g.knoten["llm_pruefung"]) assert len(mitglieder) == 2 # Klasse mittel = 3 → Haupt-Task + 2 laufend = db.query("SELECT id FROM tasks WHERE topic=? AND status='laufend'", topic) assert len(laufend) == 3 assert db.one("SELECT COUNT(*) c FROM tasks WHERE topic=? AND " "status='offen'", topic)["c"] == 2 def test_buendel_claim_nur_fuer_optin_worker(): topic = topic_anlegen() run_id = db.insert("runs", topic=topic, status="running") for i in range(3): # writer: Klasse lang → nie bündeln db.insert("tasks", run_id=run_id, topic=topic, knoten="writer", item=f"w{i}", payload=db.j({"baustein_id": i})) g = graph.get() t = dict(db.one("SELECT * FROM tasks WHERE topic=? AND item='w0'", topic)) assert engine._buendel_claimen(g, t, g.knoten["writer"]) == [] def test_teil_kaputt_wird_neu_dann_fehler(): topic = topic_anlegen() run_id = db.insert("runs", topic=topic, status="running") ids = _tasks_anlegen(topic, run_id, n=2) db.execute("UPDATE tasks SET status='laufend' WHERE topic=?", topic) g = graph.get() kn = g.knoten["llm_pruefung"] t = dict(db.one("SELECT * FROM tasks WHERE id=?", ids[0])) t["_buendel"] = [dict(db.one("SELECT * FROM tasks WHERE id=?", ids[1]))] erg = engine.Ergebnis(daten={"befunde": 0}, teil_status={ids[0]: "fertig", ids[1]: "neu"}) engine._abschluss_schreiben(g, t, kn, erg) haupt = db.one("SELECT * FROM tasks WHERE id=?", ids[0]) teil = db.one("SELECT * FROM tasks WHERE id=?", ids[1]) assert haupt["status"] == "fertig" assert teil["status"] == "offen" and teil["versuch"] == 1 # zweiter kaputter Durchlauf → sichtbar 'fehler' (max_versuche=2) db.execute("UPDATE tasks SET status='laufend' WHERE id=?", ids[1]) t2 = dict(db.one("SELECT * FROM tasks WHERE id=?", ids[0])) t2["_buendel"] = [dict(db.one("SELECT * FROM tasks WHERE id=?", ids[1]))] engine._abschluss_schreiben(g, t2, kn, engine.Ergebnis( teil_status={ids[1]: "neu"})) assert db.one("SELECT status FROM tasks WHERE id=?", ids[1])["status"] == "fehler" async def test_llm_pruefung_buendel_teilantwort(monkeypatch): """Fehlt eine Section in der Antwort, geht NUR ihr Task auf 'neu'.""" topic = topic_anlegen() run_id = db.insert("runs", topic=topic, status="running") b1 = db.insert("bausteine", topic=topic, titel="B1") b2 = db.insert("bausteine", topic=topic, titel="B2") db.insert("sections", baustein_id=b1, text="Text eins.") db.insert("sections", baustein_id=b2, text="Text zwei.") t1 = db.insert("tasks", run_id=run_id, topic=topic, knoten="llm_pruefung", item="p1", payload=db.j({"baustein_id": b1}), status="laufend") t2 = db.insert("tasks", run_id=run_id, topic=topic, knoten="llm_pruefung", item="p2", payload=db.j({"baustein_id": b2}), status="laufend") from backend import llm as llm_mod async def antwort(**kw): assert f"=== SECTION {b2} ===" in kw["werte"]["sections"] return f"===OK===\nSECTION: {b1}" # b2 fehlt in beiden Stimmen monkeypatch.setattr(llm_mod, "call", antwort) task = dict(db.one("SELECT * FROM tasks WHERE id=?", t1)) task["_buendel"] = [dict(db.one("SELECT * FROM tasks WHERE id=?", t2))] erg = await guide.llm_pruefung(task) assert erg.teil_status[t1] == "fertig" assert erg.teil_status[t2] == "neu" async def test_worker_exception_setzt_buendel_zurueck(): topic = topic_anlegen() run_id = db.insert("runs", topic=topic, status="running") ids = _tasks_anlegen(topic, run_id, n=2) db.execute("UPDATE tasks SET status='laufend' WHERE topic=?", topic) g = graph.get() async def kaputt(task): raise RuntimeError("boom") alt = engine.WORKER["guide.llm_pruefung"] engine.WORKER["guide.llm_pruefung"] = kaputt try: t = dict(db.one("SELECT * FROM tasks WHERE id=?", ids[0])) t["_buendel"] = [dict(db.one("SELECT * FROM tasks WHERE id=?", ids[1]))] await engine._ausfuehren(g, t, g.knoten["llm_pruefung"], asyncio.Semaphore(1), asyncio.Semaphore(1), asyncio.Event()) finally: engine.WORKER["guide.llm_pruefung"] = alt for tid in ids: z = db.one("SELECT * FROM tasks WHERE id=?", tid) assert z["status"] == "offen" and z["versuch"] == 1