"""Fake-E2E: kompletter Scan-Lauf (Chunker → Board → .planer/) in Sekunden, ohne LLM. Die Fake-Antworten durchlaufen die ECHTEN Gates — getestet wird der volle Pfad inkl. Nachfass, Retry, Dead-Letter und Infra-Pause.""" import asyncio import shutil from pathlib import Path import fake_agents import kanban import scan_board MINI = Path(__file__).parent / "fixtures" / "mini_repo" def _repo(tmp_path: Path) -> Path: ziel = tmp_path / "repo" shutil.copytree(MINI, ziel) return ziel async def _scan(repo: Path, monkeypatch) -> dict: monkeypatch.setattr(kanban, "RETRY_BACKOFF", 0.05) return await asyncio.wait_for( scan_board.run_scan(repo, "mini", stand="deadbeef"), timeout=30) async def test_e2e_voller_scan(fake_welt, testdb, tmp_path, monkeypatch): fake_agents.standard_antworten(fake_welt) repo = _repo(tmp_path) ergebnis = await _scan(repo, monkeypatch) assert ergebnis["status"] == "ok", ergebnis planer = repo / ".planer" assert (planer / "einheiten" / "rechner.py.md").is_file() assert (planer / "einheiten" / "anzeige.py.md").is_file() assert (planer / "kern.md").read_text().startswith("# Kern: mini") assert (planer / "architektur.md").is_file() assert (planer / "flows.md").is_file() assert (planer / "pruefung.md").is_file() assert list((planer / "features").glob("*.md")) einheiten = (planer / "einheiten" / "rechner.py.md").read_text() assert "stand: deadbeef" in einheiten assert "## rechner.py::addiere" in einheiten and "## rechner.py::Speicher" in einheiten async def test_e2e_nachfass_pfad(fake_welt, testdb, tmp_path, monkeypatch): """Erst-Scan lässt `skaliere` aus → Abdeckungs-Gate schickt den Chunk in den Nachfass.""" fake_agents.standard_antworten(fake_welt, lasse_aus={"skaliere"}) repo = _repo(tmp_path) ergebnis = await _scan(repo, monkeypatch) assert ergebnis["status"] == "ok", ergebnis assert any("-nachfass-" in k for k in fake_welt.calls) assert "## rechner.py::skaliere" in (repo / ".planer" / "einheiten" / "rechner.py.md").read_text() async def test_e2e_stoerung_retry(fake_welt, testdb, tmp_path, monkeypatch): """Einmal Garbage → Gates lehnen ab → Engine-Backoff → zweiter Versuch gelingt.""" fake_agents.standard_antworten(fake_welt) fake_welt.stoerungen.append({"muster": r"-schneiden-chunk-0000", "modus": "garbage", "mal": 1}) repo = _repo(tmp_path) ergebnis = await _scan(repo, monkeypatch) assert ergebnis["status"] == "ok", ergebnis async def test_e2e_dauerfehler_dead(fake_welt, testdb, tmp_path, monkeypatch): """Dauerhafter Fehler auf einem Chunk → Karte landet im Dead-Letter, Lauf endet.""" fake_agents.standard_antworten(fake_welt) fake_welt.stoerungen.append({"muster": r"-schneiden-chunk-0000", "modus": "fehler", "mal": 99}) repo = _repo(tmp_path) ergebnis = await _scan(repo, monkeypatch) assert ergebnis["status"] == "fehler" assert ergebnis["dead"] == ["chunk-0000"] async def test_e2e_infra_pause(fake_welt, testdb, tmp_path, monkeypatch): """AgentInfraError → Lauf pausiert sofort (kein Dead-Letter, kein Raten).""" fake_agents.standard_antworten(fake_welt) fake_welt.stoerungen.append({"muster": r"-schneiden-", "modus": "infra", "mal": 1}) repo = _repo(tmp_path) ergebnis = await _scan(repo, monkeypatch) assert ergebnis["status"] == "pausiert" assert "fake-infra" in ergebnis["infra_error"] async def test_e2e_fortsetzen_eine_sichten_runde(fake_welt, testdb, tmp_path, monkeypatch): """Fortsetzung mit Mix aus toten und fertigen Chunks: die Sammeln-Barriere darf erst öffnen, wenn der Producer ALLE Karten requeued hat — genau eine Sichten-Runde, und die Sichten sehen die volle Übersicht (Abnahme-Befund: Teil-Übersicht durch frühes Drainen).""" fake_agents.standard_antworten(fake_welt) repo = _repo(tmp_path) ergebnis = await _scan(repo, monkeypatch) assert ergebnis["status"] == "ok" # Zustand eines abgebrochenen Laufs nachstellen: ein Chunk tot, Rest fertig karten = await testdb.kanban_cards("mini", scan_board.BOARD, kind="chunk") tot = karten[0]["card_id"] await testdb.kanban_upsert_card("mini", scan_board.BOARD, tot, "chunk", "dead", None) fake_welt.calls.clear() ergebnis2 = await asyncio.wait_for( scan_board.run_scan(repo, "mini", stand="deadbeef", fortsetzen=True), timeout=30) assert ergebnis2["status"] == "ok", ergebnis2 sichten_calls = [k for k in fake_welt.calls if "-sichten-" in k] assert len(sichten_calls) == 3 # genau EINE Runde, kein Doppel-Drain features = (repo / ".planer" / "features" / "fake-bereich.md").read_text() assert "rechner.py::addiere" in features and "anzeige.py::zeige" in features # volle Übersicht async def test_e2e_events_geschrieben(fake_welt, testdb, tmp_path, monkeypatch): """Telemetrie: Agent-Events mit run_id landen in der DB.""" import agents import database monkeypatch.setattr(agents, "on_event", database.add_event) fake_agents.standard_antworten(fake_welt) repo = _repo(tmp_path) ergebnis = await _scan(repo, monkeypatch) assert ergebnis["status"] == "ok" db = await testdb.get_db() cursor = await db.execute( "SELECT count(*) FROM events WHERE topic='mini' AND kind='agent' AND run_id LIKE 'scan-%'") assert (await cursor.fetchone())[0] >= 5 # 2 Chunks + 3 Sichten