Files
Projektwissen/tests/test_einlesen.py
T
DanielandClaude Opus 5.5 27589749ff 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>
2026-09-23 14:09:48 +02:00

206 lines
9.9 KiB
Python
Raw 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/, 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