Mutantenstufe der Plankritik nach der Pruefung vom 2026-09-28: Netzsperre per unshare, ungepufferte Ausgabe, Zeitlimit je Mutant 120 s, Aufraeumen bei SIGTERM, Modus-Zeile, mutanten_status, Faelle und Treiberfall 35

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5.5 committed 2026-09-28 08:19:23 +02:00
1 parent bc9b4f4497
commit b1ad461acf
8 files changed
+312 -25

No files matched your search

+42 -15
View File
@@ -17,8 +17,11 @@ export const meta = {
// - werkzeuge ist ein Pfad, keine Ausgabe; Vorgabe D:/Claude/.vault-tools/plan-kopie.
// - Ohne kopie baut materialisieren sie selbst (kopie_bauen.py plus plan_materialisiere.py);
// ohne() muss Geheimnisdateien und große Datenordner nennen.
// - mutanten: "voll" (Vorgabe), "muster" oder "aus". Nur wirksam mit gesetztem kopie; steuert den
// Hilfsagenten werkzeug:mutanten, der mutanten_lauf_kopie.sh fährt (siehe Kommentar dort unten).
// - mutanten: "voll" (Vorgabe), "muster" oder "aus". Die Stufe greift, sobald ctx.kopie steht -
// also auch mit materialisieren, nicht nur mit args.kopie direkt gesetzt. Sie läuft auch ohne
// die Linse arithmetik in args.linsen; die Rohausgabe steht dann nur in der Rückgabe
// (mutanten_lauf, mutanten_status, mutanten_modus). Steuert den Hilfsagenten werkzeug:mutanten,
// der mutanten_lauf_kopie.sh fährt (siehe Kommentar dort unten).
// - plaetze begrenzt die gleichzeitigen Linsen; verbrauch.js senkt sie nach dem 5-Stunden-Fenster
// und der Woche.
// - Rückgabe mit status fertig oder gebremst; ein gebremster Lauf wird neu gestartet, nicht
@@ -42,13 +45,24 @@ const RANG = { blockierend: 0, wichtig: 1, klein: 2 }
const q = s => "'" + String(s).replace(/'/g, "'\\''") + "'"
// Text für die Linse arithmetik, wenn der Mutantenlauf (werkzeug:mutanten) etwas geliefert hat.
// Seit der Nachbesserung 2026-09-28 (N5, N7): modus: muster in der Rohausgabe heißt, dass kein
// Mutant gefahren ist (der Satz "prüfe bei OK an der Fehlermeldung" entfällt dann); Exit 2 oder 3
// heißt, der Lauf ist vor oder während der Mutanten gescheitert.
function mutantenText(m) {
if (!m) return ''
if (m.keineToml) return ' Der Plan hat keine mutanten.toml. Das ist ein Befund der Schwere wichtig, sobald der Plan Mutanten in "Fertig heißt" nennt.'
const zeilen = m.rohausgabe.split('\n')
const letzte = zeilen.slice(-200).join('\n')
const grenze = /ZEITGRENZE/.test(m.rohausgabe) ? ' Der Lauf hat seine Zeitgrenze erreicht: Mutanten ohne eigene Zeile sind nicht gefahren; setze für sie selbst höchstens die aus, die dir am riskantesten erscheinen, in einer eigenen Kopie.' : ''
return `${grenze} Bewerte jeden Mutanten mit LUECKE, UNKLAR, ZEIT oder MUSTER als Befund und prüfe bei OK an der Fehlermeldung, ob der Test an der gemeinten Stelle fällt; setze Mutanten nicht erneut von Hand.\n<mutantenlauf>\n${letzte}\n</mutantenlauf>`
const musterModus = /^modus: muster/m.test(m.rohausgabe)
const exitMatch = /EXIT mutanten_lauf: (\d+)/.exec(m.rohausgabe)
const exitCode = exitMatch ? Number(exitMatch[1]) : null
let hinweis = ''
if (/ZEITGRENZE/.test(m.rohausgabe)) hinweis = ' Der Lauf hat seine Zeitgrenze erreicht: Mutanten ohne eigene Zeile sind nicht gefahren; setze für sie selbst höchstens die aus, die dir am riskantesten erscheinen, in einer eigenen Kopie.'
else if (exitCode === 2 || exitCode === 3) hinweis = ` Der Mutantenlauf ist vor oder während der Mutanten gescheitert (Exit ${exitCode}); welche Mutanten ohne eigene Zeile sind, sind nicht gefahren; das ist ein Befund der Schwere wichtig.`
const bewertung = musterModus
? ' Kein Mutant ist gefahren; OK heißt hier nur, dass das Muster genau einmal in der Datei steht; ob ein Test fällt, ist unbelegt.'
: ' Bewerte jeden Mutanten mit LUECKE, UNKLAR, ZEIT oder MUSTER als Befund und prüfe bei OK an der Fehlermeldung, ob der Test an der gemeinten Stelle fällt; setze Mutanten nicht erneut von Hand.'
return `${hinweis}${bewertung}\n<mutantenlauf>\n${letzte}\n</mutantenlauf>`
}
const LINSEN = {
@@ -215,7 +229,7 @@ Gilt der Befund nur unter einer Bedingung, nenn sie. Gib deine eigene Einschätz
pruefeArgs(args)
const W = args.werkzeuge || 'D:/Claude/.vault-tools/plan-kopie'
const PY = 'PYTHONIOENCODING=utf-8 python -X utf8'
const ctx = { kopie: args.kopie || null, werkzeuge: null, temp: null, mutantenLauf: null }
const ctx = { kopie: args.kopie || null, werkzeuge: null, temp: null, mutantenLauf: null, mutantenStatus: null }
const MUTANTEN_MODUS = args.mutanten || 'voll'
const vorbereitung = []
@@ -263,22 +277,30 @@ if (!ctx.kopie && args.materialisieren) {
}
// Mutantenlauf (seit 2026-09-27). mutanten_lauf.py ändert Dateien in der Wurzel, die es bekommt,
// und setzt sie nach jedem Mutanten zurück. Die Linsen fahren gleichzeitig Tests in ctx.kopie;
// deshalb arbeitet mutanten_lauf_kopie.sh auf einer Kopie der Kopie unter /tmp in WSL.
// deshalb arbeitet mutanten_lauf_kopie.sh auf einer Kopie der Kopie unter /tmp in WSL. Dieser
// await steht in der Vorbereitung, also vor phase('Plankritik'): Der Mutantenlauf ist abgewartet,
// bevor die Linsen starten, und die Kopie der Kopie schützt dabei ctx.kopie selbst - die Linsen
// arbeiten gleichzeitig darin, mutanten_lauf.py aber nur in seiner eigenen Kopie unter /tmp.
// MSYS_NO_PATHCONV=1 ist Pflicht: Ohne sie übersetzt die Git-Bash /mnt/d/... in einen
// Windows-Pfad (Erinnerung git-bash-uebersetzt-pfade-fuer-wsl-exe). cygpath -l ist Pflicht, weil
// die Kopie oft unter dem Kurznamen PASCAL~1 liegt. cygpath -m statt -w: wsl -d Debian -- ...
// reicht die Befehlszeile an die Linux-Shell weiter, und die verschluckt jeden Backslash
// ("D:\Claude\foo" kam als "D:Claudefoo" an, gemessen 2026-09-27); Vorwärtsschrägstriche
// übersteht der Pfad, und wslpath -u im Skript wandelt ihn um. Das Bash-Werkzeug des Hilfsagenten
// endet nach höchstens zehn Minuten; das Skript begrenzt den Lauf deshalb selbst auf 540 s und
// meldet eine überschrittene Grenze mit ZEITGRENZE.
// die Kopie oft unter dem Kurznamen PASCAL~1 liegt. cygpath -m statt -w, weil wsl -d Debian -- ...
// die Befehlszeile an die Linux-Shell weiterreicht: Die Backslash-Begründung dafür
// ("D:\Claude\foo" kam als "D:Claudefoo" an, gemessen 2026-09-27) konnte der Prüfer am
// 2026-09-28 in dieser Aufrufform nicht nachstellen; -m bleibt trotzdem, weil es nicht schadet,
// und wslpath -u im Skript wandelt den Pfad ohnehin um. Das Bash-Werkzeug des Hilfsagenten endet
// nach höchstens zehn Minuten; das Skript begrenzt den Lauf deshalb selbst auf 540 s und meldet
// eine überschrittene Grenze mit ZEITGRENZE.
// mutanten_status (N8, seit 2026-09-28): aus, ohne_kopie, keine_toml, ohne_ergebnis, sonst exit_<n>.
if (MUTANTEN_MODUS === 'aus') ctx.mutantenStatus = 'aus'
else if (!ctx.kopie) ctx.mutantenStatus = 'ohne_kopie'
if (ctx.kopie && MUTANTEN_MODUS !== 'aus') {
const befehl = `MSYS_NO_PATHCONV=1 wsl -d Debian -- bash /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "$(cygpath -m -l ${q(ctx.kopie)})" "$(cygpath -m -l ${q(args.planordner)})" ${MUTANTEN_MODUS === 'muster' ? 'muster' : 'voll'}`
const r = await werkzeugAgent('werkzeug:mutanten', 'Vorbereitung', befehl, 'Mutanten aus mutanten.toml auf einer Kopie der Kopie fahren', 'Setze im Bash-Werkzeug den Parameter timeout auf 600000; der Befehl darf bis zu zehn Minuten laufen.')
const aus = r && r.laeufe && r.laeufe[0] ? String(r.laeufe[0].ausgabe || '') : ''
if (/KEINE_MUTANTEN_TOML/.test(aus)) ctx.mutantenLauf = { keineToml: true, rohausgabe: aus }
else if (/EXIT mutanten_lauf: \d+/.test(aus)) ctx.mutantenLauf = { keineToml: false, rohausgabe: aus }
else log('Mutantenlauf: Hilfsagent ohne verwertbares Ergebnis (kein KEINE_MUTANTEN_TOML, kein EXIT mutanten_lauf); Linsen laufen ohne Mutantenlauf weiter')
const exitMatch = /EXIT mutanten_lauf: (\d+)/.exec(aus)
if (/KEINE_MUTANTEN_TOML/.test(aus)) { ctx.mutantenLauf = { keineToml: true, rohausgabe: aus }; ctx.mutantenStatus = 'keine_toml' }
else if (exitMatch) { ctx.mutantenLauf = { keineToml: false, rohausgabe: aus }; ctx.mutantenStatus = 'exit_' + exitMatch[1] }
else { log('Mutantenlauf: Hilfsagent ohne verwertbares Ergebnis (kein KEINE_MUTANTEN_TOML, kein EXIT mutanten_lauf); Linsen laufen ohne Mutantenlauf weiter'); ctx.mutantenStatus = 'ohne_ergebnis' }
vorbereitung.push(r)
}
@@ -293,6 +315,9 @@ if (vorLinsen.gebremst) {
zaehlung: { roh: 0, zusammengefasst: 0, bestaetigt: 0, offen: 0, widerlegt: 0, nicht_geprueft: 0 },
kopie: ctx.kopie,
temp_ordner: tempText(),
mutanten_lauf: ctx.mutantenLauf ? ctx.mutantenLauf.rohausgabe : null,
mutanten_status: ctx.mutantenStatus,
mutanten_modus: MUTANTEN_MODUS,
befunde: [],
selbst_ausgefuehrt: [],
nicht_pruefbar: [],
@@ -355,6 +380,8 @@ return {
kopie: ctx.kopie,
temp_ordner: tempText(),
mutanten_lauf: ctx.mutantenLauf ? ctx.mutantenLauf.rohausgabe : null,
mutanten_status: ctx.mutantenStatus,
mutanten_modus: MUTANTEN_MODUS,
befunde,
selbst_ausgefuehrt: ergebnisse.map(e => ({ linse: e.linse, befehle: e.ergebnis.selbst_ausgefuehrt })),
nicht_pruefbar: ergebnisse.flatMap(e => e.ergebnis.nicht_pruefbar.map(x => e.linse + ': ' + x)),
+5 -2
View File
@@ -23,11 +23,12 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko
7. **Rot-Nachweis je Task (seit 2026-09-18):** `rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test] [--venv <wsl-pfad>] [--frontend <ordner>] [--projektname <ordnername>]`. **Für ein anderes Projekt als die Trading Plattform braucht es die drei letzten Schalter (seit 2026-09-24):** Ihre Vorgaben sind die der Trading Plattform (`~/venvs/trading`, `dashboard`, `trading-plattform`). Jedes andere Projekt nennt seine Werte, für den Nexhorizons Hub `--venv '~/venvs/hub' --frontend frontend --projektname 'active - Nexhorizons Hub'`, das Venv in einfachen Anführungszeichen, sonst macht die Git-Bash aus `~` einen Windows-Pfad. Das Skript druckt die benutzten Werte in der ersten Zeile. Die Vorgabe `trading-plattform` passt nur auf Pläne mit Arbeitsbaum-Pfaden; ältere Trading-Pläne mit absoluten Pfaden unter `active - Trading Plattform` brauchen `--projektname 'active - Trading Plattform'`, sonst endet der Lauf mit einem `ValueError`. Weiter fest verdrahtet bleiben die Distribution `Debian`, die Ordner `tests/` und `tests/fixtures/`, die Endungen `.test.ts`/`.test.tsx` und `npx.cmd vitest`. **Vorbedingung:** Das Projekt ist im Venv nicht editierbar installiert (`pip install -e`); sonst löst der Finder von setuptools ein fehlendes Untermodul gegen das echte Projekt auf, und ein Test ohne Umsetzung läuft grün. `tests/conftest.py` gilt dabei als Produktivseite und wird zurückgesetzt; nur wenn die neue Fixture eines Tasks dort liegt, lässt `--conftest-als-test` sie stehen (Beleg unten, 2026-09-22). `plan_materialisiere.py` wendet eine Taskdatei immer ganz an; „erst der Test, dann die Umsetzung" lässt sich damit nicht beobachten. Das Skript stellt den Zustand dazwischen auf Wegwerfkopien her — Basis plus Voraussetzungstasks plus der Task, danach die produktiven Zieldateien zurück auf den Stand vor dem Task — und fährt die Testdateien des Tasks (pytest in WSL, vitest unter Windows über eine Junction, die es vor dem Löschen löst). Es meldet je Task `ROT` mit der ersten Fehlerzeile oder `GRUEN`; ein Test, der ohne Umsetzung grün ist, misst sie nicht. Die Basis-Kopie zieht man **vor** der ersten Welle. `spec.json` nennt je Task die Voraussetzungstasks. Erster Einsatz am 2026-09-18 an vier Plänen der Trading Plattform: 35 Tasks, alle rot an der gemeinten Stelle, keiner grün ohne Umsetzung. Die Executer brauchen den Tanz mit dem Beiseitelegen der Produktivdatei dann nicht mehr; ihr Auftrag sagt das ausdrücklich.
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`. 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. **Die Plankritik fährt die Mutanten selbst (seit 2026-09-27):** 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 und gibt die Ausgabe an die Linse `arithmetik`. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`. Der ganze Lauf hat 540 s, weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. B1 mit 74 Mutanten brauchte 147 s. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes.
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`. 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.
## Was belegt ist
Seit 2026-09-27 gibt es `mutanten_lauf.py` und `mutanten_lauf_treiber.py` (Punkt 3 der Arbeitsweise vom 2026-09-26). Der Treiber fährt die 34 Angriffsfälle aus dem Entwurf auf einem kleinen Projekt unter `/tmp` und prüft nach jedem Fall, dass das Projekt byte-gleich zurückgesetzt ist. Am selben Tag hat der Koordinator neun Fehler in eine Kopie des Läufers eingebaut, und der Treiber hat jeden an seinem Fall gefangen:
Seit 2026-09-27 gibt es `mutanten_lauf.py` und `mutanten_lauf_treiber.py` (Punkt 3 der Arbeitsweise vom 2026-09-26). Der Treiber fährt die 35 Angriffsfälle aus dem Entwurf auf einem kleinen Projekt unter `/tmp` und prüft nach jedem Fall, dass das Projekt byte-gleich zurückgesetzt ist. Am selben Tag hat der Koordinator neun Fehler in eine Kopie des Läufers eingebaut, und der Treiber hat jeden an seinem Fall gefangen:
- „passed“ zählt als rot
- kein Zurücksetzen
- keine `.git`-Prüfung
@@ -40,6 +41,8 @@ Seit 2026-09-27 gibt es `mutanten_lauf.py` und `mutanten_lauf_treiber.py` (Punkt
Das Skript dazu liegt außerhalb des Vaults und ist nicht Teil der Werkzeuge. Erster echter Einsatz war am selben Tag B1 der Trading Plattform: 74 von 74 Mutanten getötet, alle Fehlschläge Zusicherungen, die Kopie danach unverändert. Eine Lehre aus der Übertragung: Nennt ein Plan einen parametrisierten Test ohne Fall, gehören nur die Fälle in `rot_bei`, die der Mutant umkippt. Bei D1 wären drei von fünf Fällen unter `return False` grün geblieben.
Nachbesserung vom 2026-09-28 (Prüfbericht desselben Tages): `mutanten_lauf_kopie.sh` läuft jetzt unter `unshare --map-current-user --net`, mit `python3 -u` und einem Zeitlimit je Mutant (`ZEITLIMIT_S`, kleiner als `BUDGET_S`), schreibt vor dem Lauf eine Modus-Zeile und räumt bei SIGTERM auf (`timeout -k 15`, eigener Handler in `mutanten_lauf.py`). `plankritik.js` gibt zusätzlich `mutanten_status` und `mutanten_modus` zurück und erklärt der Linse `arithmetik` Modus `muster` und Exit 2/3. Treiber-Fall 35 (SIGTERM mitten in einem hängenden Mutanten) und die zusätzlichen Fälle in `faelle_workflows.mjs`/`mutanten_workflows.py` sind gefahren und grün; die Rot-Ausgaben davor stehen im Bericht des Executers vom selben Tag.
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.
Seit 2026-09-24 hat `rot_nachweis.py` die Schalter `--venv`, `--frontend` und `--projektname`; vorher waren `~/venvs/trading`, `dashboard` und `trading-plattform/` fest verdrahtet. Ein Hub-Lauf im Trading-Venv wäre für die Tasks mit `argon2-cffi` und `pyotp` rot aus einem fehlenden Paket gewesen, nicht aus der fehlenden Umsetzung. Fehlt das Venv in WSL, steht beim Task jetzt `VORAUSSETZUNG FEHLT` statt `ROT`, und die Schlusszeile zählt diese Läufe getrennt. Belegt am selben Tag: Der Vorgabe-Befehl ist byte-gleich mit der alten Fassung, falsche Schalterwerte enden mit Exit 3, und der erste echte Lauf über 38 Tasks des Hub meldete keinen grün ohne Umsetzung. Im selben Lauf zeigte sich eine zweite Lücke: Beim Zurücksetzen blieben Ordner stehen, die erst der Task angelegt hatte. Ein leeres `hub/auth/` liest Python als Namespace-Paket, und die Fehlerzeile nannte `hub.auth.passwort` statt `hub.auth`. Jetzt gehen solche Ordner mit, sobald sie leer sind; derselbe Lauf für die Tasks 7, 9 und 11 zeigt danach genau die Zeile aus dem Plan. Die unabhängige Prüfung desselben Tages fand eine dritte Lücke: Ein vergessenes `--venv` fiel still auf das Trading-Venv zurück, und Task 7 stand als `ROT … No module named 'argon2'` da. Seitdem wird eine erste Fehlerzeile, die ein Modul außerhalb des Projekts und außerhalb der Zieldateien vermisst, als `VORAUSSETZUNG FEHLT` gemeldet. Das trifft auch den Ende-zu-Ende-Task des Hub, dessen Tests `playwright` brauchen. Mit den Vorgaben lieferte das Skript für die sieben Tasks des Trading-Pakets K vor und nach allen Änderungen dieselben Zeilen. Zwei weitere Lücken desselben Tages, beide ein ROT, das nichts misst: **Ein Arbeitsordner mit Windows-Kurznamen** (`C:/Users/PASCAL~1/…`) ließ vitest dort starten, die Dateien aber über den langen Namen auflösen, und jede Oberflächen-Testdatei endete als `Cannot find module '/src/…'`. Das Skript löst den Arbeitsordner jetzt auf den langen Namen auf; danach zeigte Trading-Task 395 wieder `9 failed | 52 passed` statt `no tests`, und die Hub-Tasks 24, 25, 29 und 37 die Zeile aus dem Plan (`Error: Failed to resolve import "./kontrast" …`). Die erste `Error:`-Zeile steht seitdem mit in der Ergebniszeile, weil die `FAIL`-Zeile allein den Grund nicht zeigt. **Ein Test, der über Vite eine Datei aus `node_modules` lädt** (`?raw`), scheitert hinter der Junction mit `Denied ID`, weil Vite nichts außerhalb seines Wurzelordners ausliefert; das Skript meldet das als `VORAUSSETZUNG FEHLT`. Für solche Tasks läuft der Nachweis in einer Kopie mit echtem `node_modules` (242 MB, gut 20 Sekunden).
+46 -4
View File
@@ -125,6 +125,16 @@ class RuecksetzenGescheitert(Exception):
super().__init__("Ruecksetzen gescheitert bei Mutant {0} ({1})".format(mutant_id, datei))
class SigtermUnterbrechung(Exception):
"""Wird aus dem SIGTERM-Handler ausgeloest, damit die finally-Bloecke
(Ruecksetzen der Zieldatei, Aufraeumen der Temp-Ordner) laufen. Siehe
Nachbesserung 2026-09-28, N4."""
def _sigterm_werfen(signum, frame):
raise SigtermUnterbrechung()
def ist_relativer_pfad(pfad):
if not isinstance(pfad, str) or pfad == "":
return False
@@ -442,12 +452,18 @@ def pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimi
zeit_ueberschritten = False
absturz = None
proc = None
try:
try:
proc = subprocess.Popen(
cmd, cwd=str(wurzel), env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
start_new_session=True,
)
except OSError as exc:
absturz = "pytest-Aufruf gescheitert ({0}): {1}".format(python_exe, exc)
if proc is not None:
try:
proc.communicate(timeout=zeitlimit)
except subprocess.TimeoutExpired:
@@ -457,8 +473,20 @@ def pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimi
except (ProcessLookupError, PermissionError):
pass
proc.communicate()
except OSError as exc:
absturz = "pytest-Aufruf gescheitert ({0}): {1}".format(python_exe, exc)
except BaseException:
# Auch ein SIGTERM (SigtermUnterbrechung) darf keinen pytest-Prozess
# zurueck lassen: Prozessgruppe beenden, dann weiterwerfen, damit die
# finally-Bloecke der Aufrufer (Ruecksetzen der Zieldatei, Aufraeumen
# der Temp-Ordner) laufen. Siehe Nachbesserung 2026-09-28, N4.
try:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
except (ProcessLookupError, PermissionError):
pass
try:
proc.communicate()
except Exception:
pass
raise
meldungen = []
try:
@@ -473,14 +501,16 @@ def pytest_lauf(python_exe, knoten_liste, wurzel, home_dir, plugin_dir, zeitlimi
pass
except OSError:
pass
return zeit_ueberschritten, meldungen, absturz
finally:
# Muss auch bei einem SIGTERM mitten im Lauf greifen (N4): sonst bleibt die
# Ergebnisdatei liegen, obwohl der pytest-Prozess schon beendet ist.
try:
os.remove(ergebnis_pfad)
except OSError:
pass
return zeit_ueberschritten, meldungen, absturz
def gruppen_ausfuehren(gruppen, wurzel, home_dir, plugin_dir, zeitlimit):
reports = {}
@@ -755,6 +785,7 @@ def ablauf(args, bericht):
plugin_dir = Path(tempfile.mkdtemp(prefix="mutanten_lauf_plugin_"))
home_dir = Path(tempfile.mkdtemp(prefix="mutanten_lauf_home_"))
aktueller_mutant_id = None
try:
(plugin_dir / (PLUGIN_NAME + ".py")).write_text(PLUGIN_QUELLTEXT, encoding="utf-8")
@@ -784,11 +815,13 @@ def ablauf(args, bericht):
print(" " + mutant["hinweis"])
continue
aktueller_mutant_id = mutant["id"]
status, test_ergebnisse = mutant_anwenden_und_pruefen(
mutant, muster["start"], muster["aus_zeilen"], muster["wird_zeilen"],
wurzel, home_dir, plugin_dir,
kontext["python_je_datei"], kontext["python"], args.zeitlimit,
)
aktueller_mutant_id = None
eintrag = {
"id": mutant["id"],
"datei": mutant["datei"],
@@ -808,6 +841,14 @@ def ablauf(args, bericht):
alle_ok = all(m["status"] == "OK" for m in bericht["mutanten"])
return 0 if alle_ok else 1
except SigtermUnterbrechung:
zeile = (
"ABBRUCH: SIGTERM, Mutant {0} zurueckgesetzt".format(aktueller_mutant_id)
if aktueller_mutant_id else "ABBRUCH: SIGTERM"
)
print(zeile)
bericht["befunde"] = list(befunde) + [zeile]
return 3
except RuecksetzenGescheitert as exc:
bericht["befunde"] = list(befunde) + [str(exc)]
return 2
@@ -830,6 +871,7 @@ def ausgabe_schreiben(bericht, args):
def main(argv=None):
signal.signal(signal.SIGTERM, _sigterm_werfen)
args = parse_args(argv)
bericht = {
"wurzel": args.wurzel,
@@ -125,6 +125,10 @@ Die Meldung ist `longrepr.reprcrash.message`, wenn es das gibt, sonst die erste
| `ZEIT` | Zeitlimit überschritten |
| `MUSTER` | Muster nicht genau einmal gefunden, nichts gefahren |
**Modus-Zeile (seit 2026-09-28, Nachbesserung N5):** Der Wrapper `mutanten_lauf_kopie.sh` schreibt vor dem eigentlichen Lauf eine eigene Zeile, unabhängig von `mutanten_lauf.py` selbst: `modus: voll` oder `modus: muster (nur Musterpruefung, kein Mutant gefahren)`. Damit weiß eine Linse, die nur die Rohausgabe sieht, ob überhaupt ein Mutant gefahren ist, bevor sie ein `OK` bewertet.
**SIGTERM (seit 2026-09-28, Nachbesserung N4):** `mutanten_lauf.py` registriert einen eigenen Handler für `SIGTERM`, der eine Ausnahme auslöst, damit die laufenden `finally`-Blöcke greifen: der zuletzt angewendete Mutant wird per SHA-Probe zurückgesetzt, ein laufender pytest-Prozess wird über seine Prozessgruppe beendet, und die Temp-Ordner (`mutanten_lauf_home_*`, `mutanten_lauf_plugin_*`) sowie die Ergebnisdatei werden geräumt. Vor dem Ende steht die Zeile `ABBRUCH: SIGTERM, Mutant <id> zurueckgesetzt` (oder ohne den Mutant-Teil, wenn beim Signal noch keiner lief). Der Exit-Code ist in diesem Fall 3. Geprüft durch Treiber-Fall 35.
Ausgabe auf stdout, UTF-8, je Mutant eine Zeile: `D1 OK DA::test_x=ROT(<meldung, 120 Zeichen>) …`. Tests erscheinen in der Kurzform aus `rot_bei`. Ein vorhandener `hinweis` folgt in der nächsten Zeile, eingerückt. Oben stehen Wurzel, Mutantendatei, Anzahl gesamt und ausgewählt und je Interpreter die Python-Version. Unten eine gerechnete Zusammenfassung: `ergebnis: <n> Mutanten, <a> OK, <b> LUECKE, <c> UNKLAR, <d> ZEIT, <e> MUSTER`.
`--bericht` schreibt dasselbe als JSON: `{"wurzel", "mutanten_datei", "befunde": [...], "grundlinie": {knoten: {ergebnis, meldung}}, "mutanten": [{"id", "datei", "status", "hinweis", "treffer", "tests": [{"eintrag", "knoten", "ergebnis", "meldung"}]}], "zusammenfassung": {...}}`. Der Bericht wird auch bei einem Abbruch geschrieben, mit allem, was bis dahin feststand.
@@ -146,6 +150,8 @@ Jeder pytest-Aufruf bekommt eine Kopie der Umgebung mit diesen Änderungen:
- `http_proxy`, `https_proxy`, `all_proxy` und dieselben in Großschrift zeigen auf `http://127.0.0.1:9`; `no_proxy` und `NO_PROXY` werden entfernt. Bibliotheken, die Proxy-Variablen lesen, laufen damit gegen einen geschlossenen Port.
- `PYTHONDONTWRITEBYTECODE=1`, `COLUMNS=300`, `MUTANTEN_LAUF_ERGEBNIS=<temp-datei>`, `PYTHONPATH` mit dem Plugin-Ordner vorn.
**Netzsperre (seit 2026-09-28, Nachbesserung N1):** Der Wrapper `mutanten_lauf_kopie.sh` startet `mutanten_lauf.py` selbst unter `unshare --map-current-user --net`, nicht erst den einzelnen pytest-Aufruf. Gemessen in WSL Debian als Benutzer `claude` (util-linux 2.41.5): Die UID bleibt innen wie außen 1000, es gibt nur die Schnittstelle `lo`, `connect` auf eine Adresse außerhalb von localhost endet mit `OSError` (errno 101) oder `gaierror`. Nicht `-r` (macht UID 0, könnte Rechte-Tests der Mutanten selbst verfälschen). Fehlt `unshare` ohne Netz, bricht der Wrapper vorher mit `MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich` ab, statt einen ungesperrten Lauf durchzulassen.
**Was das nicht abdichtet:** einen Unterprozess mit absolutem Pfad (so kam der Vorfall vom 2026-09-26 zustande), einen Socket ohne Proxy, einen Client mit `trust_env=False` oder mit eigenem Transport, Dateien mit absolutem Pfad außerhalb von `HOME`. Die eigentliche Sperre bleibt die Netzsperre der Testdateien selbst und eine Kopie ohne Geheimnisdateien (`ohne`).
Alle Temp-Ordner liegen außerhalb der Wurzel und werden am Ende entfernt, auch nach einem Abbruch.
@@ -190,6 +196,7 @@ Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testd
| 32 | Abdichtung: ein Test prüft, dass `HOME` nicht das echte ist und `https_proxy` gesetzt ist | Grundlinie grün, Exit 0 |
| 33 | `--bericht` schreibt gültiges JSON mit den Status aus Fall 2 | Datei lesbar, Status stimmt |
| 34 | `python_je_datei` setzt für eine Testdatei einen zweiten Interpreter (derselbe Pfad über einen anderen Namen, etwa eine Verknüpfung im Temp) | Exit 0 |
| 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 |
Der Treiber gibt je Fall eine Zeile aus (`Fall 5 ok` oder `Fall 5 FEHLER: erwartet …, bekommen …`) und am Ende die gerechnete Zahl der Fehler. Exit 0 nur, wenn alle Fälle wie erwartet ausgehen und jedes Mal byte-gleich zurückgesetzt wurde.
+36 -1
View File
@@ -9,6 +9,24 @@
# Grenze meldet die Zeile ZEITGRENZE, die bis dahin gefahrenen Mutanten stehen in der Ausgabe.
# Beide Pfade sind Windows-Pfade (auch Kurznamen wie PASCAL~1),
# die dieses Skript selbst mit wslpath in WSL-Pfade umwandelt.
#
# Nachbesserung 2026-09-28 (Pruefbericht vom selben Tag, Befunde 1 bis 4, 7):
# - Netzsperre: mutanten_lauf.py laeuft in einem eigenen Netz-Namensraum
# (unshare --map-current-user --net). Gemessen in WSL Debian als Benutzer claude
# (util-linux 2.41.5): UID innen bleibt 1000 wie aussen, nur Schnittstelle lo, jeder
# Verbindungsversuch nach aussen (etwa 127.0.0.1:8144, wo das Dashboard lauscht) scheitert.
# Fehlt unshare ohne Netz, bricht das Skript vorher ab statt einen ungesperrten Mutantenlauf
# still durchzulassen.
# - Ungepufferte Ausgabe: python3 -u, damit bei einer Zeitgrenze die bis dahin gefahrenen
# Mutanten wirklich in der Ausgabe stehen.
# - Zeitlimit je Mutant: ZEITLIMIT_S (Vorgabe 120 s, kleiner als BUDGET_S) geht als --zeitlimit
# an mutanten_lauf.py; B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant.
# - Modus-Zeile vor dem Lauf ("modus: voll" / "modus: muster (...)"), damit die Linse weiss, ob
# ueberhaupt ein Mutant gefahren ist.
# - Aufraeumen bei SIGTERM: timeout -k 15 schickt nach der Frist noch ein SIGKILL hinterher, falls
# mutanten_lauf.py nach dem SIGTERM nicht rechtzeitig endet; sein eigener SIGTERM-Handler setzt
# die Zieldatei vorher zurueck und raeumt seine Temp-Ordner auf.
# - chmod 700 auf die Kopie der Kopie direkt nach dem Kopieren.
set -u
KOPIE_WIN="${1:-}"
@@ -25,6 +43,17 @@ if [ "$MODUS" != "voll" ] && [ "$MODUS" != "muster" ]; then
exit 2
fi
BUDGET_S="${BUDGET_S:-540}"
ZEITLIMIT_S="${ZEITLIMIT_S:-120}"
if ! [ "$ZEITLIMIT_S" -lt "$BUDGET_S" ] 2>/dev/null; then
echo "MUTANTEN_LAUF_FEHLER: ZEITLIMIT_S ($ZEITLIMIT_S) muss kleiner sein als BUDGET_S ($BUDGET_S)"
exit 2
fi
if ! unshare --map-current-user --net true 2>/dev/null; then
echo "MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich"
exit 2
fi
KOPIE=$(wslpath -u "$KOPIE_WIN")
PLANORDNER=$(wslpath -u "$PLANORDNER_WIN")
@@ -48,14 +77,20 @@ TMPKOPIE=$(mktemp -d /tmp/plankritik-mut-XXXXXX)
trap 'rm -rf "$TMPKOPIE"' EXIT
cp -a "$KOPIE/." "$TMPKOPIE/"
chmod 700 "$TMPKOPIE"
find "$TMPKOPIE" -name __pycache__ -type d -prune -exec rm -rf {} +
MUSTER_SCHALTER=""
if [ "$MODUS" = "muster" ]; then
MUSTER_SCHALTER="--nur-muster"
echo "modus: muster (nur Musterpruefung, kein Mutant gefahren)"
else
echo "modus: voll"
fi
timeout "$BUDGET_S" python3 /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py --wurzel "$TMPKOPIE" --mutanten "$MUTANTEN_TOML" $MUSTER_SCHALTER
timeout -k 15 "$BUDGET_S" unshare --map-current-user --net \
python3 -u /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py \
--wurzel "$TMPKOPIE" --mutanten "$MUTANTEN_TOML" --zeitlimit "$ZEITLIMIT_S" $MUSTER_SCHALTER
EXITCODE=$?
if [ "$EXITCODE" -eq 124 ]; then
echo "ZEITGRENZE: Mutantenlauf nach $BUDGET_S s abgebrochen; Mutanten ohne Zeile oben sind nicht gefahren"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Treiber fuer mutanten_lauf.py: 34 Angriffsfaelle auf einer Wegwerfkopie.
"""Treiber fuer mutanten_lauf.py: 35 Angriffsfaelle auf einer Wegwerfkopie.
Zweck: Baut in einem Temp-Ordner unter /tmp ein kleines Testprojekt, fuehrt
mutanten_lauf.py je Fall mit einer eigenen mutanten.toml aus und vergleicht
@@ -11,14 +11,21 @@ Aufruf: python mutanten_lauf_treiber.py
Baut nichts ausserhalb von /tmp, geht nicht ins Netz, raeumt am Ende auf.
Spezifikation: .vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md, Abschnitt 6.
Fall 35 (Nachbesserung 2026-09-28, N11) prueft das Aufraeumen bei SIGTERM: Ein
Mutant haengt, der Laeufer bekommt nach wenigen Sekunden selbst ein SIGTERM.
Damit die Pruefung nur die Reste dieses einen Falls sieht, bekommt der Laeufer
dafuer ein eigenes TMPDIR (tempfile respektiert die Umgebungsvariable).
"""
import hashlib
import os
import shutil
import signal
import subprocess
import sys
import tempfile
import time
from pathlib import Path
HIER = Path(__file__).resolve().parent
@@ -930,11 +937,84 @@ def fall_34(basis, projektpfad):
shutil.rmtree(alt_python_dir, ignore_errors=True)
def fall_35(basis, projektpfad):
# N11: Ein Mutant haengt (Endlosschleife), der Laeufer selbst bekommt nach wenigen
# Sekunden SIGTERM. Erwartet: der Laeufer raeumt auf und meldet ABBRUCH: SIGTERM,
# kein pytest-Prozess dieses Falls bleibt am Leben, keine Temp-Reste bleiben liegen,
# und die Zieldatei ist danach byte-gleich mit dem Ausgangsstand (generisch geprueft
# durch pruefe_unveraendert im main()).
toml = projektpfad / "mutanten_35.toml"
schreibe_toml(toml, (
kopf() +
"[[mutant]]\n"
"id = \"M35\"\n"
"datei = \"paket/modul_zeit.py\"\n"
"aus = '''\n"
" return n + 1\n"
"'''\n"
"wird = '''\n"
" while True:\n"
" pass\n"
"'''\n"
"rot_bei = [\"tests/test_zeit.py::test_f7\"]\n"
))
eigenes_tmpdir = Path(tempfile.mkdtemp(prefix="mutanten_lauf_fall35_tmpdir_"))
env = os.environ.copy()
env["TMPDIR"] = str(eigenes_tmpdir)
cmd = [INTERPRETER, str(LAEUFER), "--wurzel", str(basis), "--mutanten", str(toml), "--zeitlimit", "60"]
ok = True
zusatz = ""
try:
proc = subprocess.Popen(cmd, cwd=str(basis), env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
time.sleep(3)
proc.send_signal(signal.SIGTERM)
try:
stdout, _ = proc.communicate(timeout=30)
except subprocess.TimeoutExpired:
proc.kill()
stdout, _ = proc.communicate()
ok = False
zusatz += "Laeufer endete nicht nach SIGTERM; "
if "ABBRUCH: SIGTERM" not in stdout:
ok = False
zusatz += "keine ABBRUCH-Zeile; "
# kein Prozess dieses Falls lebt danach: Sein HOME liegt unter eigenes_tmpdir,
# und diese Umgebungsvariable geht an den pytest-Unterprozess mit.
uebrige_pids = []
for envdatei in Path("/proc").glob("[0-9]*/environ"):
try:
inhalt = envdatei.read_bytes()
except OSError:
continue
if str(eigenes_tmpdir).encode() in inhalt:
uebrige_pids.append(envdatei.parent.name)
if uebrige_pids:
ok = False
zusatz += "Prozesse mit HOME im Fall-Tmpdir noch da: {0}; ".format(uebrige_pids)
# keine Temp-Ordner und keine Ergebnisdatei aus diesem Lauf
uebrige_ordner = (
list(eigenes_tmpdir.glob("mutanten_lauf_home_*"))
+ list(eigenes_tmpdir.glob("mutanten_lauf_plugin_*"))
+ list(eigenes_tmpdir.glob("mutanten_lauf_ergebnis_*"))
)
if uebrige_ordner:
ok = False
zusatz += "Temp-Reste: {0}; ".format(uebrige_ordner)
melde("Fall 35", ok, zusatz + "stdout(Ende)={0}".format(stdout[-500:]))
finally:
shutil.rmtree(eigenes_tmpdir, ignore_errors=True)
FAELLE = [
fall_01, fall_02, fall_03, fall_04, fall_05, fall_06, fall_07, fall_08, fall_09, fall_10,
fall_11, fall_12, fall_13, fall_14, fall_15, fall_16, fall_17, fall_18, fall_19, fall_20,
fall_21, fall_22, fall_23, fall_24, fall_25, fall_26, fall_27, fall_28, fall_29, fall_30,
fall_31, fall_32, fall_33, fall_34,
fall_31, fall_32, fall_33, fall_34, fall_35,
]
@@ -542,6 +542,97 @@ fall('plankritik', 'ZEITGRENZE im Mutantenlauf: Linse erfährt es, Hilfsagent f
return fehler
})
// Fälle für die Nachbesserung vom 2026-09-28 (N5, N7, N8, N10): fünf Mutanten aus
// eigene_mutanten.py töten, dazu Modus muster, Exit 2/124 und mutanten_status/mutanten_modus.
fall('plankritik', 'Modus muster wird als muster an den Wrapper übergeben, nicht als voll', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ ...PK_ARGS, mutanten: 'muster' }, {})
const hilfe = mitLabel(p, 'werkzeug:mutanten')[0]
const zeile = (hilfe ? hilfe.prompt.split('\n').find(z => z.includes('mutanten_lauf_kopie.sh')) : '') || ''
soll(/ muster$/.test(zeile), 'Befehl endet nicht auf muster: ' + zeile)
return fehler
})
fall('plankritik', 'Mutantenstufe läuft auch mit materialisierter Kopie ohne args.kopie', async () => {
const { fehler, soll } = pruefe()
const args = { projekt: 'D:/x', planordner: 'D:/x/plan', materialisieren: { ohne: ['config/secrets.toml'] } }
const { protokoll: p } = await PK(args, {})
soll(mitLabel(p, 'werkzeug:mutanten').length === 1, 'Mutantenlauf lief nicht mit materialisierter Kopie')
return fehler
})
fall('plankritik', 'arithmetik bekommt mehr als die letzten fünf Zeilen des Mutantenlaufs', async () => {
const { fehler, soll } = pruefe()
const marke = 'MARKE-VOR-DEM-ENDE'
const fuellzeilen = Array.from({ length: 50 }, (_, i) => 'zeile ' + i).join('\n')
const ausgabe = `wurzel: /tmp/x\n${marke}\n${fuellzeilen}\nEXIT mutanten_lauf: 0`
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: ausgabe })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && arith.prompt.includes(marke), 'Marke außerhalb der letzten fünf Zeilen fehlt im Auftrag')
return fehler
})
fall('plankritik', 'mutanten_lauf in der Rückgabe ist bei einem Lauf nicht null', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: e } = await PK(PK_ARGS, {})
soll(e && e.mutanten_lauf !== null && /EXIT mutanten_lauf/.test(e.mutanten_lauf), 'mutanten_lauf: ' + JSON.stringify(e && e.mutanten_lauf))
return fehler
})
fall('plankritik', 'Wurzel im Mutanten-Befehl ist ctx.kopie, nie args.projekt', async () => {
const { fehler, soll } = pruefe()
const { protokoll: p } = await PK({ ...PK_ARGS, projekt: 'D:/anderes-projekt' }, {})
const hilfe = mitLabel(p, 'werkzeug:mutanten')[0]
soll(hilfe && hilfe.prompt.includes(PK_ARGS.kopie) && !hilfe.prompt.includes('anderes-projekt'), 'Befehl nennt args.projekt statt ctx.kopie: ' + (hilfe && hilfe.prompt))
return fehler
})
fall('plankritik', 'modus muster: Linse erfährt, dass kein Mutant gefahren ist', async () => {
const { fehler, soll } = pruefe()
const ausgabe = 'wurzel: /tmp/x\nmodus: muster (nur Musterpruefung, kein Mutant gefahren)\nM1 OK Trefferzahl=1\nEXIT mutanten_lauf: 0'
const { protokoll: p } = await PK({ ...PK_ARGS, mutanten: 'muster' }, { mutantenAusgabe: ausgabe })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && /Kein Mutant ist gefahren/.test(arith.prompt), 'Hinweis auf Musterlauf fehlt')
soll(arith && !/prüfe bei OK an der Fehlermeldung/.test(arith.prompt), 'Satz "prüfe bei OK" steht trotz Modus muster noch da')
return fehler
})
fall('plankritik', 'Exit 2 des Mutantenlaufs: Linse erfährt, dass er vor oder während der Mutanten gescheitert ist', async () => {
const { fehler, soll } = pruefe()
const ausgabe = 'wurzel: /tmp/x\nbefunde:\n - x\nEXIT mutanten_lauf: 2'
const { protokoll: p } = await PK(PK_ARGS, { mutantenAusgabe: ausgabe })
const arith = mitLabel(p, 'plankritik:arithmetik')[0]
soll(arith && /vor oder während der Mutanten gescheitert \(Exit 2\)/.test(arith.prompt) && /Schwere wichtig/.test(arith.prompt), 'Hinweis auf Exit 2 fehlt: ' + (arith && arith.prompt.slice(-400)))
return fehler
})
fall('plankritik', 'mutanten_status und mutanten_modus in allen Zuständen', async () => {
const { fehler, soll } = pruefe()
const { ergebnis: eAus } = await PK({ ...PK_ARGS, mutanten: 'aus' }, {})
soll(eAus && eAus.mutanten_status === 'aus' && eAus.mutanten_modus === 'aus', 'aus: ' + JSON.stringify(eAus && [eAus.mutanten_status, eAus.mutanten_modus]))
const { ergebnis: eOhneKopie } = await PK({ projekt: 'D:/x', planordner: 'D:/x/plan' }, {})
soll(eOhneKopie && eOhneKopie.mutanten_status === 'ohne_kopie' && eOhneKopie.mutanten_modus === 'voll', 'ohne_kopie: ' + JSON.stringify(eOhneKopie && [eOhneKopie.mutanten_status, eOhneKopie.mutanten_modus]))
const { ergebnis: eKeineToml } = await PK(PK_ARGS, { mutantenAusgabe: 'KEINE_MUTANTEN_TOML' })
soll(eKeineToml && eKeineToml.mutanten_status === 'keine_toml', 'keine_toml: ' + JSON.stringify(eKeineToml && eKeineToml.mutanten_status))
const { ergebnis: eOhneErgebnis } = await PK(PK_ARGS, { mutantenAusgabe: 'irgendeine unbrauchbare Ausgabe ohne Marker' })
soll(eOhneErgebnis && eOhneErgebnis.mutanten_status === 'ohne_ergebnis', 'ohne_ergebnis: ' + JSON.stringify(eOhneErgebnis && eOhneErgebnis.mutanten_status))
const { ergebnis: eExit2 } = await PK(PK_ARGS, { mutantenAusgabe: 'EXIT mutanten_lauf: 2' })
soll(eExit2 && eExit2.mutanten_status === 'exit_2', 'exit_2: ' + JSON.stringify(eExit2 && eExit2.mutanten_status))
const { ergebnis: eExitDefault } = await PK(PK_ARGS, {})
soll(eExitDefault && eExitDefault.mutanten_status === 'exit_0' && eExitDefault.mutanten_modus === 'voll', 'exit_0: ' + JSON.stringify(eExitDefault && [eExitDefault.mutanten_status, eExitDefault.mutanten_modus]))
const { ergebnis: eMuster } = await PK({ ...PK_ARGS, mutanten: 'muster' }, {})
soll(eMuster && eMuster.mutanten_modus === 'muster', 'muster: ' + JSON.stringify(eMuster && eMuster.mutanten_modus))
return fehler
})
// ---------------------------------------------------------------- pruefung
const PR_ARGS = { projekt: 'D:/x', plan: 'D:/x/plan' }
const PR = (a, k, o = {}) => fuehreAus(WF('pruefung'), a, antworten(k), { ordner: ORDNER, ...o })
@@ -72,11 +72,13 @@ MUTANTEN = [
("plankritik", "doppelte Linse erlaubt", " if (a.linsen && new Set(a.linsen).size !== a.linsen.length) throw", " if (false) throw"),
("plankritik", "Mutantenlauf-Rohausgabe geht nicht an arithmetik", "${mutantenText(ctx.mutantenLauf)}`,", "`,"),
("plankritik", "MSYS_NO_PATHCONV fehlt im Mutantenlauf-Befehl", "`MSYS_NO_PATHCONV=1 wsl -d Debian", "`wsl -d Debian"),
("plankritik", "Zeitgrenze des Mutantenlaufs verschwiegen", " const grenze = /ZEITGRENZE/.test(m.rohausgabe) ?", " const grenze = false ?"),
("plankritik", "Zeitgrenze des Mutantenlaufs verschwiegen", "if (/ZEITGRENZE/.test(m.rohausgabe)) hinweis =", "if (false) hinweis ="),
("plankritik", "Hilfsagent ohne Zeitvorgabe", ", 'Setze im Bash-Werkzeug den Parameter timeout auf 600000; der Befehl darf bis zu zehn Minuten laufen.')", ")"),
("welle-mit-pruefung", "status fertig ohne Pruefung", "status: pruefung ? 'fertig' : 'ohne_pruefung',", "status: 'fertig',"),
("welle-mit-pruefung", "Ablesung ohne try", " try {\n r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'welle-mit-pruefung:' + anlass, wunsch: 0 })\n } catch (e) {\n r = { status: 'nicht_gelesen', grund: 'verbrauch.js nicht ausführbar: ' + (e && e.message) }\n }", " r = await workflow({ scriptPath: VERBRAUCH }, { anlass: 'welle-mit-pruefung:' + anlass, wunsch: 0 })"),
("verbrauch", "5-Stunden-Regex unverankert", "const FUENF_STUNDEN = /^\\s*5[- ]?hour/i", "const FUENF_STUNDEN = /hour/i"),
("plankritik", "Muster-Satz fehlt (N5)", "musterModus\n ? ' Kein Mutant ist gefahren; OK heißt hier nur, dass das Muster genau einmal in der Datei steht; ob ein Test fällt, ist unbelegt.'\n : ", ""),
("plankritik", "Exit 2 wird wie 0 behandelt (N7)", "exitCode === 2 || exitCode === 3", "false"),
]