"""Engine-Kern: atomarer Claim, Zombie-Reset, Idempotenz, Kanten-Validierung.""" import pytest from backend import db, engine, graph from .conftest import topic_anlegen def test_claim_atomar(): topic = topic_anlegen() run_id = db.insert("runs", topic=topic, status="running") tid = db.insert("tasks", run_id=run_id, topic=topic, knoten="suche", item="x") assert db.execute("UPDATE tasks SET status='laufend' WHERE id=? AND " "status='offen'", tid) == 1 # zweiter Claim verliert assert db.execute("UPDATE tasks SET status='laufend' WHERE id=? AND " "status='offen'", tid) == 0 def test_task_unique_idempotent(): topic = topic_anlegen() run_id = db.insert("runs", topic=topic, status="running") a = db.insert("tasks", ignore=True, run_id=run_id, topic=topic, knoten="suche", item="r1:q:abc") b = db.insert("tasks", ignore=True, run_id=run_id, topic=topic, knoten="suche", item="r1:q:abc") assert a is not None and b is None # Runde im Item-Schlüssel: 2. Runde blockiert nicht c = db.insert("tasks", ignore=True, run_id=run_id, topic=topic, knoten="suche", item="r2:q:abc") assert c is not None async def test_zombie_reset_beim_start(): import asyncio topic = topic_anlegen() run_id = db.insert("runs", topic=topic, status="stopped") db.insert("tasks", run_id=run_id, topic=topic, knoten="montage", item="montage", status="laufend") run_id2 = engine.lauf_starten(topic) await asyncio.sleep(0.1) t = db.one("SELECT status, fehler FROM tasks WHERE topic=? AND item='montage'", topic) assert t["status"] != "laufend" or t["fehler"] != "" # Zombie zurückgesetzt engine.lauf_stoppen(topic) with __import__("contextlib").suppress(asyncio.CancelledError): await engine._laeufe[topic] assert run_id2 != run_id def test_kanten_validierung(): g = graph.get() kn = g.knoten["suche"] t = {"id": 1, "run_id": 1, "topic": "x", "knoten": "suche", "item": "i", "runde": 1} erg = engine.Ergebnis(neue_tasks=[{"knoten": "montage", "item": "m"}]) with pytest.raises(graph.GraphFehler): engine._abschluss_schreiben(g, t, kn, erg) async def test_infra_zaehlt_nicht_gegen_versuch(): """LaufPause aus einem Worker setzt den Task zurück auf offen (kein versuch+1) und pausiert den Run.""" import asyncio async def kaputt(task): from backend.llm import LaufPause raise LaufPause("test") topic = topic_anlegen() run_id = db.insert("runs", topic=topic, status="running") tid = db.insert("tasks", run_id=run_id, topic=topic, knoten="suche", item="x", status="laufend", versuch=0) g = graph.get() alt = engine.WORKER["suche.suchen"] engine.WORKER["suche.suchen"] = kaputt try: t = dict(db.one("SELECT * FROM tasks WHERE id=?", tid)) await engine._ausfuehren(g, t, g.knoten["suche"], asyncio.Semaphore(1), asyncio.Semaphore(1), asyncio.Event()) finally: engine.WORKER["suche.suchen"] = alt task = db.one("SELECT * FROM tasks WHERE id=?", tid) assert task["status"] == "offen" assert task["versuch"] == 0 # Infra zählt nicht als Inhalts-Versuch assert db.one("SELECT status FROM runs WHERE id=?", run_id)["status"] == "paused"