139 lines
5.2 KiB
Python
139 lines
5.2 KiB
Python
"""_race-Hedging: Stall-Slots bekommen einen parallelen Zwilling statt den Timeout-Cap
|
||
abzuwarten (gemessen: 4 Panel-Stalls à 160–230 s pro Lauf auf dem kritischen Pfad)."""
|
||
|
||
import asyncio
|
||
|
||
import pipeline
|
||
|
||
|
||
def _slot(payload=lambda r: r[1]):
|
||
return {"key": "k1", "prompt": "p", "role": "judge", "capabilities": "none", "payload": payload}
|
||
|
||
|
||
async def test_hedge_zwilling_rettet_stall(monkeypatch):
|
||
"""Original stallt → nach HEDGE_NACH_S startet der Zwilling (key -h), sein Ergebnis
|
||
gewinnt, das hängende Original wird gekillt."""
|
||
calls, killed = [], []
|
||
|
||
async def fake_agent(key, prompt, timeout, **kw):
|
||
calls.append(key)
|
||
if key.endswith("-h"):
|
||
return (0, "zwilling", "")
|
||
await asyncio.sleep(30) # Stall — würde sonst den ganzen Cap verbrennen
|
||
return (0, "original", "")
|
||
|
||
monkeypatch.setattr(pipeline, "run_agent", fake_agent)
|
||
monkeypatch.setattr(pipeline, "kill_process", lambda k: killed.append(k))
|
||
monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0.05)
|
||
res = await pipeline._race("t", "Test", [_slot()], 1, 60, "claude")
|
||
assert res == ["zwilling"]
|
||
assert calls == ["k1", "k1-h"]
|
||
assert "k1" in killed # das hängende Original läuft nicht weiter
|
||
|
||
|
||
async def test_hedge_original_gewinnt_zwilling_wird_gekillt(monkeypatch):
|
||
"""Kommt das Original doch noch vor dem Zwilling an, wird der Zwilling gekillt
|
||
und sein spätes Ergebnis nicht gewertet."""
|
||
killed = []
|
||
|
||
async def fake_agent(key, prompt, timeout, **kw):
|
||
await asyncio.sleep(0.3 if key.endswith("-h") else 0.15)
|
||
return (0, key, "")
|
||
|
||
monkeypatch.setattr(pipeline, "run_agent", fake_agent)
|
||
monkeypatch.setattr(pipeline, "kill_process", lambda k: killed.append(k))
|
||
monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0.05)
|
||
res = await pipeline._race("t", "Test", [_slot()], 1, 60, "claude")
|
||
assert res == ["k1"]
|
||
assert "k1-h" in killed
|
||
|
||
|
||
async def test_hedge_aus_bei_null(monkeypatch):
|
||
"""HEDGE_NACH_S=0 → kein Zwilling, Verhalten wie zuvor."""
|
||
calls = []
|
||
|
||
async def fake_agent(key, prompt, timeout, **kw):
|
||
calls.append(key)
|
||
await asyncio.sleep(0.1)
|
||
return (0, "ok", "")
|
||
|
||
monkeypatch.setattr(pipeline, "run_agent", fake_agent)
|
||
monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0)
|
||
res = await pipeline._race("t", "Test", [_slot()], 1, 60, "claude")
|
||
assert res == ["ok"]
|
||
assert calls == ["k1"]
|
||
|
||
|
||
async def test_late_fold_nachzuegler_zaehlt_nach(monkeypatch):
|
||
"""Quorum 2 kehrt sofort zurück; der dritte Slot wird nicht gekillt, sein Ergebnis
|
||
geht an `late` (ersetzt den grace-Timer der Finder-Runden)."""
|
||
import time
|
||
killed, spaet = [], []
|
||
|
||
async def fake_agent(key, prompt, timeout, **kw):
|
||
if key == "k3":
|
||
await asyncio.sleep(0.2)
|
||
return (0, "dritter", "")
|
||
return (0, key, "")
|
||
|
||
async def late(val):
|
||
spaet.append(val)
|
||
|
||
monkeypatch.setattr(pipeline, "run_agent", fake_agent)
|
||
monkeypatch.setattr(pipeline, "kill_process", lambda k: killed.append(k))
|
||
monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0)
|
||
slots = [{"key": f"k{i}", "prompt": "p", "role": "quick", "capabilities": "none",
|
||
"payload": lambda r: r[1]} for i in (1, 2, 3)]
|
||
t0 = time.monotonic()
|
||
res = await pipeline._race("t", "Test", slots, 2, 60, "claude", late=late)
|
||
assert time.monotonic() - t0 < 0.15 # kein Warten auf k3
|
||
assert sorted(res) == ["k1", "k2"]
|
||
assert "k3" not in killed
|
||
await asyncio.sleep(0.3)
|
||
assert spaet == ["dritter"]
|
||
|
||
|
||
async def test_late_fold_invalider_nachzuegler_ignoriert(monkeypatch):
|
||
"""Nachzügler mit invalidem Payload löst late NICHT aus (best-effort)."""
|
||
spaet = []
|
||
|
||
async def fake_agent(key, prompt, timeout, **kw):
|
||
if key == "k3":
|
||
await asyncio.sleep(0.1)
|
||
return (1, "", "kaputt")
|
||
return (0, key, "")
|
||
|
||
async def late(val):
|
||
spaet.append(val)
|
||
|
||
monkeypatch.setattr(pipeline, "run_agent", fake_agent)
|
||
monkeypatch.setattr(pipeline, "kill_process", lambda k: None)
|
||
monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0)
|
||
slots = [{"key": f"k{i}", "prompt": "p", "role": "quick", "capabilities": "none",
|
||
"payload": lambda r: r[1]} for i in (1, 2, 3)]
|
||
res = await pipeline._race("t", "Test", slots, 2, 60, "claude", late=late)
|
||
assert res is not None
|
||
await asyncio.sleep(0.25)
|
||
assert spaet == []
|
||
|
||
|
||
async def test_hedge_zwilling_ersetzt_restart(monkeypatch):
|
||
"""Scheitert das Original, während der Zwilling noch läuft, gibt es KEINEN
|
||
zusätzlichen Restart — der Zwilling ist der Retry."""
|
||
calls = []
|
||
|
||
async def fake_agent(key, prompt, timeout, **kw):
|
||
calls.append(key)
|
||
if key.endswith("-h"):
|
||
await asyncio.sleep(0.2)
|
||
return (0, "zwilling", "")
|
||
await asyncio.sleep(0.1)
|
||
return (1, "", "kaputt") # Fehler NACH dem Hedge-Start
|
||
|
||
monkeypatch.setattr(pipeline, "run_agent", fake_agent)
|
||
monkeypatch.setattr(pipeline, "kill_process", lambda k: None)
|
||
monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0.05)
|
||
res = await pipeline._race("t", "Test", [_slot()], 1, 60, "claude")
|
||
assert res == ["zwilling"]
|
||
assert calls == ["k1", "k1-h"] # kein dritter Spawn
|