Jedes Zeichen der Kategorie Cc ausser LF und jedes der Kategorie Cf in .claude/workflows/**/*.js, gelesen als Bytes. Anlass: plankritik.js mit CRLF scheiterte am Workflow-Werkzeug. Der Hook nach dem Bearbeiten loest auch bei Workflows aus. Regressionslauf 28/28 angeschlagen, 6/6 stumm; Hook-Treiber 58/58; Vorfall in exakter Form gemeldet (U+000D). Co-Authored-By: Claude Opus 5.5 <[email protected]>
553 lines
23 KiB
Python
553 lines
23 KiB
Python
"""Fährt die Angriffsliste gegen regelwerk_nach_edit.py, je Fall auf einem Wegwerf-Vault.
|
|
|
|
Ersteller: Pascal Michel
|
|
Erstellt: 2026-09-13
|
|
Entwurf: ENTWURF.md im selben Ordner
|
|
|
|
Statt des echten regelwerk_check.py liegt im Wegwerf-Vault eine Attrappe. Sie
|
|
schreibt ihren Aufruf nach aufruf.json und zeigt dann ein festgelegtes
|
|
Verhalten: grün, Befund, Abbruch, ACHTUNG, stumm, Absturz, hängt, sehr viel
|
|
Ausgabe. Ihre Zeilen haben die Form der echten Ausgabe (regelwerk_check.py
|
|
Zeilen 114, 368, 370 und 372). So lässt sich je Fall unterscheiden, ob der
|
|
Hook still blieb, ohne die Prüfung zu starten, oder still blieb, nachdem sie
|
|
grün war.
|
|
|
|
Grundlinie zuerst: Ein grüner Check muss still bleiben und nachweislich
|
|
gelaufen sein, ein Befund muss gemeldet werden. Sonst misst der Rest nichts.
|
|
|
|
Seit der Nachbesserung Runde 2 bestimmt regelwerk_nach_edit.py seine
|
|
Vault-Wurzel ausschließlich aus dem eigenen Skriptpfad, das Feld cwd der
|
|
Hook-Eingabe zählt nicht mehr (Prüferbefund Runde 2, Koordinatorentscheidung).
|
|
lauf_fall() kopiert den Hook deshalb für jeden Fall zuerst in den Wegwerf-
|
|
Vault, statt das Original mit einem cwd-Feld dorthin zu verweisen.
|
|
|
|
Aufruf:
|
|
python .vault-tools/session-hooks/faelle_nach_edit.py
|
|
python .vault-tools/session-hooks/faelle_nach_edit.py --echt
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
HIER = Path(__file__).resolve().parent
|
|
HOOK = HIER / "regelwerk_nach_edit.py"
|
|
MAX_KONTEXT = 4100
|
|
|
|
FAKE_KOPF = '''import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
Path(__file__).resolve().parents[2].joinpath("aufruf.json").write_text(
|
|
json.dumps({"argv": sys.argv[1:], "exe": sys.executable}), encoding="utf-8")
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
'''
|
|
|
|
FAKES = {
|
|
"gruen": FAKE_KOPF + '''print()
|
|
print("Keine der 6 gelaufenen Prüfungen hat angeschlagen.")
|
|
sys.exit(0)
|
|
''',
|
|
"befund": FAKE_KOPF + '''print("[klein] AI/Regelwerk/regel.md — status: pending-review — Freigabe steht aus")
|
|
print()
|
|
print("1 Befund(e) aus 6 von 6 Prüfungen.")
|
|
sys.exit(1)
|
|
''',
|
|
"abbruch": FAKE_KOPF + '''print("[blockierend] regelwerk_check.py — Abbruch: KeyError: 'x'")
|
|
print()
|
|
print("1 Befund(e) aus 2 von 6 Prüfungen.")
|
|
sys.exit(2)
|
|
''',
|
|
"achtung": FAKE_KOPF + '''print()
|
|
print("Keine der 5 gelaufenen Prüfungen hat angeschlagen.")
|
|
print("ACHTUNG: nur 5 von 6 Prüfungen sind gelaufen — der Rest hatte keine Voraussetzung.")
|
|
sys.exit(0)
|
|
''',
|
|
"stumm": FAKE_KOPF + '''sys.exit(0)
|
|
''',
|
|
"absturz": FAKE_KOPF + '''raise RuntimeError("Absturz im Check")
|
|
''',
|
|
"haengt": FAKE_KOPF + '''time.sleep(120)
|
|
''',
|
|
"viel": FAKE_KOPF + '''for i in range(200):
|
|
print("[wichtig] AI/Regelwerk/regel.md — toter Wikilink: [[ziel%d]]" % i)
|
|
print()
|
|
print("200 Befund(e) aus 6 von 6 Prüfungen.")
|
|
sys.exit(1)
|
|
''',
|
|
}
|
|
|
|
|
|
def schreibe(vault: Path, rel: str, inhalt: str | bytes) -> None:
|
|
# Immer Bytes: write_text würde unter Windows jedes LF zu CRLF machen.
|
|
pfad = vault / rel
|
|
pfad.parent.mkdir(parents=True, exist_ok=True)
|
|
pfad.write_bytes(inhalt if isinstance(inhalt, bytes) else inhalt.encode("utf-8"))
|
|
|
|
|
|
def baue_vault(v: Path, fake: str | None) -> None:
|
|
schreibe(v, "CLAUDE.md", "# Stamm\n")
|
|
schreibe(v, "AI/Regelwerk/regel.md", "---\nstatus: reference\n---\n")
|
|
schreibe(v, "AI/Regelwerk/unter/tief.md", "x\n")
|
|
schreibe(v, ".claude/agents/planer.md", "---\nname: planer\n---\n")
|
|
schreibe(v, ".claude/skills/demo/SKILL.md", "---\nname: demo\n---\n")
|
|
schreibe(v, ".claude/workflows/demo.js", "export const meta = {}\n")
|
|
schreibe(v, "Inbox/notiz.md", "x\n")
|
|
schreibe(v, "Code/CLAUDE.md", "x\n")
|
|
if fake is not None:
|
|
schreibe(v, ".vault-tools/regelwerk-check/regelwerk_check.py", FAKES[fake])
|
|
|
|
|
|
def vorwaerts(pfad: Path) -> str:
|
|
return str(pfad).replace("\\", "/")
|
|
|
|
|
|
def laufwerk_getauscht(pfad: Path) -> str:
|
|
s = str(pfad)
|
|
if re.match(r"^[A-Za-z]:", s):
|
|
return s[0].swapcase() + s[1:]
|
|
return s
|
|
|
|
|
|
def j(daten: object) -> bytes:
|
|
return json.dumps(daten).encode("utf-8")
|
|
|
|
|
|
def edit(v: Path, datei: object, **extra: object) -> bytes:
|
|
daten: dict[str, object] = {
|
|
"session_id": "faelle",
|
|
"transcript_path": "",
|
|
"cwd": str(v),
|
|
"permission_mode": "default",
|
|
"hook_event_name": "PostToolUse",
|
|
"tool_name": "Edit",
|
|
"tool_input": {"file_path": datei},
|
|
"tool_response": {},
|
|
}
|
|
daten.update(extra)
|
|
return j(daten)
|
|
|
|
|
|
def ohne_cwd(v: Path, datei: object) -> bytes:
|
|
daten = json.loads(edit(v, datei))
|
|
del daten["cwd"]
|
|
return j(daten)
|
|
|
|
|
|
@dataclass
|
|
class Fall:
|
|
name: str
|
|
fake: str | None
|
|
stdin: Callable[[Path], bytes]
|
|
erwartung: str
|
|
enthaelt: list[str] = field(default_factory=list)
|
|
system_enthaelt: str = ""
|
|
max_dauer: float = 10.0
|
|
# None: Subprozess erbt die Umgebung dieses Treibers unveraendert (bisheriges
|
|
# Verhalten). Gesetzt: ersetzt die Umgebung vollstaendig (Pruefbefunde Runde 2).
|
|
umgebung: Callable[[Path], dict[str, str]] | None = None
|
|
|
|
|
|
BEFUND = ["[klein] AI/Regelwerk/regel.md"]
|
|
BEFUND_SYSTEM = "Exit 1, 1 Befundzeile(n)"
|
|
|
|
|
|
def still(name: str, stdin: Callable[[Path], bytes]) -> Fall:
|
|
return Fall(name, "befund", stdin, "still-ohne-lauf")
|
|
|
|
|
|
def meldet(name: str, stdin: Callable[[Path], bytes]) -> Fall:
|
|
return Fall(name, "befund", stdin, "meldung", BEFUND, BEFUND_SYSTEM)
|
|
|
|
|
|
def cwd_zeigt_anderswo(v: Path) -> bytes:
|
|
"""cwd zeigt auf einen anderen, echten (leeren) Ordner. Seit der
|
|
Nachbesserung Runde 2 liest der Hook cwd gar nicht mehr - die Wurzel ist
|
|
ausschliesslich der Ordner, in dem der Hook selbst liegt (hier: v, weil
|
|
lauf_fall() ihn dorthin kopiert). Das Ergebnis muss trotzdem melden,
|
|
nicht still bleiben (Pruefbefund Runde 2)."""
|
|
anderswo = v.parent / "anderswo"
|
|
anderswo.mkdir(parents=True, exist_ok=True)
|
|
return edit(v, str(v / "AI" / "Regelwerk" / "regel.md"), cwd=str(anderswo))
|
|
|
|
|
|
def ohne_pythonioencoding(_: Path) -> dict[str, str]:
|
|
"""Claude Code setzt PYTHONIOENCODING beim Hook-Aufruf nicht; nur die globale
|
|
Planvorgabe setzt sie vor dem Aufruf dieses Treibers selbst (Pruefbefund Runde 2)."""
|
|
umgebung = dict(os.environ)
|
|
umgebung.pop("PYTHONIOENCODING", None)
|
|
return umgebung
|
|
|
|
|
|
GRUNDLINIE = [
|
|
Fall("GRUNDLINIE | gruener Check bleibt still und lief", "gruen",
|
|
lambda v: edit(v, str(v / "CLAUDE.md")), "still-nach-lauf"),
|
|
Fall("GRUNDLINIE | Befund wird gemeldet", "befund",
|
|
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung", BEFUND, BEFUND_SYSTEM),
|
|
]
|
|
|
|
FAELLE = [
|
|
still("leer | stdin leer", lambda v: b""),
|
|
still("leer | stdin nur Leerraum", lambda v: b" \r\n"),
|
|
still("falscher Typ| stdin kein JSON", lambda v: b"kein json"),
|
|
still("kodierung | stdin ungueltiges UTF-8", lambda v: b"\xff\xfe\x00"),
|
|
still("falscher Typ| stdin ist eine Liste",
|
|
lambda v: j([json.loads(edit(v, str(v / "CLAUDE.md")))])),
|
|
still("falscher Typ| stdin ist eine Zahl", lambda v: b"5"),
|
|
still("fehlt | tool_input fehlt", lambda v: j({"cwd": str(v), "tool_name": "Edit"})),
|
|
still("null | tool_input ist null", lambda v: edit(v, "x", tool_input=None)),
|
|
still("falscher Typ| tool_input ist eine Liste",
|
|
lambda v: edit(v, "x", tool_input=[str(v / "CLAUDE.md")])),
|
|
still("fehlt | file_path fehlt", lambda v: edit(v, "x", tool_input={"content": "a"})),
|
|
still("null | file_path ist null", lambda v: edit(v, None)),
|
|
still("falscher Typ| file_path ist eine Zahl", lambda v: edit(v, 5)),
|
|
still("falscher Typ| file_path ist eine Liste", lambda v: edit(v, [str(v / "CLAUDE.md")])),
|
|
still("leer | file_path ist leer", lambda v: edit(v, "")),
|
|
still("wertebereich| file_path mit Nullzeichen",
|
|
lambda v: edit(v, str(v / "CLAUDE.md") + "\u0000")),
|
|
still("zusaetzlich | Pfad ausserhalb des Vaults",
|
|
lambda v: edit(v, str(v.parent / "anderswo" / "CLAUDE.md"))),
|
|
still("verweis | Nachbarordner mit gleichem Namensanfang",
|
|
lambda v: edit(v, str(v) + "-kopie\\CLAUDE.md")),
|
|
still("verweis | Datei neben dem Vault mit gleichem Namensanfang",
|
|
lambda v: edit(v, str(v) + "-CLAUDE.md")),
|
|
still("zusaetzlich | im Vault, ausserhalb des Geltungsbereichs",
|
|
lambda v: edit(v, str(v / "Inbox" / "notiz.md"))),
|
|
still("zusaetzlich | CLAUDE.md in einem Unterordner",
|
|
lambda v: edit(v, str(v / "Code" / "CLAUDE.md"))),
|
|
still("verweis | Ordnername mit gleichem Anfang",
|
|
lambda v: edit(v, str(v / "AI" / "RegelwerkAlt" / "x.md"))),
|
|
still("zusaetzlich | settings.json unter .claude",
|
|
lambda v: edit(v, str(v / ".claude" / "settings.json"))),
|
|
still("verweis | Ordnername mit gleichem Anfang wie workflows",
|
|
lambda v: edit(v, str(v / ".claude" / "workflowsAlt" / "x.js"))),
|
|
still("wertebereich| der Regelwerk-Ordner selbst",
|
|
lambda v: edit(v, str(v / "AI" / "Regelwerk"))),
|
|
still("verweis | .. fuehrt aus dem Regelwerk hinaus",
|
|
lambda v: edit(v, vorwaerts(v) + "/AI/Regelwerk/../../Inbox/notiz.md")),
|
|
still("verweis | .. fuehrt aus dem Vault hinaus",
|
|
lambda v: edit(v, vorwaerts(v) + "/../fremd/AI/Regelwerk/x.md")),
|
|
Fall("gruen | CLAUDE.md, Check ohne Befund", "gruen",
|
|
lambda v: edit(v, str(v / "CLAUDE.md")), "still-nach-lauf"),
|
|
Fall("gruen | Skill, Check ohne Befund", "gruen",
|
|
lambda v: edit(v, str(v / ".claude" / "skills" / "demo" / "SKILL.md")), "still-nach-lauf"),
|
|
meldet("pfad | Schraegstriche vorwaerts",
|
|
lambda v: edit(v, vorwaerts(v) + "/CLAUDE.md")),
|
|
meldet("pfad | Backslashes",
|
|
lambda v: edit(v, str(v) + "\\AI\\Regelwerk\\regel.md")),
|
|
meldet("pfad | Laufwerksbuchstabe in anderer Schreibung",
|
|
lambda v: edit(v, laufwerk_getauscht(v / "AI" / "Regelwerk" / "regel.md"))),
|
|
meldet("pfad | ganzer Pfad in Grossbuchstaben",
|
|
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "regel.md").upper())),
|
|
meldet("pfad | Unterordner im Regelwerk",
|
|
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "unter" / "tief.md"))),
|
|
meldet("pfad | Agentendatei",
|
|
lambda v: edit(v, str(v / ".claude" / "agents" / "planer.md"))),
|
|
meldet("pfad | Skilldatei",
|
|
lambda v: edit(v, str(v / ".claude" / "skills" / "demo" / "SKILL.md"))),
|
|
meldet("pfad | Workflowdatei",
|
|
lambda v: edit(v, str(v / ".claude" / "workflows" / "demo.js"))),
|
|
meldet("verweis | neue Datei, die es noch nicht gibt",
|
|
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "neu.md"))),
|
|
meldet("pfad | relativer Pfad", lambda v: edit(v, "AI/Regelwerk/regel.md")),
|
|
meldet("verweis | .. fuehrt ins Regelwerk hinein",
|
|
lambda v: edit(v, vorwaerts(v) + "/Inbox/../AI/Regelwerk/regel.md")),
|
|
meldet("zusaetzlich | Write statt Edit",
|
|
lambda v: edit(v, str(v / "CLAUDE.md"), tool_name="Write")),
|
|
meldet("zusaetzlich | MultiEdit",
|
|
lambda v: edit(v, str(v / "CLAUDE.md"), tool_name="MultiEdit")),
|
|
meldet("kodierung | ungueltiges UTF-8 im Pfad",
|
|
lambda v: edit(v, "PLATZHALTER").replace(
|
|
b"PLATZHALTER", vorwaerts(v).encode("utf-8") + b"/AI/Regelwerk/regel\xff.md")),
|
|
Fall("umgebung | ungueltiges UTF-8 im Pfad bleibt gueltiges JSON ohne PYTHONIOENCODING",
|
|
"befund",
|
|
lambda v: edit(v, "PLATZHALTER").replace(
|
|
b"PLATZHALTER", vorwaerts(v).encode("utf-8") + b"/AI/Regelwerk/regel\xff.md"),
|
|
"meldung", enthaelt=BEFUND, system_enthaelt=BEFUND_SYSTEM, umgebung=ohne_pythonioencoding),
|
|
Fall("umgebung | Nicht-ASCII-Pfad ohne PYTHONIOENCODING bleibt gueltiges UTF-8-JSON",
|
|
"befund",
|
|
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "Prüfüng Übersicht 🚀.md")),
|
|
"meldung", enthaelt=BEFUND, system_enthaelt=BEFUND_SYSTEM, umgebung=ohne_pythonioencoding),
|
|
meldet("pfad | cwd mit abschliessendem Backslash",
|
|
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=str(v) + "\\")),
|
|
meldet("fehlt | cwd fehlt, wird nicht mehr gelesen",
|
|
lambda v: ohne_cwd(v, str(v / "CLAUDE.md"))),
|
|
meldet("null | cwd ist null, wird nicht mehr gelesen",
|
|
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=None)),
|
|
meldet("falscher Typ| cwd ist eine Zahl, wird nicht mehr gelesen",
|
|
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=5)),
|
|
meldet("verweis | cwd existiert nicht, wird nicht mehr gelesen",
|
|
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=str(v / "gibtsnicht"))),
|
|
meldet("verweis | cwd zeigt auf einen anderen Ordner, Ergebnis bleibt der Skript-Vault",
|
|
cwd_zeigt_anderswo),
|
|
Fall("check | Exit 2, Abbruch im Check", "abbruch",
|
|
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
|
["Abbruch: KeyError"], "Exit 2, 1 Befundzeile(n)"),
|
|
Fall("check | Exit 0 mit ACHTUNG", "achtung",
|
|
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
|
["ACHTUNG: nur 5 von 6"], "Exit 0, 0 Befundzeile(n)"),
|
|
Fall("check | Exit 0 ohne Schlusszeile", "stumm",
|
|
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
|
["(keine Ausgabe)"], "Exit 0, 0 Befundzeile(n)"),
|
|
Fall("check | Check stuerzt ab", "absturz",
|
|
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
|
["--- stderr ---", "RuntimeError: Absturz im Check"], "Exit 1, 0 Befundzeile(n)"),
|
|
Fall("umgebung | regelwerk_check.py fehlt", None,
|
|
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
|
["regelwerk_check.py fehlt"], "Regelwerk-Check fehlt"),
|
|
Fall("umgebung | regelwerk_check.py haengt", "haengt",
|
|
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
|
["länger als 20 s"], "nach 20 s abgebrochen", max_dauer=28.0),
|
|
Fall("wertebereich| sehr lange Ausgabe wird gekuerzt", "viel",
|
|
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
|
["weitere Zeilen gekürzt"], "Exit 1, 200 Befundzeile(n)"),
|
|
]
|
|
|
|
|
|
def streng_utf8(roh: bytes) -> str:
|
|
# stdout muss gueltiges UTF-8 sein. Ein ungueltiges Byte darf nicht still
|
|
# ersetzt werden, sonst besteht der Umgebungsfall auch dann, wenn der Hook
|
|
# cp1252 schreibt (Pruefrunde 2, Befund zur Dekodierung).
|
|
try:
|
|
return roh.decode("utf-8")
|
|
except UnicodeDecodeError as exc:
|
|
return "UNGUELTIGES-UTF-8: %s" % exc
|
|
|
|
|
|
def fahre(
|
|
skript: Path,
|
|
eingabe: bytes,
|
|
arbeitsordner: Path,
|
|
umgebung: dict[str, str] | None = None,
|
|
) -> tuple[int, str, str, float]:
|
|
start = time.perf_counter()
|
|
lauf = subprocess.run(
|
|
[sys.executable, str(skript)],
|
|
input=eingabe,
|
|
capture_output=True,
|
|
cwd=str(arbeitsordner),
|
|
timeout=90,
|
|
env=umgebung,
|
|
)
|
|
dauer = time.perf_counter() - start
|
|
return (
|
|
lauf.returncode,
|
|
streng_utf8(lauf.stdout),
|
|
lauf.stderr.decode("utf-8", errors="replace"),
|
|
dauer,
|
|
)
|
|
|
|
|
|
def pruefe_aufruf(aufruf: Path, v: Path) -> list[str]:
|
|
if not aufruf.exists():
|
|
return ["Check wurde nicht gestartet"]
|
|
daten = json.loads(aufruf.read_text(encoding="utf-8"))
|
|
probleme: list[str] = []
|
|
if daten.get("argv") != ["--vault", str(v)]:
|
|
probleme.append(f"Aufruf-Argumente {daten.get('argv')!r} statt {['--vault', str(v)]!r}")
|
|
if os.path.normcase(str(daten.get("exe"))) != os.path.normcase(sys.executable):
|
|
probleme.append(f"Interpreter {daten.get('exe')!r} statt {sys.executable!r}")
|
|
return probleme
|
|
|
|
|
|
def pruefe_meldung(fall: Fall, out: str) -> list[str]:
|
|
roh = out.strip()
|
|
if not (roh.startswith("{") and roh.endswith("}")):
|
|
return [f"stdout ist kein JSON-Objekt: {out[:300]!r}"]
|
|
try:
|
|
daten = json.loads(roh)
|
|
except ValueError as exc:
|
|
return [f"stdout nicht parsebar: {exc}"]
|
|
if not isinstance(daten, dict) or set(daten) != {"hookSpecificOutput", "systemMessage"}:
|
|
art = sorted(daten) if isinstance(daten, dict) else type(daten).__name__
|
|
return [f"falsche Schluessel: {art}"]
|
|
innen = daten["hookSpecificOutput"]
|
|
system = daten["systemMessage"]
|
|
if not isinstance(innen, dict) or innen.get("hookEventName") != "PostToolUse":
|
|
return ["hookSpecificOutput.hookEventName ist nicht PostToolUse"]
|
|
kontext = innen.get("additionalContext")
|
|
if not isinstance(kontext, str) or not kontext:
|
|
return ["additionalContext fehlt oder ist leer"]
|
|
probleme: list[str] = []
|
|
if not isinstance(system, str) or not system or "\n" in system:
|
|
probleme.append(f"systemMessage ist keine einzelne Zeile: {system!r}")
|
|
elif fall.system_enthaelt not in system:
|
|
probleme.append(f"systemMessage ohne {fall.system_enthaelt!r}: {system!r}")
|
|
probleme += [f"Kontext ohne {t!r}" for t in fall.enthaelt if t not in kontext]
|
|
if len(kontext) > MAX_KONTEXT:
|
|
probleme.append(f"Kontext hat {len(kontext)} Zeichen, Grenze {MAX_KONTEXT}")
|
|
if probleme:
|
|
probleme.append("Kontext war:\n" + kontext)
|
|
return probleme
|
|
|
|
|
|
def pruefe(fall: Fall, v: Path, code: int, out: str, dauer: float) -> list[str]:
|
|
probleme: list[str] = []
|
|
if code != 0:
|
|
probleme.append(f"Exit {code} statt 0")
|
|
if dauer > fall.max_dauer:
|
|
probleme.append(f"Laufzeit {dauer:.1f} s ueber {fall.max_dauer:.0f} s")
|
|
aufruf = v / "aufruf.json"
|
|
if fall.erwartung == "still-ohne-lauf":
|
|
if out != "":
|
|
probleme.append(f"stdout nicht leer: {out[:300]!r}")
|
|
if aufruf.exists():
|
|
probleme.append("Check wurde gestartet, obwohl er nicht haette laufen duerfen")
|
|
elif fall.erwartung == "still-nach-lauf":
|
|
if out != "":
|
|
probleme.append(f"stdout nicht leer: {out[:300]!r}")
|
|
probleme += pruefe_aufruf(aufruf, v)
|
|
else:
|
|
probleme += pruefe_meldung(fall, out)
|
|
if fall.fake is not None:
|
|
probleme += pruefe_aufruf(aufruf, v)
|
|
return probleme
|
|
|
|
|
|
def lauf_fall(fall: Fall, arbeit: Path) -> tuple[bool, str]:
|
|
v = arbeit / "fall"
|
|
if v.exists():
|
|
shutil.rmtree(v)
|
|
baue_vault(v, fall.fake)
|
|
# Seit der Nachbesserung Runde 2 bestimmt regelwerk_nach_edit.py seine
|
|
# Vault-Wurzel ausschliesslich aus dem eigenen Skriptpfad, das Feld cwd
|
|
# der Hook-Eingabe zaehlt nicht mehr. Nur eine Kopie im Wegwerf-Vault sorgt
|
|
# dafuer, dass diese Wurzel auch tatsaechlich v ist statt des echten
|
|
# Vaults, in dem das Original unter HOOK liegt.
|
|
ziel = v / ".vault-tools" / "session-hooks"
|
|
ziel.mkdir(parents=True, exist_ok=True)
|
|
skript = ziel / HOOK.name
|
|
shutil.copy2(HOOK, skript)
|
|
umgebung = fall.umgebung(v) if fall.umgebung is not None else None
|
|
code, out, err, dauer = fahre(skript, fall.stdin(v), arbeit, umgebung)
|
|
kopf = f"exit={code} {dauer:.2f}s"
|
|
probleme = pruefe(fall, v, code, out, dauer)
|
|
if not probleme:
|
|
return True, kopf
|
|
text = kopf + "\n" + "\n".join(" " + p for p in probleme)
|
|
if err.strip():
|
|
text += "\n stderr: " + err.strip()[:800]
|
|
return False, text
|
|
|
|
|
|
def ip_hook_wirft(arbeit: Path) -> tuple[bool, str]:
|
|
"""Fehlerpfad im Hook selbst, von aussen nicht ausloesbar: im Prozess."""
|
|
v = arbeit / "fall"
|
|
if v.exists():
|
|
shutil.rmtree(v)
|
|
baue_vault(v, "befund")
|
|
# Kopie in v, wie in lauf_fall(): main() bestimmt die Wurzel seit Runde 2
|
|
# ausschliesslich aus dem eigenen Skriptpfad, der file_path unten zeigt
|
|
# aber in v hinein - ohne die Kopie faende main() keinen Geltungsbereich.
|
|
ziel = v / ".vault-tools" / "session-hooks"
|
|
ziel.mkdir(parents=True, exist_ok=True)
|
|
kopie = ziel / HOOK.name
|
|
shutil.copy2(HOOK, kopie)
|
|
spec = importlib.util.spec_from_file_location("hook_wirft", kopie)
|
|
modul = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(modul)
|
|
|
|
def wirft(wurzel: Path) -> None:
|
|
raise RuntimeError("Testfehler")
|
|
|
|
modul.fuehre_check = wirft
|
|
puffer = io.StringIO()
|
|
with contextlib.redirect_stdout(puffer):
|
|
code = modul.main(edit(v, str(v / "CLAUDE.md")))
|
|
erwartet = Fall("", "befund", lambda _: b"", "meldung",
|
|
["RuntimeError: Testfehler"], "regelwerk_nach_edit.py: RuntimeError")
|
|
probleme = pruefe_meldung(erwartet, puffer.getvalue())
|
|
if code != 0:
|
|
probleme.append(f"main gab {code} zurueck statt 0")
|
|
return not probleme, "\n".join(" " + p for p in probleme)
|
|
|
|
|
|
def echter_lauf() -> int:
|
|
vault = HIER.parent.parent
|
|
code, out, err, dauer = fahre(HOOK, edit(vault, str(vault / "CLAUDE.md")), HIER)
|
|
print(f"exit={code} Laufzeit {dauer:.2f} s")
|
|
if out.strip():
|
|
try:
|
|
print(json.dumps(json.loads(out), ensure_ascii=False, indent=2))
|
|
except ValueError:
|
|
print("FORMFEHLER, stdout ist kein JSON:")
|
|
print(out)
|
|
return 1
|
|
else:
|
|
print("(keine Ausgabe: Regelwerk-Check ohne Befund)")
|
|
if err.strip():
|
|
print("--- stderr ---")
|
|
print(err)
|
|
return 0 if code == 0 else 1
|
|
|
|
|
|
def main() -> int:
|
|
if not HOOK.is_file():
|
|
print(f"ABBRUCH: {HOOK} fehlt")
|
|
return 2
|
|
if "--echt" in sys.argv[1:]:
|
|
return echter_lauf()
|
|
|
|
arbeit = Path(tempfile.mkdtemp(prefix="nach-edit-faelle-")).resolve()
|
|
try:
|
|
print("=" * 78)
|
|
print("GRUNDLINIE")
|
|
print("=" * 78)
|
|
for fall in GRUNDLINIE:
|
|
ok, text = lauf_fall(fall, arbeit)
|
|
print(f"{'BESTANDEN' if ok else 'FEHLER':10} {fall.name} {text}")
|
|
if not ok:
|
|
print("ABBRUCH: Die Grundlinie ist nicht gruen. Der Rest misst nichts.")
|
|
return 2
|
|
|
|
print()
|
|
print("=" * 78)
|
|
print("ANGRIFFSLISTE")
|
|
print("=" * 78)
|
|
bestanden = 0
|
|
gesamt = 0
|
|
for fall in FAELLE:
|
|
gesamt += 1
|
|
try:
|
|
ok, text = lauf_fall(fall, arbeit)
|
|
except Exception as exc:
|
|
ok, text = False, f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}"
|
|
bestanden += int(ok)
|
|
print(f"{'BESTANDEN' if ok else 'FEHLER':10} {fall.name} {text}")
|
|
|
|
gesamt += 1
|
|
try:
|
|
ok, text = ip_hook_wirft(arbeit)
|
|
except Exception as exc:
|
|
ok, text = False, f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}"
|
|
bestanden += int(ok)
|
|
print(f"{'BESTANDEN' if ok else 'FEHLER':10} absturz | Hook selbst wirft (im Prozess) {text}")
|
|
|
|
print()
|
|
print(f"{bestanden} von {gesamt} Faellen bestanden.")
|
|
return 0 if bestanden == gesamt else 1
|
|
finally:
|
|
shutil.rmtree(arbeit, ignore_errors=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
except AttributeError:
|
|
pass
|
|
raise SystemExit(main())
|