120 lines
5.3 KiB
Python
120 lines
5.3 KiB
Python
"""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
|