fix(security): mutation_check ohne veralteten Bytecode

Gleich lange Mutationen (z. B. extra="forbid" -> "ignore") liefen mit altem .pyc, weil
Python den Cache nur über mtime in Sekunden und Größe prüft. Jetzt PYTHONDONTWRITEBYTECODE
und Cache-Löschung nach jedem Schreiben (cj/security-baseline).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Grovy311 2026-10-17 11:31:47 +02:00
parent ee117958b3
commit a20b6230db

View file

@ -22,11 +22,24 @@ Format (Pfade relativ zur TOML-Datei):
from __future__ import annotations from __future__ import annotations
import os
import subprocess # nosec B404 - Tests starten ist der Zweck des Skripts import subprocess # nosec B404 - Tests starten ist der Zweck des Skripts
import sys import sys
import tomllib import tomllib
from pathlib import Path from pathlib import Path
# Python prüft .pyc nur über Quell-mtime (Sekunden) und Größe. Eine gleich lange Mutation,
# die in derselben Sekunde geschrieben oder zurückgesetzt wird, liefe sonst mit dem alten
# Bytecode: Mutation wirkt nicht bzw. bleibt nach dem Lauf aktiv (llm-server, 2026-10-17).
TEST_ENV = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}
def _write(target: Path, text: str) -> None:
target.write_text(text, encoding="utf-8")
if target.suffix == ".py":
for pyc in (target.parent / "__pycache__").glob(f"{target.stem}.*.pyc"):
pyc.unlink(missing_ok=True)
def run(config_path: Path) -> int: def run(config_path: Path) -> int:
base = config_path.parent base = config_path.parent
@ -39,7 +52,7 @@ def run(config_path: Path) -> int:
return 1 return 1
# Vorbedingung: ohne Mutation muss alles grün sein, sonst beweist Rot nichts. # Vorbedingung: ohne Mutation muss alles grün sein, sonst beweist Rot nichts.
baseline = subprocess.run(test_cmd, cwd=workdir, capture_output=True, text=True) # noqa: S603 # nosec B603 baseline = subprocess.run(test_cmd, cwd=workdir, capture_output=True, text=True, env=TEST_ENV) # noqa: S603 # nosec B603
if baseline.returncode != 0: if baseline.returncode != 0:
print("Tests sind schon ohne Mutation rot – erst reparieren.\n") print("Tests sind schon ohne Mutation rot – erst reparieren.\n")
print(baseline.stdout[-3000:]) print(baseline.stdout[-3000:])
@ -55,10 +68,10 @@ def run(config_path: Path) -> int:
survivors.append(mutation["name"]) survivors.append(mutation["name"])
continue continue
try: try:
target.write_text(original.replace(mutation["old"], mutation["new"]), encoding="utf-8") _write(target, original.replace(mutation["old"], mutation["new"]))
result = subprocess.run(test_cmd, cwd=workdir, capture_output=True, text=True) # noqa: S603 # nosec B603 result = subprocess.run(test_cmd, cwd=workdir, capture_output=True, text=True, env=TEST_ENV) # noqa: S603 # nosec B603
finally: finally:
target.write_text(original, encoding="utf-8") _write(target, original)
if result.returncode == 0: if result.returncode == 0:
print(f"[ÜBERLEBT] {mutation['name']}: Tests bleiben grün") print(f"[ÜBERLEBT] {mutation['name']}: Tests bleiben grün")
survivors.append(mutation["name"]) survivors.append(mutation["name"])