Files
DanielandClaude Opus 5.5 31131cc5c3 Prozessbibliothek als eigenes Repository auslagern (#9)
- ADR 0004 ersetzt ADR 0003: Die Prozessmodelle liegen im Repository
  AG_Prozesse/Prozessbibliothek, geklont nach Prozessbibliothek/ (neuer
  Eintrag in workspace.toml, .gitignore). prozesse/ ist entfernt; das
  Ablageformat (Ordner je Prozess, drei Dateien, Status) bleibt.
- pruefe_prozesse.py prüft den Klon (Ort über setup_workspace.repos_root),
  lässt Punkt-Ordner wie .git aus und erlaubt in der Wurzel nur die
  Repo-Dateien. Fehlt der Klon, gibt es einen Hinweis statt eines Befunds;
  pytest warnt dann.
- einlesen.py sucht den Bestand in der Bibliothek und nennt Treffer als
  Prozessbibliothek/<name>/, auch wenn der Klon woanders liegt.
- Regeln aus prozesse/README.md nach docs/leitfaden/08_prozessbibliothek.md,
  inklusive Ablauf bei Konverteränderungen (zwei Pull Requests).
- Verweise in AGENTS.md, README, Skills, Leitfäden, Vorlagen, Fallstricken
  und STATUS nachgezogen.
- Tests: python -m pytest -q -> 88 bestanden, 1 xfail (#8), 1 Warnung
  (Bibliothek noch nicht geklont).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 09:40:52 +02:00

223 lines
11 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
"""Tests für scripts/einlesen.py (Skill sn-einlesen) – reines Python/pytest, in einem temporären Workspace."""
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import einlesen # noqa: E402
ARBEITSSCHRITTE = """Prozess: Antrag prüfen
S1 Start: Antrag geht ein
S2 Task: Antrag erfassen
S3 Ende: Antrag entschieden
"""
ROHTEXT = "# Wegebau abnehmen\n\nNach Fertigstellung wird der Weg begangen und die Abnahme protokolliert.\n"
FESTE_ZEIT = datetime(2026, 9, 23, 14, 5, 12, tzinfo=timezone(timedelta(hours=2)))
@pytest.fixture
def ws(tmp_path, monkeypatch):
"""Leerer Workspace mit input/, entwuerfe/ und geklonter Prozessbibliothek an Stelle des echten."""
for name in ("input", "Prozessbibliothek", "entwuerfe"):
(tmp_path / name).mkdir()
monkeypatch.setattr(einlesen, "ROOT", tmp_path)
monkeypatch.setattr(einlesen, "EINGANG", tmp_path / "input")
monkeypatch.setattr(einlesen, "BIBLIOTHEK", tmp_path / "Prozessbibliothek")
monkeypatch.setattr(einlesen, "ENTWUERFE", tmp_path / "entwuerfe")
return tmp_path
def lege_prozess_an(ws, name, text):
ordner = ws / "Prozessbibliothek" / name
ordner.mkdir()
(ordner / f"{name}.md").write_text(text, encoding="utf-8")
(ordner / "README.md").write_text(f"# {name}\n\n| | |\n|---|---|\n| Status | Entwurf |\n", encoding="utf-8")
def eintrag(bericht, dateiname):
return next(d for d in bericht["dateien"] if d["datei"].endswith(dateiname))
def test_fehlender_eingangsordner_wird_angelegt(ws):
"""Prüft, dass input/ angelegt wird, wenn es fehlt, und das im Bericht steht."""
(ws / "input").rmdir()
bericht = einlesen.pruefe_eingang()
assert (ws / "input").is_dir()
assert bericht["angelegt"] is True
assert bericht["dateien"] == []
def test_andere_dateitypen_werden_abgelehnt(ws):
"""Prüft, dass .docx, .pdf und .txt abgelehnt und nicht angefasst werden."""
for name in ("a.docx", "b.pdf", "c.txt"):
(ws / "input" / name).write_bytes(b"inhalt")
bericht = einlesen.pruefe_eingang()
assert {d["status"] for d in bericht["dateien"]} == {"abgelehnt"}
assert all("nur .md" in d["grund"] for d in bericht["dateien"])
assert (ws / "input" / "a.docx").read_bytes() == b"inhalt"
def test_unterordner_leere_und_nicht_utf8_dateien_werden_abgelehnt(ws):
"""Prüft, dass Unterordner, leere .md und .md in cp1252 mit Grund abgelehnt werden."""
(ws / "input" / "unter").mkdir()
(ws / "input" / "leer.md").write_text("\n", encoding="utf-8")
(ws / "input" / "alt.md").write_bytes("Straße".encode("cp1252"))
bericht = einlesen.pruefe_eingang()
assert "Unterordner" in eintrag(bericht, "unter")["grund"]
assert "leer" in eintrag(bericht, "leer.md")["grund"]
assert "UTF-8" in eintrag(bericht, "alt.md")["grund"]
def test_systemdateien_werden_still_uebergangen(ws):
"""Prüft, dass desktop.ini und versteckte Dateien gar nicht erst im Bericht auftauchen."""
(ws / "input" / "desktop.ini").write_text("x", encoding="utf-8")
(ws / "input" / ".gitkeep").write_text("", encoding="utf-8")
assert einlesen.pruefe_eingang()["dateien"] == []
def test_neue_md_mit_titel_namensvorschlag_und_art(ws):
"""Prüft, dass eine neue .md Titel, Ordnernamen-Vorschlag und die Art (Rohtext/Arbeitsschritte) bekommt."""
(ws / "input" / "roh.md").write_text(ROHTEXT, encoding="utf-8")
(ws / "input" / "schritte.md").write_text(ARBEITSSCHRITTE, encoding="utf-8")
bericht = einlesen.pruefe_eingang()
roh, schritte = eintrag(bericht, "roh.md"), eintrag(bericht, "schritte.md")
assert roh["status"] == "neu" and roh["titel"] == "Wegebau abnehmen"
assert roh["vorschlag_name"] == "wegebau-abnehmen" and roh["arbeitsschritt_md"] is False
assert schritte["vorschlag_name"] == "antrag-pruefen" and schritte["arbeitsschritt_md"] is True
def test_gleichnamiger_prozess_wird_als_kandidat_mit_1_gemeldet(ws):
"""Prüft, dass ein Prozess der Bibliothek mit gleichem Ordnernamen als Kandidat mit Ähnlichkeit 1 erscheint."""
lege_prozess_an(ws, "antrag-pruefen", ARBEITSSCHRITTE)
(ws / "input" / "neu.md").write_text(ARBEITSSCHRITTE.replace("erfassen", "prüfen"), encoding="utf-8")
kandidaten = eintrag(einlesen.pruefe_eingang(), "neu.md")["kandidaten"]
assert kandidaten[0]["pfad"] == "Prozessbibliothek/antrag-pruefen/"
assert kandidaten[0]["aehnlichkeit"] == 1.0
def test_inhaltlich_aehnlicher_entwurf_wird_gefunden_fremder_nicht(ws):
"""Prüft, dass ein ähnlicher lokaler Entwurf als Kandidat erscheint, ein fachfremder Prozess dagegen nicht."""
(ws / "entwuerfe" / "antragsbearbeitung.md").write_text(
"Prozess: Antragsbearbeitung\nS1 Start: Antrag geht ein\nS2 Task: Antrag erfassen und prüfen\nS3 Ende: Fertig\n",
encoding="utf-8")
lege_prozess_an(ws, "wegebau-abnehmen", "Prozess: Wegebau abnehmen\nS1 Start: Weg fertig\nS2 Ende: Protokoll\n")
(ws / "input" / "antrag.md").write_text(ARBEITSSCHRITTE, encoding="utf-8")
pfade = [k["pfad"] for k in eintrag(einlesen.pruefe_eingang(), "antrag.md")["kandidaten"]]
assert pfade == ["entwuerfe/antragsbearbeitung.md"]
def test_gitea_entwuerfe_zaehlen_nicht_zum_bestand(ws):
"""Prüft, dass Issue- und Kommentarentwürfe aus scripts/gitea.py nicht als Prozess gelten."""
(ws / "entwuerfe" / "20260923-134657-issue-Projektwissen.md").write_text("# Entwurf", encoding="utf-8")
assert einlesen.pruefe_eingang()["bestand"] == []
def test_punktordner_der_bibliothek_zaehlen_nicht_zum_bestand(ws):
"""Prüft, dass .git und andere Punkt-Ordner im Bibliotheks-Klon nicht als Prozess gelten."""
(ws / "Prozessbibliothek" / ".git").mkdir()
assert einlesen.pruefe_eingang()["bestand"] == []
def test_bibliothek_ausserhalb_des_workspace_wird_mit_ihrem_namen_genannt(ws, tmp_path_factory, monkeypatch):
"""Prüft, dass ein Prozess auch dann als 'Prozessbibliothek/<name>/' erscheint, wenn der Klon woanders liegt."""
anderswo = tmp_path_factory.mktemp("klone") / "Prozessbibliothek"
anderswo.mkdir()
monkeypatch.setattr(einlesen, "BIBLIOTHEK", anderswo)
ordner = anderswo / "antrag-pruefen"
ordner.mkdir()
(ordner / "antrag-pruefen.md").write_text(ARBEITSSCHRITTE, encoding="utf-8")
assert [e["pfad"] for e in einlesen.pruefe_eingang()["bestand"]] == ["Prozessbibliothek/antrag-pruefen/"]
def test_vermerk_steht_oben_mit_zeitstempel_und_markiert_als_eingelesen(ws):
"""Prüft, dass der Vermerk als erste Zeile mit Zeitstempel geschrieben wird und die Datei danach als eingelesen gilt."""
datei = ws / "input" / "antrag.md"
datei.write_text(ARBEITSSCHRITTE, encoding="utf-8")
zeile = einlesen.setze_vermerk(datei, "neu", "entwuerfe/antrag-pruefen.md", jetzt=FESTE_ZEIT)
inhalt = datei.read_text(encoding="utf-8")
assert inhalt.splitlines()[0] == zeile
assert zeile.startswith("<!-- eingelesen: 2026-09-23 14:05:12 +0200 | neu | entwuerfe/antrag-pruefen.md | inhalt: ")
assert inhalt.endswith(ARBEITSSCHRITTE)
befund = eintrag(einlesen.pruefe_eingang(), "antrag.md")
assert befund["status"] == "eingelesen"
assert befund["vermerk"]["ziel"] == "entwuerfe/antrag-pruefen.md"
def test_vermerk_stoert_die_umwandlung_nicht(ws):
"""Prüft, dass eine Arbeitsschritt-.md mit Vermerk weiterhin von md_to_bpmn.py gelesen wird."""
datei = ws / "input" / "antrag.md"
datei.write_text(ARBEITSSCHRITTE, encoding="utf-8")
einlesen.setze_vermerk(datei, "neu", "entwuerfe/antrag-pruefen.md")
_, _, order, _ = einlesen.md_to_bpmn.parse_markdown(datei.read_text(encoding="utf-8"))
assert order == ["S1", "S2", "S3"]
def test_aenderung_nach_dem_vermerk_ergibt_status_geaendert(ws):
"""Prüft, dass eine nach dem Einlesen bearbeitete Datei als geändert statt eingelesen erkannt wird."""
datei = ws / "input" / "antrag.md"
datei.write_text(ARBEITSSCHRITTE, encoding="utf-8")
einlesen.setze_vermerk(datei, "neu", "entwuerfe/antrag-pruefen.md")
datei.write_text(datei.read_text(encoding="utf-8") + "S4 Hinweis: Nachtrag\n", encoding="utf-8")
befund = eintrag(einlesen.pruefe_eingang(), "antrag.md")
assert befund["status"] == "geändert"
assert befund["vermerk"]["ergebnis"] == "neu"
def test_zweiter_vermerk_kommt_obenauf_der_erste_bleibt(ws):
"""Prüft, dass ein erneutes Einlesen einen neuen Vermerk oben ergänzt und den alten stehen lässt."""
datei = ws / "input" / "antrag.md"
datei.write_text(ARBEITSSCHRITTE, encoding="utf-8")
einlesen.setze_vermerk(datei, "neu", "entwuerfe/antrag-pruefen.md", jetzt=FESTE_ZEIT)
einlesen.setze_vermerk(datei, "aktualisiert", "Prozessbibliothek/antrag-pruefen/", jetzt=FESTE_ZEIT + timedelta(days=1))
zeilen = datei.read_text(encoding="utf-8").splitlines()
assert "2026-09-24" in zeilen[0] and "aktualisiert" in zeilen[0]
assert "2026-09-23" in zeilen[1] and "| neu |" in zeilen[1]
assert eintrag(einlesen.pruefe_eingang(), "antrag.md")["status"] == "eingelesen"
def test_zeilenenden_und_bom_bleiben_erhalten(ws):
"""Prüft, dass der Vermerk CRLF-Dateien nicht auf LF umstellt und ein UTF-8-BOM den Status nicht verfälscht."""
datei = ws / "input" / "win.md"
datei.write_bytes("".encode("utf-8") + ARBEITSSCHRITTE.replace("\n", "\r\n").encode("utf-8"))
einlesen.setze_vermerk(datei, "neu", "entwuerfe/x.md")
roh = datei.read_bytes()
assert b"\r\n" in roh and b"\n" not in roh.replace(b"\r\n", b"")
assert eintrag(einlesen.pruefe_eingang(), "win.md")["status"] == "eingelesen"
def test_unbekanntes_ergebnis_wird_abgewiesen(ws):
"""Prüft, dass nur die Ergebnisse 'neu' und 'aktualisiert' vermerkt werden können."""
datei = ws / "input" / "antrag.md"
datei.write_text(ARBEITSSCHRITTE, encoding="utf-8")
with pytest.raises(ValueError):
einlesen.setze_vermerk(datei, "übersprungen", "x")
assert datei.read_text(encoding="utf-8") == ARBEITSSCHRITTE
def test_vermerk_auf_andere_dateitypen_endet_mit_exit_code_1(tmp_path):
"""Prüft, dass 'vermerk' eine Nicht-.md-Datei ablehnt, ohne sie zu verändern."""
datei = tmp_path / "vorlage.docx"
datei.write_bytes(b"x")
ergebnis = subprocess.run(
[sys.executable, str(ROOT / "scripts" / "einlesen.py"), "vermerk", str(datei),
"--ergebnis", "neu", "--ziel", "x"],
capture_output=True, text=True, encoding="utf-8",
)
assert ergebnis.returncode == 1
assert datei.read_bytes() == b"x"
@pytest.mark.parametrize("titel, erwartet", [
("Antrag prüfen", "antrag-pruefen"),
("Größe & Maßnahme (Entwurf)", "groesse-massnahme-entwurf"),
(" Wegebau – Abnahme ", "wegebau-abnahme"),
])
def test_namensvorschlag_folgt_adr_0003(titel, erwartet):
"""Prüft, dass der vorgeschlagene Ordnername Kleinbuchstaben, Ziffern und Bindestriche hat, Umlaute ausgeschrieben."""
assert einlesen.slug(titel) == erwartet