init
This commit is contained in:
119
backend/tests/test_scan_fake.py
Normal file
119
backend/tests/test_scan_fake.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user