Files
claude-regelwerk/.vault-tools/plan-kopie/docs/plans/2026-10-06-pytest-konfiguration-nachbesserung-1.md
Pascal MichelandClaude Opus 5.5 ff7854cf39 Mutantenlaeufer: pytest-Konfiguration wie pytest, nie oberhalb der Wurzel; Abbruch von pytest mit Ursache statt FEHLT; Gleichwertigkeitsprobe, Treiber 46 Faelle; zwei Pruefrunden
Anlass: Plankritik 2 von C1 (After Install), fremde pyproject.toml im Windows-Temp.
Gegenprobe an sechs mutanten.toml im Vault: gleiche Ergebnisse, C1 unter Windows-Temp jetzt 69 OK statt 101 FEHLT.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 17:41:05 +02:00

1024 lines
57 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: plan
title: "Mutantenläufer: Nachbesserung 1 nach der Prüfung vom 2026-10-06"
status: active
summary: "Delta zum Plan 2026-10-06-pytest-konfiguration: Wurzel über realpath, Abschnittszeile wie iniconfig, kein Absturz bei überlangem Knoten, Ursache vorn in der Mutantenzeile, Kopfzeile je Interpreter; neue Gleichwertigkeitsprobe gegen pytest und Treiber-Fälle 41 bis 45; Entwurf und LIESMICH nachgezogen."
tags: [plan, vault-tools, pruefwerkzeuge]
created: 2026-10-06
updated: 2026-10-06
modell: sonnet
effort: low
modell_begruendung: "Jeder Block steht wörtlich da, Zieldateien und Verbote sind genannt; Entwurf und LIESMICH tragen typografische Anführungszeichen, Gedankenstriche und Auslassungspunkte, die Haiku still ersetzt."
---
Übersicht: [LIESMICH der Planwerkzeuge](../../LIESMICH.md) · Vorgänger: [Plan 2026-10-06-pytest-konfiguration](2026-10-06-pytest-konfiguration.md)
# Mutantenläufer: Nachbesserung 1
## Warum so
Ein `pruefer` hat den umgesetzten Plan am 2026-10-06 geprüft (Treiber selbst gefahren, fünf eigene Varianten, Gleichwertigkeitsprobe über 25 Aufbauten gegen `determine_setup` und echte `pytest --co`-Läufe, Gegenprobe an drei Projekten). Seine Befunde und was dieser Plan daraus macht:
| Befund | Schwere | Hier |
|---|---|---|
| Wurzel über einen Symlink: `os.path.abspath` löst nicht auf, pytest rechnet aber vom aufgelösten Arbeitsordner aus (`main.py:828`); mit `-c <symlinkpfad>/…` wurden Knoten zu `::test_…`, die Grundlinie zu `FEHLT` | wichtig | `os.path.realpath`; Treiber-Fall 41 |
| Neuer Code ohne eigenen Fall: Rückgabewert 3, `setup.py`, zweite Suche, Konfiguration im Unterordner, Abbruch in nur einer Interpretergruppe an zwei Stellen, Symlink, Kopfzeile | wichtig | Fälle 41 bis 45, Probe `mutanten_lauf_probe_konfig.py` als Fall 43, Kopfzeile in 36 und 37 |
| Ursache des Abbruchs in der Mutantenzeile abgeschnitten (120 Zeichen, Präfix und Interpreterpfad zuerst) | klein | Kurzfassung mit der Ursache vorn; Fall 40 prüft die Zeile selbst |
| Absturz bei Dateinamen über 255 Zeichen (`OSError` aus `Path.exists`) | klein | `_existiert` wie `safe_exists`; Fall 45 |
| `tox.ini` mit `[pytest] # Kommentar` gilt bei pytest, beim Läufer nicht; Satz zu kaputten Dateien stimmt nur für `pyproject.toml` | klein | Abschnittszeile wie `_parseline` in iniconfig 2.3.0 (`[` in Spalte 0, Kommentare nach `#` und `;` entfernt, `]` am Ende); Entwurf berichtigt |
| pytest meldet bei einer nicht sammelbaren Datei `found no collectors for`, nicht „not found“ | klein | Kopfkommentar und Entwurf |
| Entwurf Schritt 3 und „Auswertung je Test“ widersprechen dem Abbruch-Absatz | klein | Ausnahme dort genannt |
| Kopfzeile kann bei mehreren Interpretern eine nicht benutzte Konfiguration nennen | klein | eine Kopfzeile je Interpreter |
| `config.inipath` ist ohne eigene Konfiguration die leere Datei; `paths`-Werte aus `-o`/`PYTEST_ADDOPTS` gelten dann relativ zum Plugin-Ordner | klein | als benannte Lücke im Kopfkommentar und Entwurf |
- **Quelle der Abschnittszeile:** iniconfig 2.3.0 im WSL-Venv `trading`, `iniconfig/_parse.py`, `_parseline` (Zeilen 102 bis 127), nachgelesen am 2026-10-06 (`COMMENTCHARS = "#;"`, Abschnitt nur bei `line[0] == "["`, Kommentare per `split`, Name `line[1:-1]`).
- **Die Probe des Prüfers** hatte im `conftest.py`-Text ein `'\n'` innerhalb einer Zeichenkette; im geschriebenen `conftest.py` wurde daraus ein echter Zeilenumbruch mitten im Literal, die Datei war nicht ladbar. Die neue Probe schreibt den Zeilenumbruch mit `chr(10)`.
- **Gemessen vom Koordinator am 2026-10-06** (Rohdaten `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/07-*`): neuer Stand 45 von 45; der Stand vor dieser Nachbesserung rot bei 40, 41, 43 und 45; Varianten rot genau an ihrem Fall (ohne Rückgabewert 3 → 42; ohne `setup.py`-Zweig → 43 mit `setup_py_unterordner`; ohne zweite Suche → 43 mit drei Aufbauten; nach dem ersten Abbruch keine weitere Gruppe → 44 an erster Stelle).
## Zieldateien
Pfade relativ zu `D:/Claude/.vault-tools/plan-kopie/`:
- `mutanten_lauf.py` (geändert)
- `mutanten_lauf_treiber.py` (geändert)
- `mutanten_lauf_probe_konfig.py` (neu)
- `mutanten_lauf_ENTWURF.md` (geändert)
- `LIESMICH.md` (geändert)
## Verbote
- Nur die fünf Zieldateien anlegen oder ändern. `mutanten_lauf_kopie.sh`, `rot_nachweis.py`, die Planwerkzeuge und `.claude/workflows/*` bleiben unberührt.
- Kein `git add`, kein `git commit`.
- Jeden Block byte-genau übernehmen: keine Umformulierung, keine Ersetzung typografischer Zeichen, keine zusätzlichen Leerzeilen. Aus-Block als `old_string`, wird-Block als `new_string` im Edit-Werkzeug; die neue Datei mit dem Write-Werkzeug.
- Steht ein Aus-Block nicht genau einmal in seiner Datei, oder weicht eine Ausgabe von der Erwartung ab, hältst du an und meldest es. Nichts still zurechtbiegen.
- `python -m py_compile` legt `__pycache__` an; lösch den Ordner danach wieder.
## Schritt 1 — Tests zuerst: Treiber und Probe
Wende alle Blöcke dieses Abschnitts an.
### `mutanten_lauf_treiber.py`, Stelle 1 von 8
Aus:
```python
#!/usr/bin/env python3
"""Treiber fuer mutanten_lauf.py: 40 Angriffsfaelle auf einer Wegwerfkopie.
```
wird:
```python
#!/usr/bin/env python3
"""Treiber fuer mutanten_lauf.py: 45 Angriffsfaelle auf einer Wegwerfkopie.
```
### `mutanten_lauf_treiber.py`, Stelle 2 von 8
Aus:
```python
Dazu ein Bedienfehler von pytest in der Grundlinie und unter einem Mutanten.
"""
```
wird:
```python
Dazu ein Bedienfehler von pytest in der Grundlinie und unter einem Mutanten.
Faelle 41 bis 45 (Nachbesserung nach der Pruefung vom 2026-10-06): Wurzel ueber
einen Symlink, Rueckgabewert 3, die Gleichwertigkeitsprobe gegen pytest selbst
(mutanten_lauf_probe_konfig.py), ein Abbruch in nur einer von zwei
Interpretergruppen an erster und an letzter Stelle, ein ueberlanger Knoten.
"""
```
### `mutanten_lauf_treiber.py`, Stelle 3 von 8
Aus:
```python
LAEUFER = HIER / "mutanten_lauf.py"
INTERPRETER = "/home/claude/venvs/trading/bin/python"
```
wird:
```python
LAEUFER = HIER / "mutanten_lauf.py"
PROBE = HIER / "mutanten_lauf_probe_konfig.py"
INTERPRETER = "/home/claude/venvs/trading/bin/python"
```
### `mutanten_lauf_treiber.py`, Stelle 4 von 8
Aus:
```python
ergebnis = starte_laeufer(wurzel, toml)
ok = ergebnis.returncode == 0 and "D1 OK" in ergebnis.stdout and "D2 OK" in ergebnis.stdout
melde("Fall 36", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr))
```
wird:
```python
ergebnis = starte_laeufer(wurzel, toml)
ok = (
ergebnis.returncode == 0
and "D1 OK" in ergebnis.stdout
and "D2 OK" in ergebnis.stdout
and "keine in der Wurzel, leere Datei" in ergebnis.stdout
)
melde("Fall 36", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr))
```
### `mutanten_lauf_treiber.py`, Stelle 5 von 8
Aus:
```python
and "D2 OK" in sauber.stdout
and abgewaehlt.returncode == 2
```
wird:
```python
and "D2 OK" in sauber.stdout
and "k/pyproject.toml (aus der Wurzel)" in sauber.stdout
and abgewaehlt.returncode == 2
```
### `mutanten_lauf_treiber.py`, Stelle 6 von 8
Aus:
```python
ergebnis = starte_laeufer(wurzel, toml)
ok = (
ergebnis.returncode == 1
and "M40 UNKLAR" in ergebnis.stdout
and "pytest-Bedienfehler (Rueckgabewert 4)" in ergebnis.stdout
)
```
wird:
```python
ergebnis = starte_laeufer(wurzel, toml)
mutantenzeile = [z for z in ergebnis.stdout.split("\n") if z.startswith("M40 ")]
ok = (
ergebnis.returncode == 1
and len(mutantenzeile) == 1
and "M40 UNKLAR" in mutantenzeile[0]
and "pytest-Bedienfehler (Rueckgabewert 4)" in mutantenzeile[0]
and "--gibt-es-nicht" in mutantenzeile[0]
)
```
### `mutanten_lauf_treiber.py`, Stelle 7 von 8
Aus:
```python
FAELLE = [
```
wird:
```python
def fall_41(basis, projektpfad):
rahmen = Path(tempfile.mkdtemp(prefix="mutanten_lauf_treiber_symlink_"))
try:
dateien = baue_projekt(rahmen / "echt" / "k")
os.symlink(rahmen / "echt", rahmen / "link")
wurzel = rahmen / "link" / "k"
snap = snapshot(dateien)
toml = rahmen / "mutanten.toml"
schreibe_toml(toml, mutant_d1_d2())
ergebnis = starte_laeufer(wurzel, toml)
ok = ergebnis.returncode == 0 and "D1 OK" in ergebnis.stdout and "D2 OK" in ergebnis.stdout
melde("Fall 41", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr))
pruefe_unveraendert(dateien, snap, "Fall 41")
finally:
shutil.rmtree(rahmen, ignore_errors=True)
def fall_42(basis, projektpfad):
rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False)
try:
(wurzel / "conftest.py").write_text(
"def pytest_collection_modifyitems(items):\n"
" raise RuntimeError(\"interner Fehler fuer Fall 42\")\n",
encoding="utf-8",
)
dateien = dateien + [wurzel / "conftest.py"]
snap = snapshot(dateien)
toml = rahmen / "mutanten.toml"
schreibe_toml(toml, mutant_d1_d2())
ergebnis = starte_laeufer(wurzel, toml)
ausgabe = ergebnis.stdout + ergebnis.stderr
ok = (
ergebnis.returncode == 2
and "pytest-Abbruch mit internem Fehler (Rueckgabewert 3)" in ausgabe
and "interner Fehler fuer Fall 42" in ausgabe
and "ist FEHLT" not in ausgabe
)
melde("Fall 42", ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe))
pruefe_unveraendert(dateien, snap, "Fall 42")
finally:
shutil.rmtree(rahmen, ignore_errors=True)
def fall_43(basis, projektpfad):
arbeit = Path(tempfile.mkdtemp(prefix="mutanten_lauf_treiber_probe_"))
try:
ergebnis = subprocess.run(
[INTERPRETER, str(PROBE), str(LAEUFER), str(arbeit)],
capture_output=True, text=True, timeout=900,
)
ok = ergebnis.returncode == 0 and ", abweichungen: 0" in ergebnis.stdout
melde("Fall 43", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr))
finally:
shutil.rmtree(arbeit, ignore_errors=True)
def fall_44(basis, projektpfad):
zweit_dir = Path(tempfile.mkdtemp(prefix="mutanten_lauf_fall44_"))
venv_link = zweit_dir / "venv_zwei"
zweit_python = venv_link / "bin" / "python"
try:
os.symlink(Path(INTERPRETER).parent.parent, venv_link)
toml = projektpfad / "mutanten.toml"
zusatz = "\n[python_je_datei]\n\"tests/test_zeit.py\" = \"{0}\"\n".format(zweit_python)
befund_zwei = "Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit {0};".format(zweit_python)
befund_eins = "Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit {0};".format(INTERPRETER)
reihenfolgen = [
["tests/test_zeit.py::test_gibt_es_nicht", "tests/test_modul.py::test_f1_positiv"],
["tests/test_modul.py::test_f1_positiv", "tests/test_zeit.py::test_gibt_es_nicht"],
]
for stelle, rot_bei in zip(("erste", "letzte"), reihenfolgen):
schreibe_toml(toml, kopf(zusatz=zusatz) + mutant_luecke("M44", rot_bei))
ergebnis = starte_laeufer(basis, toml)
ausgabe = ergebnis.stdout + ergebnis.stderr
ok = (
ergebnis.returncode == 2
and ausgabe.count(befund_zwei) == 1
and befund_eins not in ausgabe
and "ist FEHLT" not in ausgabe
and "test_f1_positiv ist" not in ausgabe
)
melde("Fall 44 ({0} Stelle)".format(stelle), ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe))
finally:
shutil.rmtree(zweit_dir, ignore_errors=True)
def fall_45(basis, projektpfad):
toml = projektpfad / "mutanten.toml"
schreibe_toml(toml, kopf() + mutant_luecke("M45", ["tests/" + "a" * 300 + ".py::test_x"]))
ergebnis = starte_laeufer(basis, toml)
ausgabe = ergebnis.stdout + ergebnis.stderr
ok = (
ergebnis.returncode == 2
and "Grundlinie: pytest-" in ausgabe
and "unerwarteter Abbruch" not in ausgabe
)
melde("Fall 45", ok, "exit={0}\n{1}".format(ergebnis.returncode, ausgabe[-2000:]))
FAELLE = [
```
### `mutanten_lauf_treiber.py`, Stelle 8 von 8
Aus:
```python
fall_31, fall_32, fall_33, fall_34, fall_35, fall_36, fall_37, fall_38, fall_39, fall_40,
]
```
wird:
```python
fall_31, fall_32, fall_33, fall_34, fall_35, fall_36, fall_37, fall_38, fall_39, fall_40,
fall_41, fall_42, fall_43, fall_44, fall_45,
]
```
### `mutanten_lauf_probe_konfig.py`
Die ganze Datei ist neu.
```python
#!/usr/bin/env python3
"""Gleichwertigkeitsprobe fuer die Konfigurationssuche von mutanten_lauf.py.
Zweck: Je Aufbau zwei Vergleiche. Erstens pytest_konfig_bestimmen aus
mutanten_lauf.py gegen determine_setup aus pytest selbst (Konfigurationsdatei
und rootdir). Zweitens ein echtes "pytest --co" ohne -c in einem sauberen
Rahmen gegen einen Lauf mit den Argumenten des Laeufers in einem Rahmen, ueber
dem eine fremde pyproject.toml, ein fremdes conftest.py und ein fremdes
setup.py liegen. Verglichen werden Rueckgabewert, Knotennamen und die Liste
der geladenen conftest.py-Dateien.
Aufruf (in WSL, mit dem Python eines Venvs, in dem pytest liegt):
python mutanten_lauf_probe_konfig.py <mutanten_lauf.py> <arbeitsordner>
Exit 0 ohne Abweichung, 1 mit Abweichung, 2 bei falschem Aufruf.
Geltungsbereich: ein Fangnetz fuer die Aufbauten unten, kein Nachweis fuer
jeden denkbaren Baum. Ein Knoten mit ".." fehlt, weil mutanten_lauf.py ihn
schon beim Lesen der mutanten.toml abweist. Eine Wurzel ueber einen Symlink
prueft Treiber-Fall 41.
Grundlage: Probe des Pruefers vom 2026-10-06 (25 Aufbauten). Das conftest.py
der Probe schreibt den Zeilenumbruch hier mit chr(10), damit kein Backslash
durch die Zeichenkette muss.
"""
import importlib.util
import os
import subprocess
import sys
from pathlib import Path
TEST = "def test_a():\n pass\n\n\ndef test_b():\n pass\n"
CONFTEST = "import os\nopen(os.environ['CFLOG'], 'a').write(__file__ + chr(10))\n"
INI = "[pytest]\n"
AUFBAUTEN = {
"wurzel_pytest_ini": ({"pytest.ini": INI, "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
"unterordner_ini": (
{"tests/pytest.ini": INI, "tests/test_a.py": TEST, "conftest.py": CONFTEST, "tests/conftest.py": CONFTEST},
["tests/test_a.py::test_a"],
),
"zwei_ordner_ini_im_ersten": (
{"tests/a/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST},
["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"],
),
"zwei_ordner_ini_im_zweiten": (
{"tests/b/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST},
["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"],
),
"setup_py_wurzel": ({"setup.py": "", "tests/test_a.py": TEST, "conftest.py": CONFTEST}, ["tests/test_a.py::test_a"]),
"setup_py_unterordner": (
{"sub/setup.py": "", "sub/tests/test_a.py": TEST, "conftest.py": CONFTEST, "sub/conftest.py": CONFTEST},
["sub/tests/test_a.py::test_a"],
),
"setup_py_neben_ini": (
{"tests/a/setup.py": "", "tests/b/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST},
["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"],
),
"pyproject_ohne_abschnitt": (
{"pyproject.toml": "[project]\nname = 'x'\n", "tests/test_a.py": TEST, "conftest.py": CONFTEST},
["tests/test_a.py::test_a"],
),
"pyproject_ohne_unten_ini_oben": (
{"tests/pyproject.toml": "[project]\nname = 'x'\n", "pytest.ini": INI, "tests/test_a.py": TEST},
["tests/test_a.py::test_a"],
),
"pyproject_ohne_oben_ini_unten": (
{"pyproject.toml": "[project]\nname = 'x'\n", "tests/pytest.ini": INI, "tests/test_a.py": TEST},
["tests/test_a.py::test_a"],
),
"pyproject_ini_options_leer": ({"pyproject.toml": "[tool.pytest.ini_options]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
"pyproject_tool_pytest_leer": ({"pyproject.toml": "[tool.pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
"pyproject_toml_modus": ({"pyproject.toml": "[tool.pytest]\nminversion = '8'\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
"tox_mit_abschnitt": ({"tox.ini": "[tox]\n[pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
"tox_ohne_abschnitt": ({"tox.ini": "[tox]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
"tox_abschnitt_mit_kommentar": ({"tox.ini": "[pytest] # k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
"setup_cfg_tool_pytest": ({"setup.cfg": "[tool:pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
"pytest_toml_leer": ({"pytest.toml": "", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
"punkt_pytest_ini_unterordner": ({"tests/.pytest.ini": "", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
"knoten_fehlt_teilweise": (
{"tests/pytest.ini": INI, "tests/test_a.py": TEST},
["tests/test_a.py::test_a", "gibtsnicht/test_x.py::test_a"],
),
"knoten_fehlen_alle": ({"tests/pytest.ini": INI, "tests/test_a.py": TEST}, ["gibtsnicht/test_x.py::test_a"]),
"ordner_als_knoten": ({"tests/pytest.ini": INI, "tests/test_a.py": TEST}, ["tests"]),
"testdatei_in_der_wurzel": ({"test_a.py": TEST, "conftest.py": CONFTEST}, ["test_a.py::test_a"]),
"ohne_konfiguration": (
{"tests/test_a.py": TEST, "conftest.py": CONFTEST, "tests/conftest.py": CONFTEST},
["tests/test_a.py::test_a"],
),
}
def baue(rahmen, dateien, fremd):
wurzel = rahmen / "k"
for rel, inhalt in dateien.items():
pfad = wurzel / rel
pfad.parent.mkdir(parents=True, exist_ok=True)
pfad.write_text(inhalt, encoding="utf-8")
if fremd:
(rahmen / "pyproject.toml").write_text("[tool.pytest.ini_options]\naddopts = '--new-first'\n", encoding="utf-8")
(rahmen / "conftest.py").write_text(CONFTEST, encoding="utf-8")
(rahmen / "setup.py").write_text("", encoding="utf-8")
return wurzel
def sammeln(wurzel, zusatz, knoten, cflog):
umgebung = dict(os.environ, CFLOG=str(cflog), PYTHONDONTWRITEBYTECODE="1")
lauf = subprocess.run(
[sys.executable, "-m", "pytest", "--co", "-q", "-p", "no:cacheprovider"] + zusatz + knoten,
cwd=str(wurzel), env=umgebung, capture_output=True, text=True, timeout=120,
)
knotennamen = sorted(zeile for zeile in lauf.stdout.splitlines() if "::" in zeile)
geladen = []
if cflog.exists():
geladen = sorted(os.path.relpath(z, str(wurzel)) for z in cflog.read_text(encoding="utf-8").split("\n") if z)
cflog.unlink()
return lauf.returncode, knotennamen, geladen
def main():
if len(sys.argv) != 3:
print("Aufruf: mutanten_lauf_probe_konfig.py <mutanten_lauf.py> <arbeitsordner>")
return 2
spec = importlib.util.spec_from_file_location("mutanten_lauf_unter_probe", sys.argv[1])
laeufer = importlib.util.module_from_spec(spec)
spec.loader.exec_module(laeufer)
from _pytest.config.findpaths import determine_setup
arbeit = Path(sys.argv[2])
leere_konfig = arbeit / "plugin" / "pytest.ini"
leere_konfig.parent.mkdir(parents=True, exist_ok=True)
leere_konfig.write_text("", encoding="utf-8")
cflog = arbeit / "cflog"
abweichungen = 0
for name, (dateien, knoten) in AUFBAUTEN.items():
sauber = baue(arbeit / name / "sauber", dateien, False)
fremd = baue(arbeit / name / "fremd", dateien, True)
vorher = os.getcwd()
os.chdir(sauber)
try:
rootdir, inipath, _, _ = determine_setup(
inifile=None, override_ini=None, args=knoten, rootdir_cmd_arg=None, invocation_dir=sauber,
)
erwartet = (str(inipath) if inipath else None, str(rootdir))
except Exception as exc:
erwartet = ("Ausnahme " + type(exc).__name__, None)
finally:
os.chdir(vorher)
konfig, rootdir_l = laeufer.pytest_konfig_bestimmen(sauber, knoten)
bekommen = (str(konfig) if konfig else None, str(rootdir_l))
ohne_c = sammeln(sauber, [], knoten, cflog)
mit_c = sammeln(fremd, laeufer.pytest_konfig_argumente(fremd, knoten, leere_konfig), knoten, cflog)
gleich_suche = erwartet == bekommen
gleich_lauf = ohne_c == mit_c
if not (gleich_suche and gleich_lauf):
abweichungen += 1
print("{0:34s} suche={1} lauf={2}".format(
name, "gleich" if gleich_suche else "ABWEICHUNG", "gleich" if gleich_lauf else "ABWEICHUNG"))
if not gleich_suche:
print(" pytest: {0}".format(erwartet))
print(" laeufer: {0}".format(bekommen))
if not gleich_lauf:
print(" ohne -c, sauber: {0}".format(ohne_c))
print(" laeufer, fremd: {0}".format(mit_c))
print("aufbauten: {0}, abweichungen: {1}".format(len(AUFBAUTEN), abweichungen))
return 0 if abweichungen == 0 else 1
if __name__ == "__main__":
sys.exit(main())
```
### Erwarteter Fehlschlag vor der Umsetzung
Der neue Treiber und die neue Probe laufen gegen den Läufer, wie er jetzt im Baum steht (vor Schritt 2). In der Git-Bash:
```bash
# Laeufer im jetzigen Stand, neuer Treiber und neue Probe in einen leeren Rot-Ordner
R=/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot2
rm -rf "$R" && mkdir -p "$R"
cp /d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_probe_konfig.py "$R/"
```
Dann in der PowerShell:
```powershell
# Treiber in WSL gegen den Laeufer vor der Nachbesserung
wsl -d Debian -- timeout 1500 /home/claude/venvs/trading/bin/python /mnt/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot2/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot2\ausgabe.txt"
"exit=$LASTEXITCODE"
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot2\ausgabe.txt" -Pattern "FEHLER:|gerechnete"
```
Erwartet: `exit=1`, und die Treffer enden genau auf diese fünf Zeilen:
```
Fall 40 FEHLER: exit=1
Fall 41 FEHLER: exit=2
Fall 43 FEHLER: exit=1
Fall 45 FEHLER: exit=3
gerechnete Fehler: 4
```
## Schritt 2 — Umsetzung
Wende alle Blöcke dieses Abschnitts in der Reihenfolge an.
### `mutanten_lauf.py`, Stelle 1 von 13
Aus:
```python
-p no:cacheprovider passten, und jeder Test der Grundlinie stand als FEHLT da
(Plankritik 2 von C1 im Projekt After Install, 2026-10-05). Benannte Luecken:
tox.ini und setup.cfg werden zeilenweise nach ihrem Abschnitt durchsucht, nicht
mit iniconfig; ein setup.cfg mit [pytest] (bei pytest selbst ein Fehler) gilt
hier als keine Konfiguration.
```
wird:
```python
-p no:cacheprovider passten, und jeder Test der Grundlinie stand als FEHLT da
(Plankritik 2 von C1 im Projekt After Install, 2026-10-05). Die Wurzel wird
vorher mit realpath aufgeloest: pytest rechnet vom Arbeitsordner aus, den der
Kernel ohne Verknuepfungen meldet; ueber einen Symlink angegeben, passten die
Knotennamen sonst nicht (Pruefung vom 2026-10-06). Benannte Luecken: tox.ini
und setup.cfg werden zeilenweise nach ihrem Abschnitt durchsucht wie in
iniconfig 2.3.0 (_parse.py, _parseline), eine Datei, die iniconfig nicht lesen
kann, uebergeht der Laeufer, pytest bricht dort mit 4 ab; ein setup.cfg mit
[pytest] (bei pytest selbst ein Fehler) gilt hier als keine Konfiguration; ohne
Konfiguration in der Wurzel ist config.inipath die leere Datei, ini-Werte vom
Typ paths aus -o oder PYTEST_ADDOPTS gelten dann relativ zum Plugin-Ordner.
```
### `mutanten_lauf.py`, Stelle 2 von 13
Aus:
```python
ExitCode in _pytest/config/__init__.py), steht das Ende seiner Ausgabe in der
Grundlinie als eigener Befund und unter einem Mutanten in der Meldung der
Tests ohne Bericht und ohne Sammelfehler. Ein Sammelfehler bleibt ROT, auch
wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht
sammeln laesst, ergibt zusaetzlich "not found" (Treiber-Fall 11).
```
wird:
```python
ExitCode in _pytest/config/__init__.py), steht das Ende seiner Ausgabe in der
Grundlinie als eigener Befund je Interpreter und unter einem Mutanten, als
eine Zeile mit der Ursache vorn, in der Meldung der Tests ohne Bericht und
ohne Sammelfehler. Ein Sammelfehler bleibt ROT, auch wenn pytest dabei mit 4
endet: Ein Knoten in einer Datei, die sich nicht sammeln laesst, ergibt
zusaetzlich "found no collectors for" (Treiber-Fall 11).
```
### `mutanten_lauf.py`, Stelle 3 von 13
Aus:
```python
def _ini_hat_abschnitt(pfad, abschnitt):
"""Zeilenweise Suche nach [abschnitt]. Eine nicht lesbare Datei zaehlt als
Treffer, damit pytest sie selbst liest und den Fehler selbst meldet."""
try:
```
wird:
```python
def _ini_hat_abschnitt(pfad, abschnitt):
"""Suche nach der Abschnittszeile wie _parseline in iniconfig 2.3.0: "[" in
Spalte 0, Kommentare nach # und ; entfernt, dann "]" am Ende. Eine nicht
lesbare Datei zaehlt als Treffer, damit pytest den Fehler selbst meldet."""
try:
```
### `mutanten_lauf.py`, Stelle 4 von 13
Aus:
```python
return True
kopf = "[" + abschnitt + "]"
return any(zeile.strip() == kopf for zeile in text.split("\n"))
```
wird:
```python
return True
for zeile in text.split("\n"):
zeile = zeile.rstrip()
if not zeile.startswith("["):
continue
for zeichen in "#;":
zeile = zeile.split(zeichen)[0].rstrip()
if zeile.endswith("]") and zeile[1:-1] == abschnitt:
return True
return False
```
### `mutanten_lauf.py`, Stelle 5 von 13
Aus:
```python
def _konfig_suchen(startordner, wurzel):
```
wird:
```python
def _existiert(pfad):
"""Wie safe_exists in pytest 9.1.1: ein zu langer Name ist kein Absturz."""
try:
return pfad.exists()
except OSError:
return False
def _konfig_suchen(startordner, wurzel):
```
### `mutanten_lauf.py`, Stelle 6 von 13
Aus:
```python
for pfad in pfade:
if not pfad.exists():
continue
```
wird:
```python
for pfad in pfade:
if not _existiert(pfad):
continue
```
### `mutanten_lauf.py`, Stelle 7 von 13
Aus:
```python
Rueckgabe (konfig, rootdir); konfig ist None, wenn die Wurzel keine hat."""
wurzel = Path(os.path.abspath(str(wurzel)))
ordner = []
```
wird:
```python
Rueckgabe (konfig, rootdir); konfig ist None, wenn die Wurzel keine hat."""
wurzel = Path(os.path.realpath(str(wurzel)))
ordner = []
```
### `mutanten_lauf.py`, Stelle 8 von 13
Aus:
```python
pfad = wurzel / knoten.split("::", 1)[0]
if pfad.exists():
ordner.append(pfad if pfad.is_dir() else pfad.parent)
```
wird:
```python
pfad = wurzel / knoten.split("::", 1)[0]
if _existiert(pfad):
ordner.append(pfad if pfad.is_dir() else pfad.parent)
```
### `mutanten_lauf.py`, Stelle 9 von 13
Aus:
```python
return zeilen[-AUSGABE_ENDE_ZEILEN:]
```
wird:
```python
return zeilen[-AUSGABE_ENDE_ZEILEN:]
def kernzeile(ende, rueckgabe):
"""Die Zeile, die die Ursache nennt: bei 4 die erste mit "error" ausser der
Gebrauchszeile von argparse, bei 3 und ohne Treffer die letzte."""
if rueckgabe == 4:
for zeile in ende:
if "error" in zeile.lower() and not zeile.startswith("ERROR: usage:"):
return zeile.strip()
return ende[-1].strip() if ende else "(keine Ausgabe)"
```
### `mutanten_lauf.py`, Stelle 10 von 13
Aus:
```python
if not zeit_ueberschritten and proc.returncode in PYTEST_ABBRUCH:
abbruch = "{0} (Rueckgabewert {1}) mit {2}; Ende der Ausgabe:\n {3}".format(
PYTEST_ABBRUCH[proc.returncode], proc.returncode, python_exe,
"\n ".join(ausgabe_ende(*ausgabe)) or "(keine Ausgabe)",
)
```
wird:
```python
if not zeit_ueberschritten and proc.returncode in PYTEST_ABBRUCH:
ende = ausgabe_ende(*ausgabe)
abbruch = {
"lang": "{0} (Rueckgabewert {1}) mit {2}; Ende der Ausgabe:\n {3}".format(
PYTEST_ABBRUCH[proc.returncode], proc.returncode, python_exe,
"\n ".join(ende) or "(keine Ausgabe)",
),
"kurz": "{0} (Rueckgabewert {1}): {2}".format(
PYTEST_ABBRUCH[proc.returncode], proc.returncode, kernzeile(ende, proc.returncode),
),
}
```
### `mutanten_lauf.py`, Stelle 11 von 13
Aus:
```python
if python_exe in abbrueche:
befunde.append("Grundlinie: " + abbrueche[python_exe])
abgebrochen.update(knoten_liste)
```
wird:
```python
if python_exe in abbrueche:
befunde.append("Grundlinie: " + abbrueche[python_exe]["lang"])
abgebrochen.update(knoten_liste)
```
### `mutanten_lauf.py`, Stelle 12 von 13
Aus:
```python
if abbruch:
meldung = abbruch
if ergebnis in ("GRUEN", "UEBERSPRUNGEN", "ABGEWAEHLT"):
```
wird:
```python
if abbruch:
meldung = abbruch["kurz"]
if ergebnis in ("GRUEN", "UEBERSPRUNGEN", "ABGEWAEHLT"):
```
### `mutanten_lauf.py`, Stelle 13 von 13
Aus:
```python
alle_knoten.append(knoten)
print("pytest-konfiguration (Grundlinie): {0}".format(pytest_konfig_beschreibung(kontext["wurzel"], alle_knoten)))
```
wird:
```python
alle_knoten.append(knoten)
gruppen = gruppiere_nach_interpreter(alle_knoten, kontext["python"], kontext["python_je_datei"])
for interpreter, knoten_liste in gruppen.items():
print("pytest-konfiguration (Grundlinie, {0}): {1}".format(
interpreter, pytest_konfig_beschreibung(kontext["wurzel"], knoten_liste)))
```
### `mutanten_lauf_ENTWURF.md`, Stelle 1 von 5
Aus:
```markdown
| Hält die Abdichtung Netz und echte Zugänge fern? | Annahme, aufgelöst als Projektvorgabe: Sie ist ein Fangnetz mit benannten Lücken (Abschnitt 5), kein Nachweis. |
| Welche pytest-Konfiguration gilt? | Quelle: pytest 9.1.1, `_pytest/config/findpaths.py` (`determine_setup`, `locate_config`, `get_common_ancestor`, `load_config_dict_from_file`; SHA-256 `535d149e6f3962e4…`, in den Venvs `trading` und `mattermost` gleich) und `_pytest/config/__init__.py` (Vorgabe von `confcutdir`, `ExitCode`), nachgelesen am 2026-10-06. Projektvorgabe seit 2026-10-06: gesucht wird wie pytest, aber nie oberhalb der Wurzel (Abschnitt 3, „Konfiguration“). |
```
wird:
```markdown
| Hält die Abdichtung Netz und echte Zugänge fern? | Annahme, aufgelöst als Projektvorgabe: Sie ist ein Fangnetz mit benannten Lücken (Abschnitt 5), kein Nachweis. |
| Welche pytest-Konfiguration gilt? | Quelle: pytest 9.1.1, `_pytest/config/findpaths.py` (`determine_setup`, `locate_config`, `get_common_ancestor`, `load_config_dict_from_file`; SHA-256 `535d149e6f3962e4…`, in den Venvs `trading` und `mattermost` gleich) und `_pytest/config/__init__.py` (Vorgabe von `confcutdir`, `ExitCode`), nachgelesen am 2026-10-06; die Abschnittszeile in `tox.ini` und `setup.cfg` aus iniconfig 2.3.0 (`_parse.py`, `_parseline`) im selben Venv. Projektvorgabe seit 2026-10-06: gesucht wird wie pytest, aber nie oberhalb der Wurzel (Abschnitt 3, „Konfiguration“). |
```
### `mutanten_lauf_ENTWURF.md`, Stelle 2 von 5
Aus:
```markdown
2. **Muster.** Für jeden ausgewählten Mutanten: Datei als Bytes lesen, streng als UTF-8 dekodieren (sonst Befund), an `\n` trennen, die Zeilenfolge aus `aus` an jeder Startposition suchen (überlappende Treffer zählen mit). Genau ein Treffer ist gut. Sonst Status `MUSTER` mit der Trefferzahl; enthält die Datei ein `\r`, steht der Hinweis „Datei hat CR“ dabei. `aus` gleich `wird` ist ein Formatbefund aus Schritt 1 („wirkungslos“).
3. **Grundlinie.** Alle genannten Tests der ausgewählten Mutanten ohne Mutant fahren, gruppiert nach Interpreter. Jeder muss `GRUEN` sein. Fehlt einer, ist er abgewählt (etwa durch den Marker `integration`), übersprungen oder rot, ist das ein Befund mit der Meldung, und der Lauf endet mit Exit 2, bevor ein Mutant läuft.
4. **Mutanten** in der Reihenfolge der Datei, `MUSTER`-Fälle ausgelassen. Je Mutant:
```
wird:
```markdown
2. **Muster.** Für jeden ausgewählten Mutanten: Datei als Bytes lesen, streng als UTF-8 dekodieren (sonst Befund), an `\n` trennen, die Zeilenfolge aus `aus` an jeder Startposition suchen (überlappende Treffer zählen mit). Genau ein Treffer ist gut. Sonst Status `MUSTER` mit der Trefferzahl; enthält die Datei ein `\r`, steht der Hinweis „Datei hat CR“ dabei. `aus` gleich `wird` ist ein Formatbefund aus Schritt 1 („wirkungslos“).
3. **Grundlinie.** Alle genannten Tests der ausgewählten Mutanten ohne Mutant fahren, gruppiert nach Interpreter. Jeder muss `GRUEN` sein. Fehlt einer, ist er abgewählt (etwa durch den Marker `integration`), übersprungen oder rot, ist das ein Befund mit der Meldung, und der Lauf endet mit Exit 2, bevor ein Mutant läuft. Bricht pytest für einen Interpreter ab (Rückgabewert 3 oder 4), steht statt der Zeilen je Test ein Befund für diesen Interpreter (unten, „Abbruch von pytest“).
4. **Mutanten** in der Reihenfolge der Datei, `MUSTER`-Fälle ausgelassen. Je Mutant:
```
### `mutanten_lauf_ENTWURF.md`, Stelle 3 von 5
Aus:
```markdown
- Reihenfolge der Namen und Bedingungen wie in pytest 9.1.1: `pytest.toml`, `.pytest.toml`, `pytest.ini`, `.pytest.ini` immer; `pyproject.toml` mit `[tool.pytest]` oder `[tool.pytest.ini_options]`; `tox.ini` mit `[pytest]`; `setup.cfg` mit `[tool:pytest]`; ohne Treffer die erste `pyproject.toml` überhaupt. Erst vom gemeinsamen Vorfahren aus, ohne Treffer und ohne `setup.py` dazwischen noch einmal von jedem Testordner aus.
- Gefunden: `-c <datei>`. pytest setzt `rootdir` und `confcutdir` dann selbst auf ihren Ordner, wie bei der eigenen Suche.
- Nicht gefunden: `-c <leere pytest.ini im Plugin-Ordner> --rootdir <r> --confcutdir <r>`, `r` ist der Ordner mit `setup.py` oder sonst die Wurzel. Ohne `--confcutdir` läge `confcutdir` im Plugin-Ordner, und pytest lüde `conftest.py`-Dateien oberhalb der Wurzel.
- Die Kopfzeile `pytest-konfiguration (Grundlinie): …` nennt, was für die Grundlinie gilt.
- Benannte Lücken: `tox.ini` und `setup.cfg` werden zeilenweise nach ihrem Abschnitt durchsucht, nicht mit `iniconfig`; eine nicht lesbare oder kaputte Datei gilt als Treffer, damit pytest den Fehler selbst meldet; ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration.
**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit <python>; Ende der Ausgabe: …` (bei 3: `pytest-Abbruch mit internem Fehler`) statt einer `FEHLT`-Zeile je Test; der Bericht führt die Tests mit `FEHLT` und der Meldung „pytest abgebrochen, siehe Befunde“. Unter einem Mutanten tragen die Tests ohne Bericht und ohne Sammelfehler diesen Text als Meldung; sie machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich „not found“ (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte).
**Auswertung je Test:** eine Phase `failed` → `ROT` (bei `xfail` nicht); sonst `skipped` in einer Phase oder `xfail` → `UEBERSPRUNGEN`; sonst `call` mit `passed` → `GRUEN`; abgewählt → `ABGEWAEHLT`; gar kein Bericht → `FEHLT`. Unter einem Mutanten zählt `FEHLT` als `ROT` mit der Meldung „Sammelfehler: …“, wenn es für seine Testdatei (oder einen übergeordneten Knoten) einen `sammeln`-Bericht gibt. In der Grundlinie ist jedes `FEHLT` ein Befund.
```
wird:
```markdown
- Die Wurzel wird zuerst mit `realpath` aufgelöst. pytest rechnet vom Arbeitsordner aus, den der Kernel ohne Verknüpfungen meldet; über einen Symlink angegeben, wurden die Knotennamen sonst zu `::test_…` (Prüfung vom 2026-10-06, Treiber-Fall 41).
- Reihenfolge der Namen und Bedingungen wie in pytest 9.1.1: `pytest.toml`, `.pytest.toml`, `pytest.ini`, `.pytest.ini` immer; `pyproject.toml` mit `[tool.pytest]` oder `[tool.pytest.ini_options]`; `tox.ini` mit `[pytest]`; `setup.cfg` mit `[tool:pytest]`; ohne Treffer die erste `pyproject.toml` überhaupt. Erst vom gemeinsamen Vorfahren aus, ohne Treffer und ohne `setup.py` dazwischen noch einmal von jedem Testordner aus. Die Abschnittszeile in `tox.ini` und `setup.cfg` erkennt der Läufer wie `_parseline` in iniconfig 2.3.0: `[` in Spalte 0, Kommentare nach `#` und `;` entfernt, dann `]` am Ende. Ein Pfad, dessen Name zu lang ist, gilt wie bei `safe_exists` in pytest als nicht vorhanden.
- Gefunden: `-c <datei>`. pytest setzt `rootdir` und `confcutdir` dann selbst auf ihren Ordner, wie bei der eigenen Suche.
- Nicht gefunden: `-c <leere pytest.ini im Plugin-Ordner> --rootdir <r> --confcutdir <r>`, `r` ist der Ordner mit `setup.py` oder sonst die Wurzel. Ohne `--confcutdir` läge `confcutdir` im Plugin-Ordner, und pytest lüde `conftest.py`-Dateien oberhalb der Wurzel.
- Die Kopfzeilen `pytest-konfiguration (Grundlinie, <python>): …` nennen je Interpreter, was für seinen Aufruf in der Grundlinie gilt.
- Gegen pytest selbst geprüft wird das mit `mutanten_lauf_probe_konfig.py` (Treiber-Fall 43): 24 Aufbauten, je `pytest_konfig_bestimmen` gegen `determine_setup` und ein echtes `pytest --co` ohne `-c` gegen einen Lauf mit den Argumenten des Läufers unter fremder Konfiguration.
- Benannte Lücken: Eine `tox.ini` oder ein `setup.cfg`, die iniconfig nicht lesen kann, übergeht der Läufer, pytest bricht dort mit 4 ab; eine nicht lesbare `pyproject.toml` oder `pytest.toml` gilt dagegen als Treffer, damit pytest den Fehler selbst meldet. Ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration. Ohne Konfiguration in der Wurzel ist `config.inipath` die leere Datei; ini-Werte vom Typ `paths`, die über `-o` oder `PYTEST_ADDOPTS` kommen, gelten dann relativ zum Plugin-Ordner.
**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit <python>; Ende der Ausgabe: …` (bei 3: `pytest-Abbruch mit internem Fehler`) statt einer `FEHLT`-Zeile je Test; der Bericht führt die Tests mit `FEHLT` und der Meldung „pytest abgebrochen, siehe Befunde“. Unter einem Mutanten tragen die Tests ohne Bericht und ohne Sammelfehler eine kurze Fassung als Meldung, die Ursache vorn, damit sie in die 120 Zeichen der Mutantenzeile passt: `pytest-Bedienfehler (Rueckgabewert 4): <zeile>`. Die Zeile ist bei 4 die erste mit „error“ außer der Gebrauchszeile `ERROR: usage:` von argparse, bei 3 die letzte. Diese Tests machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich `found no collectors for …` (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte).
**Auswertung je Test:** eine Phase `failed` → `ROT` (bei `xfail` nicht); sonst `skipped` in einer Phase oder `xfail` → `UEBERSPRUNGEN`; sonst `call` mit `passed` → `GRUEN`; abgewählt → `ABGEWAEHLT`; gar kein Bericht → `FEHLT`. Unter einem Mutanten zählt `FEHLT` als `ROT` mit der Meldung „Sammelfehler: …“, wenn es für seine Testdatei (oder einen übergeordneten Knoten) einen `sammeln`-Bericht gibt. In der Grundlinie ist jedes `FEHLT` ein Befund, außer pytest ist für diesen Interpreter abgebrochen; dann steht der eine Befund aus „Abbruch von pytest“.
```
### `mutanten_lauf_ENTWURF.md`, Stelle 4 von 5
Aus:
```markdown
Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testdatei, `pyproject.toml` mit `[tool.pytest.ini_options]`; für die Fälle 36 bis 40 je ein eigenes als Unterordner `k/` eines Rahmen-Ordners), schreibt je Fall eine `mutanten.toml`, startet den Läufer als Unterprozess und prüft Exit-Code und ein Meldungsstück. **Zuerst die Grundlinie:** Der saubere Fall muss Exit 0 und nur `OK` liefern, sonst misst der Rest nichts. Nach **jedem** Fall prüft der Treiber, dass jede Datei des kleinen Projekts byte-gleich mit dem Ausgangsstand ist.
```
wird:
```markdown
Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testdatei, `pyproject.toml` mit `[tool.pytest.ini_options]`; für die Fälle 36 bis 42 je ein eigenes als Unterordner `k/` eines Rahmen-Ordners), schreibt je Fall eine `mutanten.toml`, startet den Läufer als Unterprozess und prüft Exit-Code und ein Meldungsstück. **Zuerst die Grundlinie:** Der saubere Fall muss Exit 0 und nur `OK` liefern, sonst misst der Rest nichts. Nach **jedem** Fall prüft der Treiber, dass jede Datei des kleinen Projekts byte-gleich mit dem Ausgangsstand ist.
```
### `mutanten_lauf_ENTWURF.md`, Stelle 5 von 5
Aus:
```markdown
| 35 | ein Mutant hängt, der Läufer bekommt nach drei Sekunden SIGTERM (eigenes `TMPDIR`), seit 2026-09-28 | Zeile `ABBRUCH: SIGTERM`, kein Prozess mit `HOME` im Fall-Tmpdir, keine `mutanten_lauf_*`-Reste, Zieldatei byte-gleich |
| 36 | Wurzel ohne eigene Konfiguration, darüber eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"` (der Fall der Plankritik 2 von C1), seit 2026-10-06 | Exit 0, zweimal `OK` |
| 37 | Wurzel mit eigener `pyproject.toml` (Marker `integration` abgewählt), darüber dieselbe fremde | sauberer Fall Exit 0; Test mit Marker in der Grundlinie Exit 2 und `ABGEWAEHLT`, die eigene Konfiguration gilt also |
| 38 | eigene Konfiguration mit `addopts = "--gibt-es-nicht"` | Exit 2, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` aus der pytest-Ausgabe, keine Zeile `ist FEHLT` |
| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` |
| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1, `UNKLAR`, Meldung mit `pytest-Bedienfehler (Rueckgabewert 4)` |
```
wird:
```markdown
| 35 | ein Mutant hängt, der Läufer bekommt nach drei Sekunden SIGTERM (eigenes `TMPDIR`), seit 2026-09-28 | Zeile `ABBRUCH: SIGTERM`, kein Prozess mit `HOME` im Fall-Tmpdir, keine `mutanten_lauf_*`-Reste, Zieldatei byte-gleich |
| 36 | Wurzel ohne eigene Konfiguration, darüber eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"` (der Fall der Plankritik 2 von C1), seit 2026-10-06 | Exit 0, zweimal `OK`, Kopfzeile „keine in der Wurzel, leere Datei“ |
| 37 | Wurzel mit eigener `pyproject.toml` (Marker `integration` abgewählt), darüber dieselbe fremde | sauberer Fall Exit 0 mit Kopfzeile `k/pyproject.toml (aus der Wurzel)`; Test mit Marker in der Grundlinie Exit 2 und `ABGEWAEHLT`, die eigene Konfiguration gilt also |
| 38 | eigene Konfiguration mit `addopts = "--gibt-es-nicht"` | Exit 2, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` aus der pytest-Ausgabe, keine Zeile `ist FEHLT` |
| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` |
| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1; die Mutantenzeile selbst trägt `UNKLAR`, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` |
| 41 | Wurzel über einen Symlink angegeben (`link/k` → `echt/k`), seit der Nachbesserung vom 2026-10-06 | Exit 0, zweimal `OK` |
| 42 | ein `conftest.py` der Wurzel wirft in `pytest_collection_modifyitems` | Exit 2, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3)` und der Text der Ausnahme, keine Zeile `ist FEHLT` |
| 43 | `mutanten_lauf_probe_konfig.py`: 24 Aufbauten gegen `determine_setup` und gegen echte `pytest --co`-Läufe | Exit 0, `abweichungen: 0` |
| 44 | zwei Interpretergruppen (`python_je_datei`), nur in einer ein Knoten, den es nicht gibt; diese Gruppe einmal an erster, einmal an letzter Stelle | je Exit 2, genau ein Befund mit dem Interpreter dieser Gruppe, keiner für die andere, keine Zeile `ist FEHLT` |
| 45 | ein Knoten mit einem Dateinamen über 255 Zeichen | Exit 2 mit einem Befund `Grundlinie: pytest-…`, kein `unerwarteter Abbruch` |
```
### `LIESMICH.md`, Stelle 1 von 2
Aus:
```markdown
8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel <wegwerfkopie> --mutanten <planordner>/mutanten.toml [--nur ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <s>]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). Es fährt zuerst die Grundlinie, dann je Mutant nur die genannten Tests, setzt die Datei zurück und prüft das per SHA-256. Je Test gibt es die Fehlermeldung mit aus; ob ein Test an der gemeinten Stelle fällt, liest man dort. `--nur-muster` prüft nur, ob jedes Muster genau einmal dasteht, ohne pytest. Die Kopie legt man in WSL unter `/tmp` an (`cp -a`, danach `__pycache__` löschen). Auf `/mnt/d` läuft es auch, nur langsamer. **Aus der Git-Bash ein Skript nie mit `wsl … bash /mnt/d/…` starten:** Die Git-Bash macht aus dem Pfad einen Windows-Pfad. Aus der PowerShell starten oder `MSYS_NO_PATHCONV=1` davorsetzen. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`).
**Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** Liegt `mutanten.toml` im Planordner, startet `plankritik.js` in der Vorbereitung `mutanten_lauf_kopie.sh`. Das Skript kopiert die Kopie mit angewendetem Plan nach `/tmp` in WSL, fährt dort den Läufer in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_<n>`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes.
```
wird:
```markdown
8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel <wegwerfkopie> --mutanten <planordner>/mutanten.toml [--nur ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <s>]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). Ob die Suche pytest selbst entspricht, prüft `mutanten_lauf_probe_konfig.py <mutanten_lauf.py> <arbeitsordner>` mit dem Python eines Venvs, in dem pytest liegt (24 Aufbauten, Exit 0 ohne Abweichung). Es fährt zuerst die Grundlinie, dann je Mutant nur die genannten Tests, setzt die Datei zurück und prüft das per SHA-256. Je Test gibt es die Fehlermeldung mit aus; ob ein Test an der gemeinten Stelle fällt, liest man dort. `--nur-muster` prüft nur, ob jedes Muster genau einmal dasteht, ohne pytest. Die Kopie legt man in WSL unter `/tmp` an (`cp -a`, danach `__pycache__` löschen). Auf `/mnt/d` läuft es auch, nur langsamer. **Aus der Git-Bash ein Skript nie mit `wsl … bash /mnt/d/…` starten:** Die Git-Bash macht aus dem Pfad einen Windows-Pfad. Aus der PowerShell starten oder `MSYS_NO_PATHCONV=1` davorsetzen. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`).
**Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** Liegt `mutanten.toml` im Planordner, startet `plankritik.js` in der Vorbereitung `mutanten_lauf_kopie.sh`. Das Skript kopiert die Kopie mit angewendetem Plan nach `/tmp` in WSL, fährt dort den Läufer in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_<n>`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes.
```
### `LIESMICH.md`, Stelle 2 von 2
Aus:
```markdown
Seit 2026-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen), zusammen 17.
```
wird:
```markdown
Nachbesserung vom 2026-10-06 (Plankritik 2 von C1 im Projekt After Install, Plan `docs/plans/2026-10-06-pytest-konfiguration.md` und seine Nachbesserung): pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel; Abbruch von pytest mit Ursache statt `FEHLT`. Belegt am selben Tag:
- Treiber 45 von 45 grün. Die neuen Fälle 36 bis 45 waren je mindestens einmal rot: gegen den Läufer vor der Änderung (36, 38, 40), gegen den Stand vor der Nachbesserung (40, 41, 43, 45) und gegen kaputte Varianten (37 ohne eigene Konfiguration, 39 ohne `--confcutdir`, 42 ohne Rückgabewert 3, 43 ohne `setup.py`-Zweig oder ohne zweite Suche, 44 nach dem ersten Abbruch ohne weitere Gruppe).
- Gegenprobe an allen sechs `mutanten.toml` im Vault, alter gegen neuer Läufer auf Kopien aus `git archive HEAD` unter `/tmp`: Headscale A, Mattermost, After Install C1 und D-40 gleich (175, 130, 69 und 41 Mutanten `OK`); Headscale B und Trading Plattform WH enden bei beiden mit Exit 2, weil ihr Stand nicht zum Plan passt. Unter dem Windows-Temp endete C1 mit dem alten Läufer mit 101 × `FEHLT`, mit dem neuen mit 69 `OK`.
- Ein `pruefer` fand in der ersten Fassung den Symlink-Fall und die fehlenden Fälle; beides ist in der Nachbesserung behoben.
- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`).
Seit 2026-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen), zusammen 17.
```
## Schritt 3 — Verifikation
```bash
# Kompilieren, nur ASCII in den Python-Dateien, keine CR in allen fuenf Dateien, Cache wieder weg
cd /d/Claude/.vault-tools/plan-kopie
python -m py_compile mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py && echo kompiliert
rm -rf __pycache__
LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py
for f in mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py mutanten_lauf_ENTWURF.md LIESMICH.md; do printf '%s %s\n' "$f" "$(tr -cd '\r' < "$f" | wc -c)"; done
```
Erwartet: `kompiliert`, je Python-Datei die Zahl `0`, je Datei die Zahl `0`.
```powershell
# Treiber gegen den nachgebesserten Laeufer in WSL
wsl -d Debian -- timeout 1500 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot2\ausgabe-neu.txt"
"exit=$LASTEXITCODE"
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot2\ausgabe-neu.txt" -Pattern "FEHLER:|gerechnete"
```
Erwartet: `exit=0` und als einziger Treffer eine Zeile, die auf `gerechnete Fehler: 0` endet.
Danach `C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot2` löschen.
## Bericht
Melde: je Zieldatei die Zahl der angewendeten Aus/wird-Paare (und für die neue Datei „neu“), die Ausgaben aus Schritt 1 und Schritt 3 im Wortlaut, und jede Abweichung vom Plan oder getroffene Annahme. „Keine Abweichungen“ nur, wenn es keine gab.
## Fertig heißt (Koordinator)
1. Die fünf Zieldateien sind byte-gleich mit der Kopie, auf die dieser Plan angewendet wurde (`cmp`), und sonst ist unter `.vault-tools/` nichts geändert außer den Plänen unter `plan-kopie/docs/`.
2. Treiber in WSL: Exit 0, `gerechnete Fehler: 0`, selbst gefahren.
3. Rot-Nachweis wie unter „Gemessen“ oben.
4. Gegenprobe an den sechs `mutanten.toml` mit dem nachgebesserten Läufer, alter gegen neuer Läufer, wie beim Vorgänger: gleicher Exit-Code, gleiches Ergebnis je Test der Grundlinie, gleicher Status je Mutant; C1 unter dem Windows-Temp mit dem neuen Läufer wie unter `/tmp`.
5. Ein `pruefer` hat die Nachbesserung gelesen; er sagt, ob eine weitere Runde noch Substanz hat.
## Risiken
1. **Die Probe hängt an pytest-Interna.** `determine_setup` ist keine öffentliche Schnittstelle. Ändert eine spätere pytest-Fassung die Signatur, scheitert Fall 43 mit einer Ausnahme. Merkmal: `Fall 43 FEHLER` mit einem `TypeError` im Text; dann die Probe an die neue Fassung anpassen, nicht den Läufer.
2. **Laufzeit des Treibers.** Fall 43 fährt 48 `pytest --co`-Läufe. Merkmal: Der Treiber braucht deutlich länger als vorher; das Zeitlimit im Befehl oben ist dafür auf 1500 Sekunden gesetzt.