"""Rot-Nachweis je Task: die neuen Tests eines Tasks gegen den Produktivcode VOR dem Task, auf Wegwerfkopien. Koordinator-Werkzeug (entstanden 2026-09-18, Trading Plattform). plan_materialisiere.py wendet eine Taskdatei immer ganz an; "erst der Test, dann die Umsetzung" laesst sich damit nicht beobachten. Dieses Skript stellt den Zustand dazwischen her: Basis + Voraussetzungstasks + der Task selbst, danach die PRODUKTIVEN Zieldateien des Tasks zurueck auf den Stand vor dem Task (neue Produktivdateien geloescht). Dann laufen die Testdateien des Tasks. Es tut etwas und berichtet; das Urteil faellt der Koordinator: ROT mindestens ein Fehlschlag oder Sammelfehler -- die erste Fehlerzeile steht dabei, damit man sieht, ob der Test an der gemeinten Stelle faellt und nicht an einem Tippfehler. GRUEN kein Fehlschlag ohne die Umsetzung -- ein Test, der so gruen ist, misst die Umsetzung nicht (ausser der Plan fuehrt ihn ausdruecklich als Regressionsnetz). Geltungsbereich und Luecken: - Testdateien sind Zieldateien unter tests/ sowie *.test.ts und *.test.tsx; alles unter tests/fixtures/ gilt als Testseite und bleibt stehen. Alles andere ist produktiv. - Python-Tests laufen in WSL (Debian, ~/venvs/trading), Oberflaechentests unter Windows mit npx vitest; dafuer setzt das Skript eine Junction auf das node_modules des Projekts und loest sie vor dem Loeschen. - Erzeugte Fixtures (der Plan laesst sie mit altem Code erzeugen) holt --fixturen aus einem Ordner nach, wenn sie nach dem Anwenden fehlen. - Es sieht nicht, WELCHE Tests rot sein muessten; es zaehlt und zeigt die erste Fehlerzeile. - Die Basis ist eine Kopie, die der Aufrufer VOR der ersten Welle gezogen hat; das Skript fasst das Projekt selbst nie an. Aufruf: python rot_nachweis.py [--fixturen ] [--node-modules ] --node-modules nennt das node_modules des Projekts, wenn die Basis-Kopie keines traegt. spec.json: [{"task": "task-141-....md", "vor": ["task-...md", ...]}, ...] """ import json import os import re import shlex import shutil import subprocess import sys from pathlib import Path HIER = Path(__file__).resolve().parent ZIEL = re.compile(r"^- `(.+?)`") PROJEKTNAME = "trading-plattform/" def zieldateien(taskdatei: Path) -> list[str]: """Zieldateien relativ zum Projekt. Absolute Pfade werden hinter dem Projektordner abgeschnitten.""" drin = False ziele: list[str] = [] for zeile in taskdatei.read_text(encoding="utf-8").replace("\r\n", "\n").split("\n"): if zeile.startswith("## "): drin = zeile.startswith("## Zieldateien") continue treffer = ZIEL.match(zeile) if drin else None if treffer: pfad = treffer.group(1).replace("\\", "/").removeprefix("./") if re.match(r"^[A-Za-z]:/", pfad): stelle = pfad.lower().find(PROJEKTNAME) if stelle < 0: raise ValueError(f"{taskdatei.name}: absoluter Pfad ausserhalb des Projekts: {pfad}") pfad = pfad[stelle + len(PROJEKTNAME) :] ziele.append(pfad) return ziele def ist_test(pfad: str) -> bool: return pfad.startswith("tests/") or pfad.endswith((".test.ts", ".test.tsx")) def wende_an(planordner: Path, kopie: Path, tasks: list[str]) -> str: if not tasks: return "" lauf = subprocess.run( [sys.executable, "-X", "utf8", str(HIER / "plan_materialisiere.py"), str(planordner), str(kopie), *tasks], capture_output=True, text=True, encoding="utf-8", errors="replace", ) return "" if lauf.returncode == 0 else (lauf.stdout + lauf.stderr)[-600:] def wsl_pfad(pfad: Path) -> str: text = str(pfad.resolve()).replace("\\", "/") return f"/mnt/{text[0].lower()}{text[2:]}" def loesche(ordner: Path) -> None: junction = ordner / "dashboard" / "node_modules" if junction.exists(): os.rmdir(junction) # loest nur die Verknuepfung, nie das Ziel shutil.rmtree(ordner, ignore_errors=True) def erste_fehlerzeile(ausgabe: str) -> str: for zeile in ausgabe.splitlines(): kern = zeile.strip() if kern.startswith(("E ", "FAILED", "ERROR", "AssertionError", "Error:", "TypeError", "ReferenceError")) or " FAIL " in zeile: return kern[:220] return "" def main() -> int: argumente = sys.argv[1:] fixturen = None if "--fixturen" in argumente: stelle = argumente.index("--fixturen") fixturen = Path(argumente[stelle + 1]) del argumente[stelle : stelle + 2] node_modules_arg = None if "--node-modules" in argumente: stelle = argumente.index("--node-modules") node_modules_arg = Path(argumente[stelle + 1]) del argumente[stelle : stelle + 2] if len(argumente) != 4: print(__doc__) return 3 basis, planordner, arbeit, spec_pfad = (Path(a) for a in argumente) if not basis.is_dir() or not planordner.is_dir() or not spec_pfad.is_file(): print("VORAUSSETZUNG FEHLT: Basis-Kopie, Planordner oder Spec") return 3 arbeit.mkdir(parents=True, exist_ok=True) node_modules = None for kandidat in (node_modules_arg, basis / "dashboard" / "node_modules"): if kandidat is not None and kandidat.exists(): node_modules = kandidat break ergebnisse = [] for eintrag in json.loads(spec_pfad.read_text(encoding="utf-8")): task, voraus = eintrag["task"], eintrag.get("vor", []) name = task.split("-")[1] if "-" in task else task vor, rot = arbeit / f"{name}_vor", arbeit / f"{name}_rot" for ordner in (vor, rot): loesche(ordner) shutil.copytree(basis, vor, ignore=shutil.ignore_patterns("node_modules", "__pycache__")) fehler = wende_an(planordner, vor, voraus) shutil.copytree(vor, rot) fehler = fehler or wende_an(planordner, rot, [task]) if fehler: ergebnisse.append((task, "ANWENDEN GESCHEITERT", fehler.replace("\n", " | "))) print(f"{task}: ANWENDEN GESCHEITERT: {fehler[-300:]}") continue ziele = zieldateien(planordner / task) tests = [z for z in ziele if ist_test(z) and not z.startswith("tests/fixtures/")] for ziel in ziele: if ist_test(ziel): if not (rot / ziel).exists() and fixturen is not None and (fixturen / Path(ziel).name).is_file(): (rot / ziel).parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(fixturen / Path(ziel).name, rot / ziel) continue if (vor / ziel).is_file(): shutil.copyfile(vor / ziel, rot / ziel) elif (rot / ziel).is_file(): (rot / ziel).unlink() python_tests = [t for t in tests if t.endswith(".py")] ober_tests = [t for t in tests if not t.endswith(".py")] zeilen = [] if python_tests: # shlex.quote: Ein Arbeitsordner mit Leerzeichen (Benutzername im Temp-Pfad) liess das cd # scheitern; jeder Task stand dann als "ROT pytest: keine Summenzeile" da, ohne dass ein Test # lief (gefunden 2026-09-20). befehl = f"cd {shlex.quote(wsl_pfad(rot))} && ~/venvs/trading/bin/python -m pytest {' '.join(python_tests)} -q -p no:cacheprovider 2>&1 | tail -60" lauf = subprocess.run(["wsl.exe", "-d", "Debian", "--", "bash", "-lc", befehl], capture_output=True, text=True, encoding="utf-8", errors="replace") ausgabe = lauf.stdout summe = next((z.strip() for z in reversed(ausgabe.splitlines()) if re.search(r"\d+ (passed|failed|error)", z)), "keine Summenzeile") # Gruen ist nur ein Lauf mit Summenzeile und ohne Fehlschlag. Ohne Summenzeile hat pytest gar # nicht gesammelt (etwa ein ImportError in tests/conftest.py) -- das ist rot, nicht gruen. # Gefunden am 2026-09-18: Task 172 stand als GRUEN da, obwohl kein einziger Test lief. rot_ja = bool(re.search(r"\d+ (failed|error)", summe)) or summe == "keine Summenzeile" zeilen.append(("ROT " if rot_ja else "GRUEN") + f" pytest: {summe} | {erste_fehlerzeile(ausgabe)}") if ober_tests: if node_modules is None: zeilen.append("VORAUSSETZUNG FEHLT: node_modules in der Basis-Kopie") else: import _winapi _winapi.CreateJunction(str(node_modules.resolve()), str(rot / "dashboard" / "node_modules")) relativ = [t.removeprefix("dashboard/") for t in ober_tests] lauf = subprocess.run(["npx.cmd", "vitest", "run", *relativ], cwd=rot / "dashboard", capture_output=True, text=True, encoding="utf-8", errors="replace") ausgabe = re.sub(r"\x1b\[[0-9;]*m", "", lauf.stdout + lauf.stderr) summe = next((z.strip() for z in ausgabe.splitlines() if z.strip().startswith("Tests ")), "") if not summe: # Ohne Summenzeile ist vitest nicht bis zum Ende gekommen (Importfehler, Startfehler): # das Ende der Ausgabe zeigen, statt "rot" zu behaupten, ohne zu wissen woran. summe = "keine Summenzeile; Ende der Ausgabe: " + " / ".join(z.strip() for z in ausgabe.splitlines()[-6:] if z.strip())[:300] rot_ja = "failed" in summe or lauf.returncode != 0 zeilen.append(("ROT " if rot_ja else "GRUEN") + f" vitest: {summe} | {erste_fehlerzeile(ausgabe)}") if not tests: zeilen.append("ohne Testdatei unter den Zieldateien") for zeile in zeilen: print(f"{task}: {zeile}") ergebnisse.append((task, zeile[:5].strip(), zeile)) for ordner in (vor, rot): loesche(ordner) gruen = [e for e in ergebnisse if e[1] == "GRUEN"] print(f"\n{len(ergebnisse)} Laeufe, davon {len(gruen)} gruen ohne Umsetzung.") return 0 if __name__ == "__main__": try: sys.exit(main()) except Exception as ausnahme: # noqa: BLE001 - ein Absturz des Werkzeugs ist selbst ein Befund print(f"BEFUND: das Werkzeug ist abgebrochen: {ausnahme!r}") sys.exit(2)