This commit is contained in:
team3
2026-07-27 11:29:57 +02:00
parent 15ad6dfe8a
commit 1c8a4fd4fd
30 changed files with 1133 additions and 173 deletions

View File

@@ -1,4 +1,5 @@
import asyncio
import json
import pytest
@@ -8,7 +9,7 @@ import projekte
import pruefung
ANTWORT = """```json
{"probleme": [{"feld": "regeln", "funktion": "Etwas tun", "original": "Zwei Aussagen, und noch eine",
{"probleme": [{"feld": "regeln", "original": "Zwei Aussagen, und noch eine",
"problem": "Zwei Aussagen je Regel", "vorschlag": "Zwei Aussagen"}]}
```"""
@@ -71,7 +72,11 @@ async def test_providerfehler_landet_in_meldung(projekt, monkeypatch):
monkeypatch.setattr(agents, "frage", kaputt)
await pruefung.starte(projekt)
await warte_bis_fertig(projekt)
assert pruefung.status(projekt)["meldung"] == "Provider antwortet mit HTTP 500"
s = pruefung.status(projekt)
assert s["fehler"] == 2
assert s["meldung"] == (
"2 von 2 Prüfungen fehlgeschlagen: Provider antwortet mit HTTP 500"
)
async def test_verworfener_lauf_stoppt(projekt, monkeypatch):
@@ -114,23 +119,214 @@ async def test_parallel_begrenzt(projekt, monkeypatch):
async def test_status_ohne_lauf():
assert pruefung.status("Fremd") == {
"laeuft": False, "fertig": 0, "gesamt": 0, "probleme": [], "meldung": ""
"laeuft": False, "fertig": 0, "gesamt": 0, "fehler": 0,
"probleme": [], "meldung": "",
}
def test_antwort_lesen_ohne_zaun():
probleme = pruefung._antwort_lesen('{"probleme": [{"original": "x", "vorschlag": "y"}]}')
assert probleme == [{"original": "x", "vorschlag": "y", "funktion": ""}]
probleme = pruefung._antwort_lesen('{"probleme": [{"feld": "titel", "original": "x", "vorschlag": "y"}]}')
assert probleme == [{"feld": "titel", "original": "x", "vorschlag": "y"}]
def test_listenstrich_wird_entfernt():
probleme = pruefung._antwort_lesen(
'{"probleme": [{"original": "- alt", "vorschlag": "- neu", "funktion": " F "}]}')
'{"probleme": [{"feld": "regeln", "original": "- alt", "vorschlag": "- neu"}]}')
assert probleme[0]["original"] == "alt"
assert probleme[0]["vorschlag"] == "neu"
assert probleme[0]["funktion"] == "F"
def test_antwort_ohne_json():
with pytest.raises(ValueError):
pruefung._antwort_lesen("keine Ahnung")
def test_unmaskierte_quotes_im_original():
# Karteninhalt enthält Anführungszeichen; das Modell maskiert sie nicht
probleme = pruefung._antwort_lesen(
'{"probleme": [{"feld": "testen", "original": "Klick auf "Feature entfernen"",'
' "problem": "x", "vorschlag": "Klick auf "Feature entfernen" mit LMB"}]}')
assert probleme[0]["original"] == 'Klick auf "Feature entfernen"'
assert probleme[0]["vorschlag"] == 'Klick auf "Feature entfernen" mit LMB'
def test_gueltiges_json_bleibt_unberuehrt():
probleme = pruefung._antwort_lesen(
r'{"probleme": [{"feld": "testen", "original": "Klick auf \"X\"", "vorschlag": "y"}]}')
assert probleme[0]["original"] == 'Klick auf "X"'
def test_zeilenumbruch_im_string():
probleme = pruefung._antwort_lesen(
'{"probleme": [{"feld": "testen", "original": "erste\nzweite", "vorschlag": "y"}]}')
assert probleme[0]["original"] == "erste\nzweite"
def _eins(felder=pruefung.ALLE_FELDER, **werte):
p = {"feld": "testen", "original": "Bestätigen", "problem": "x", "vorschlag": "y"}
p.update(werte)
return pruefung._antwort_lesen(json.dumps({"probleme": [p]}), felder)
def test_mehrzeiliger_vorschlag_bleibt_erhalten():
# Die alte Zeile bleibt stehen, der Endzustand kommt dahinter — ohne Aktionsfeld
p = _eins(vorschlag="- Bestätigen mit LMB\n- Feature erstellt")[0]
assert p["vorschlag"] == "Bestätigen mit LMB\nFeature erstellt"
def test_leerer_vorschlag_nur_wo_das_feld_leer_sein_darf():
for feld in ("regeln", "testen", "ziel"):
assert len(_eins(feld=feld, vorschlag="")) == 1 # streicht die Stelle
for feld in ("titel", "badge"):
assert _eins(feld=feld, vorschlag="") == [] # wären danach kaputt
def test_mehrere_zeilen_nur_bei_listenfeldern():
for feld in ("regeln", "testen"):
assert len(_eins(feld=feld, vorschlag="a\nb")) == 1
for feld in ("titel", "badge", "ziel"):
assert _eins(feld=feld, vorschlag="a\nb") == []
def test_erfundenes_feld_wird_verworfen():
# Das Modell meldete „struktur" — kein Feld der Karte, also nie anwendbar
assert _eins(feld="struktur") == []
assert _eins(feld="") == []
def test_feld_muss_zum_bereich_passen():
feature = pruefung.BEREICHE["feature"]["felder"]
funktion = pruefung.BEREICHE["funktion"]["felder"]
assert _eins(feld="testen", felder=funktion) != []
assert _eins(feld="testen", felder=feature) == [] # Karte hat kein testen
assert _eins(feld="badge", vorschlag="kern", felder=feature) != []
assert _eins(feld="badge", vorschlag="kern", felder=funktion) == []
def test_dieselbe_stelle_nur_einmal():
probleme = pruefung._antwort_lesen(
'{"probleme": ['
'{"feld": "regeln", "original": "a", "problem": "eins", "vorschlag": "x"},'
'{"feld": "regeln", "original": "a", "problem": "zwei", "vorschlag": "y"},'
'{"feld": "testen", "original": "a", "problem": "drei", "vorschlag": "z"}]}')
assert [p["problem"] for p in probleme] == ["eins", "drei"] # feld trennt
KARTE = (
"<!-- titel -->\nLogin\n<!-- art -->\noptional\n<!-- ziel -->\nAnmelden\n"
"<!-- regeln -->\n- Kartenregel\n"
"<!-- funktion -->\nAnmelden\n<!-- testen -->\n- Formular öffnen\n"
"<!-- funktion -->\nAbmelden\n"
)
def test_aufgaben_eine_je_ebene(datenordner):
projekte.anlegen("Test")
f = features.anlegen("Test", KARTE)
liste = pruefung.aufgaben(f)
# Dateireihenfolge, nicht alphabetisch — sortiert wird erst in der Anzeige
assert [a["funktion"] for a in liste] == ["", "Anmelden", "Abmelden"]
assert [a["bereich"] for a in liste] == ["feature", "funktion", "funktion"]
assert all(a["feature"] == "login" for a in liste)
def test_aufgabe_bekommt_nur_ihren_ausschnitt(datenordner):
projekte.anlegen("Test")
f = features.anlegen("Test", KARTE)
karte, anmelden, abmelden = pruefung.aufgaben(f)
inhalt = {a["funktion"]: pruefung.inhalt(a) for a in (karte, anmelden, abmelden)}
# Kartenebene: badge statt art, Funktionstitel nur als Kontext für den Badge
assert "<!-- badge -->\noptional" in inhalt[""]
assert "<!-- regeln -->\n- Kartenregel" in inhalt[""]
assert "<!-- funktionen -->\n- Anmelden\n- Abmelden" in inhalt[""]
assert "Formular öffnen" not in inhalt[""] # keine Details der Funktionen
# Funktion: eigener Titel, eigenes testen, nichts von der Karte
assert "<!-- titel -->\nAnmelden" in inhalt["Anmelden"]
assert "<!-- testen -->\n- Formular öffnen" in inhalt["Anmelden"]
assert "Kartenregel" not in inhalt["Anmelden"]
# Leere Felder werden gar nicht erst mitgeschickt
assert inhalt["Abmelden"] == "<!-- titel -->\nAbmelden\n"
async def test_lauf_zaehlt_alle_stellen(datenordner, monkeypatch):
projekte.anlegen("Test")
features.anlegen("Test", KARTE)
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
gesehen = []
async def merke(system, inhalt):
gesehen.append((system, inhalt))
return '{"probleme": []}'
monkeypatch.setattr(agents, "frage", merke)
await pruefung.starte("Test")
await warte_bis_fertig("Test")
assert pruefung.status("Test")["gesamt"] == 3 # 1 Karte + 2 Funktionen
assert len(gesehen) == 3
# Der Kartenprüfer kennt die Badge-Regeln, der Funktionsprüfer nicht
karten_system = next(s for s, i in gesehen if "badge" in i)
assert "## Badge der Karte" in karten_system
assert "## Testen einer Funktion" not in karten_system
async def test_herkunft_kommt_vom_code_nicht_vom_modell(datenordner, monkeypatch):
projekte.anlegen("Test")
features.anlegen("Test", KARTE)
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
async def luegt(system, inhalt):
# Das Modell behauptet eine fremde Funktion — das muss ignoriert werden
return ('{"probleme": [{"feld": "titel", "funktion": "Erfunden",'
' "original": "x", "vorschlag": "y"}]}')
monkeypatch.setattr(agents, "frage", luegt)
await pruefung.starte("Test")
await warte_bis_fertig("Test")
zuordnung = sorted(p["funktion"] for p in pruefung.status("Test")["probleme"])
assert zuordnung == ["", "Abmelden", "Anmelden"]
def test_stil_katalog_je_bereich():
feature = pruefung._stil_lesen("feature")
funktion = pruefung._stil_lesen("funktion")
# allgemein steckt in beiden und steht vorn
for stil in (feature, funktion):
assert stil.startswith("# Stil-Katalog")
assert "## Allgemein" in stil
for ueberschrift in (
"## Titel der Karte", "## Badge der Karte",
"## Ziel der Karte", "## Regeln der Karte",
):
assert ueberschrift in feature
assert ueberschrift not in funktion # der Funktionsprüfer sieht das nicht
for ueberschrift in (
"## Titel einer Funktion", "## Ziel einer Funktion",
"## Regeln einer Funktion", "## Testen einer Funktion",
):
assert ueberschrift in funktion
assert ueberschrift not in feature
def test_neue_stil_datei_kommt_ohne_code_dazu(tmp_path, monkeypatch):
(tmp_path / "allgemein.md").write_text("## Allgemein\n")
(tmp_path / "feature-neu.md").write_text("## Neu\n")
monkeypatch.setattr(pruefung.config, "STIL_DIR", tmp_path)
stil = pruefung._stil_lesen("feature")
assert "## Neu" in stil
assert stil.index("## Allgemein") < stil.index("## Neu") # alphabetisch
assert "## Neu" not in pruefung._stil_lesen("funktion")
def test_stil_ordner_leer_bricht_ab(tmp_path, monkeypatch):
monkeypatch.setattr(pruefung.config, "STIL_DIR", tmp_path)
with pytest.raises(ValueError):
pruefung._stil_lesen("feature")

