"""_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, 0.1, "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, 0.1, "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, 0.1, "claude") assert res == ["ok"] assert calls == ["k1"] async def test_hedge_schwelle_skaliert_mit_timeout(monkeypatch): """Die Schwelle ist max(HEDGE_NACH_S, timeout/2): ein gesunder Call, der länger als die Untergrenze, aber kürzer als das halbe Timeout läuft, bekommt KEINEN Zwilling (pauschale 90 s hedgten jeden normalen Fix-Call).""" calls = [] async def fake_agent(key, prompt, timeout, **kw): calls.append(key) await asyncio.sleep(0.15) # > Untergrenze 0.05, < timeout/2 = 0.5 return (0, "ok", "") monkeypatch.setattr(pipeline, "run_agent", fake_agent) monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0.05) res = await pipeline._race("t", "Test", [_slot()], 1, 1.0, "claude") assert res == ["ok"] assert calls == ["k1"] 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, 0.1, "claude") assert res == ["zwilling"] assert calls == ["k1", "k1-h"] # kein dritter Spawn