"""Re-Scan über git-Diff: nur berührte Dateien werden neu gescannt, Löschungen räumen auf, sauberer Tree ist Pflicht. Läuft komplett in der Fake-Welt.""" import shutil import subprocess from pathlib import Path import pytest import fake_agents import kanban import repo as repo_mod MINI = Path(__file__).parent / "fixtures" / "mini_repo" def _git(wurzel, *args): subprocess.run(["git", "-C", str(wurzel), *args], check=True, capture_output=True, env={"PATH": "/usr/bin:/bin", "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t", "HOME": str(wurzel)}) def _repo(tmp_path: Path) -> Path: ziel = tmp_path / "repo" shutil.copytree(MINI, ziel) _git(ziel, "init", "-b", "main") _git(ziel, "add", "-A") _git(ziel, "commit", "-m", "start") return ziel @pytest.fixture def cli(monkeypatch): import cli as cli_mod monkeypatch.setattr(kanban, "RETRY_BACKOFF", 0.05) return cli_mod async def test_scan_dann_rescan_nur_geaenderte(cli, fake_welt, testdb, tmp_path): fake_agents.standard_antworten(fake_welt) wurzel = _repo(tmp_path) assert await cli._scan(wurzel) == 0 stand1 = repo_mod.head_hash(wurzel) assert f"stand: {stand1}" in (wurzel / ".planer" / "einheiten" / "rechner.py.md").read_text() _git(wurzel, "add", "-A") _git(wurzel, "commit", "-m", "scan-artefakte") (wurzel / "rechner.py").write_text((wurzel / "rechner.py").read_text() + "\n\ndef neu():\n return 1\n") _git(wurzel, "add", "-A") _git(wurzel, "commit", "-m", "neue funktion") stand2 = repo_mod.head_hash(wurzel) fake_welt.calls.clear() assert await cli._rescan(wurzel) == 0 # Nur rechner.py wurde neu gescannt … assert all("anzeige" not in k for k in fake_welt.calls if "-schneiden-" in k) rechner = (wurzel / ".planer" / "einheiten" / "rechner.py.md").read_text() anzeige = (wurzel / ".planer" / "einheiten" / "anzeige.py.md").read_text() assert f"stand: {stand2}" in rechner and "## rechner.py::neu" in rechner assert f"stand: {stand1}" in anzeige # unverändert übernommen # … aber die Sichten wurden neu aggregiert und referenzieren auch den Bestand assert any("-sichten-" in k for k in fake_welt.calls) kern = (wurzel / ".planer" / "kern.md").read_text() assert kern.startswith("# Kern:") async def test_rescan_ohne_aenderung_ruft_kein_llm(cli, fake_welt, testdb, tmp_path): fake_agents.standard_antworten(fake_welt) wurzel = _repo(tmp_path) assert await cli._scan(wurzel) == 0 _git(wurzel, "add", "-A") _git(wurzel, "commit", "-m", "scan-artefakte") fake_welt.calls.clear() assert await cli._rescan(wurzel) == 0 assert fake_welt.calls == [] async def test_rescan_geloeschte_datei_raeumt_auf(cli, fake_welt, testdb, tmp_path): fake_agents.standard_antworten(fake_welt) wurzel = _repo(tmp_path) assert await cli._scan(wurzel) == 0 _git(wurzel, "add", "-A") _git(wurzel, "commit", "-m", "scan-artefakte") (wurzel / "anzeige.py").unlink() _git(wurzel, "add", "-A") _git(wurzel, "commit", "-m", "anzeige weg") assert await cli._rescan(wurzel) == 0 assert not (wurzel / ".planer" / "einheiten" / "anzeige.py.md").exists() async def test_unsauberer_tree_bricht_ab(cli, fake_welt, testdb, tmp_path): wurzel = _repo(tmp_path) (wurzel / "schmutz.py").write_text("X = 1\n") with pytest.raises(SystemExit): await cli._scan(wurzel) async def test_kein_git_repo_bricht_ab(cli, fake_welt, testdb, tmp_path): kein_repo = tmp_path / "kein-repo" kein_repo.mkdir() with pytest.raises(SystemExit): await cli._scan(kein_repo)