View File

@@ -58,6 +58,17 @@ async def test_feature_crud(client):
assert (await client.delete("/api/projekte/Alpha/features/login")).status_code == 404
async def test_pruefung_ohne_konfiguration(client, monkeypatch):
import agents
monkeypatch.setattr(agents.config, "MINIMAX_API_KEY", "")
await client.post("/api/projekte", json={"name": "Alpha"})
res = await client.post("/api/projekte/Alpha/pruefung")
# 400, nicht 409 — 409 heißt „läuft bereits" und würde die Meldung verschlucken
assert res.status_code == 400
assert "MINIMAX_API_KEY" in res.json()["detail"]
async def test_unbekanntes_projekt(client):
assert (await client.get("/api/projekte/Fremd/features")).status_code == 404
assert (await client.post("/api/projekte/Fremd/features", json={"roh": ROH})).status_code == 404

View File

@@ -0,0 +1,146 @@
import pruefung
import struktur
def _pruefe(bereich, feld, text):
return struktur.befunde(bereich, {feld: text})
def _probleme(befunde):
return [b["problem"] for b in befunde]
def test_satzzeichen_am_zeilenende():
b = _pruefe("funktion", "regeln", "- Passwort nie loggen.")[0]
assert b["problem"] == "Satzzeichen am Zeilenende"
assert b["vorschlag"] == "Passwort nie loggen"
def test_punkt_im_wort_ist_kein_satzende():
# Der Prüfer meldete hier „Punkt am Zeilenende" — die Zeile endet auf „v"
assert _pruefe("funktion", "regeln", "- API Zugänge stehen in .env") == []
assert _pruefe("funktion", "regeln", "- Nur kern und optional erlaubt") == []
def test_fuellwort_wird_entfernt():
b = _pruefe("funktion", "regeln", "- Auch Features entfernen")[0]
assert b["problem"] == "Füllwort „auch“"
assert b["vorschlag"] == "Features entfernen"
def test_liste_der_fuellwoerter_ist_abschliessend():
# Das Modell hielt „Im", „Nur" und „direkt" für Füllwörter — sind sie nicht
for zeile in ("- Im Modal Namen eingeben", "- Nur wenige Wörter",
"- Feature direkt in Vollmodus geöffnet"):
assert _pruefe("funktion", "regeln", zeile) == []
def test_wortgrenze_je_bereich_und_feld():
assert _probleme(_pruefe("feature", "titel", "Karten der Features prüfen")) \
== ["Mehr als 3 Wörter"]
assert _pruefe("funktion", "titel", "Karten der Features prüfen") == []
assert _probleme(_pruefe("funktion", "testen", "- Eins zwei drei vier fünf sechs sieben acht neun")) \
== ["Mehr als 8 Wörter"]
def test_ohne_vorschlag_wenn_der_code_nicht_kuerzen_kann():
b = _pruefe("funktion", "testen", "- Eins zwei drei vier fünf sechs sieben acht neun")[0]
assert b["vorschlag"] == "" # den Ersatz schreibt das Modell
def test_klammer_und_schraegstrich():
assert _probleme(_pruefe("funktion", "regeln", "- Rand blau (kern)")) \
== ["Klammer oder Schrägstrich"]
assert _probleme(_pruefe("funktion", "regeln", "- Kern/Optional getrennt")) \
== ["Klammer oder Schrägstrich"]
def test_ziel_saetze_und_laenge():
lang = "Dieses Ziel hat deutlich mehr als zwölf Wörter in einem einzigen langen Satz"
assert "Satz mit mehr als 12 Wörtern" in _probleme(_pruefe("feature", "ziel", lang))
assert _probleme(_pruefe("feature", "ziel", "Eins. Zwei. Drei")) == ["Mehr als 2 Sätze"]
def test_badge_nur_kern_oder_optional():
assert _pruefe("feature", "badge", "optional") == []
b = _pruefe("feature", "badge", "wichtig")[0]
assert b["vorschlag"] == "kern"
def test_jede_zeile_einzeln():
befunde = _pruefe("funktion", "regeln", "- Erste Regel.\n- Auch zweite Regel")
assert _probleme(befunde) == ["Satzzeichen am Zeilenende", "Füllwort „auch“"]
def test_ein_befund_je_zeile():
# Zwei Vorschläge für dieselbe Zeile wären nicht unabhängig anwendbar
befunde = _pruefe("funktion", "regeln", "- Auch Passwort prüfen.")
assert len(befunde) == 1
assert befunde[0]["problem"] == "Satzzeichen am Zeilenende, Füllwort „auch“"
assert befunde[0]["vorschlag"] == "Passwort prüfen"
def test_unreparierbares_zieht_den_ganzen_befund_zum_modell():
# Zu lang und Füllwort: der Code streicht nicht halb, das Modell schreibt neu
b = _pruefe("funktion", "testen", "- Auch eins zwei drei vier fünf sechs sieben acht")[0]
assert b["problem"] == "Füllwort „auch“, Mehr als 8 Wörter"
assert b["vorschlag"] == ""
def test_auftrag_nennt_feld_und_stelle():
b = _pruefe("feature", "titel", "Karten der Features prüfen")[0]
assert struktur.auftrag(b) == '- titel „Karten der Features prüfen“: Mehr als 3 Wörter'
async def test_pruefe_verbindet_messung_und_modell(datenordner, monkeypatch):
import agents
import features
import projekte
projekte.anlegen("Test")
f = features.anlegen(
"Test",
"<!-- titel -->\nLogin\n<!-- funktion -->\nAnmelden\n"
"<!-- regeln -->\n- Auch Passwort prüfen.\n",
)
gesehen = []
async def modell(system, inhalt):
gesehen.append(inhalt)
# Das Modell meldet ein erfundenes Feld und dieselbe Zeile noch einmal
return ('{"probleme": ['
'{"feld": "struktur", "original": "x", "vorschlag": "y"},'
'{"feld": "regeln", "original": "Auch Passwort prüfen.",'
' "problem": "Füllwort", "vorschlag": "Passwort prüfen"},'
'{"feld": "regeln", "original": "Auch Passwort prüfen.",'
' "problem": "zwei Aussagen", "vorschlag": "Passwort prüfen"}]}')
monkeypatch.setattr(agents, "frage", modell)
aufgabe = pruefung.aufgaben(f)[1]
probleme = await pruefung._pruefe({"funktion": "<KATALOG>"}, aufgabe)
# Ein Befund für die Zeile, beide Verstöße auf einmal behoben — ohne Modell
assert [p["problem"] for p in probleme] == [
"Satzzeichen am Zeilenende, Füllwort „auch“",
]
assert probleme[0]["vorschlag"] == "Passwort prüfen"
# Erfundenes Feld fällt raus, Doppelmeldung der gemessenen Zeile ebenso
assert probleme[0]["feld"] == "regeln"
assert probleme[0]["funktion"] == "Anmelden"
# Nichts zu kürzen -> kein Auftrag im Ausschnitt
assert "Gemessen" not in gesehen[0]
def test_gemessene_befunde_haengen_am_ausschnitt(datenordner):
import features
import projekte
projekte.anlegen("Test")
f = features.anlegen("Test", "<!-- titel -->\nEin viel zu langer Kartentitel\n")
karte = pruefung.aufgaben(f)[0]
befunde = struktur.befunde("feature", karte["felder"])
assert "Gemessen" not in pruefung.inhalt(karte)
text = pruefung.inhalt(karte, befunde)
assert "Gemessen, bitte neu schreiben:" in text
assert "Mehr als 3 Wörter" in text