Eingangsordner input/ und Skill sn-einlesen (#5)
- input/ ist lokal und nicht versioniert (.gitignore); scripts/einlesen.py legt ihn bei Bedarf an. - scripts/einlesen.py ordnet jede Datei ein: abgelehnt (kein .md, nicht UTF-8, leer, Unterordner), neu, geändert, eingelesen. Für neue und geänderte Dateien: Prozessname, Ordnernamen-Vorschlag, ob schon eine Arbeitsschritt-.md, und ähnliche Prozesse aus prozesse/ und entwuerfe/. „vermerk“ schreibt nach Erfolg eine erste Zeile <!-- eingelesen: <Zeitstempel> | neu|aktualisiert | <Ziel> | inhalt: <sha> -->; die Prüfsumme erkennt spätere Änderungen. Zeilenenden bleiben erhalten. - Skill sn-einlesen: Übersicht, Bestandsvergleich, Entscheidung des Nutzers, neue Prozesse durch sn-bpmn, Vermerk nur bei Erfolg. - docs/leitfaden/07_prozesse_einlesen.md: Ablauf für neue Nutzer. - AGENTS.md, README, Leitfaden 03, Skill sn-bpmn, Fallstricke, STATUS. - Tests: python -m pytest -q -> 79 bestanden (18 neu in tests/test_einlesen.py). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
"""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/, prozesse/ und entwuerfe/ an Stelle des echten."""
|
||||
for name in ("input", "prozesse", "entwuerfe"):
|
||||
(tmp_path / name).mkdir()
|
||||
monkeypatch.setattr(einlesen, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(einlesen, "EINGANG", tmp_path / "input")
|
||||
monkeypatch.setattr(einlesen, "PROZESSE", tmp_path / "prozesse")
|
||||
monkeypatch.setattr(einlesen, "ENTWUERFE", tmp_path / "entwuerfe")
|
||||
return tmp_path
|
||||
|
||||
|
||||
def lege_prozess_an(ws, name, text):
|
||||
ordner = ws / "prozesse" / 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 vorhandener Prozess 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"] == "prozesse/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_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", "prozesse/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
|
||||
Reference in New Issue
Block a user