333 lines
12 KiB
Python
333 lines
12 KiB
Python
import asyncio
|
|
import json
|
|
|
|
import pytest
|
|
|
|
import agents
|
|
import features
|
|
import projekte
|
|
import pruefung
|
|
|
|
ANTWORT = """```json
|
|
{"probleme": [{"feld": "regeln", "original": "Zwei Aussagen, und noch eine",
|
|
"problem": "Zwei Aussagen je Regel", "vorschlag": "Zwei Aussagen"}]}
|
|
```"""
|
|
|
|
|
|
@pytest.fixture
|
|
def projekt(datenordner):
|
|
projekte.anlegen("Test")
|
|
features.anlegen("Test", "<!-- titel -->\nEins\n")
|
|
features.anlegen("Test", "<!-- titel -->\nZwei\n")
|
|
return "Test"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def kein_lauf():
|
|
pruefung._laeufe.clear()
|
|
yield
|
|
pruefung._laeufe.clear()
|
|
|
|
|
|
async def warte_bis_fertig(projekt):
|
|
for _ in range(50):
|
|
if not pruefung.status(projekt)["laeuft"]:
|
|
return
|
|
await asyncio.sleep(0.01)
|
|
raise AssertionError("Prüfung wurde nicht fertig")
|
|
|
|
|
|
async def test_lauf_sammelt_probleme(projekt, monkeypatch):
|
|
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
|
|
monkeypatch.setattr(agents, "frage", lambda system, inhalt: _antwort())
|
|
await pruefung.starte(projekt)
|
|
await warte_bis_fertig(projekt)
|
|
|
|
s = pruefung.status(projekt)
|
|
assert s["gesamt"] == 2
|
|
assert s["fertig"] == 2
|
|
assert s["meldung"] == ""
|
|
assert len(s["probleme"]) == 2
|
|
assert s["probleme"][0]["feature"] in ("eins", "zwei")
|
|
assert s["probleme"][0]["vorschlag"] == "Zwei Aussagen"
|
|
|
|
|
|
async def _antwort():
|
|
return ANTWORT
|
|
|
|
|
|
async def test_ohne_konfiguration_kein_lauf(projekt, monkeypatch):
|
|
monkeypatch.setattr(agents.config, "MINIMAX_API_KEY", "")
|
|
with pytest.raises(ValueError):
|
|
await pruefung.starte(projekt)
|
|
assert pruefung.status(projekt)["laeuft"] is False
|
|
|
|
|
|
async def test_providerfehler_landet_in_meldung(projekt, monkeypatch):
|
|
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
|
|
|
|
async def kaputt(system, inhalt):
|
|
raise RuntimeError("Provider antwortet mit HTTP 500")
|
|
|
|
monkeypatch.setattr(agents, "frage", kaputt)
|
|
await pruefung.starte(projekt)
|
|
await warte_bis_fertig(projekt)
|
|
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):
|
|
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
|
|
monkeypatch.setattr(agents.config, "PRUEFUNG_PARALLEL", 1)
|
|
aufrufe = []
|
|
|
|
async def zaehle(system, inhalt):
|
|
aufrufe.append(inhalt)
|
|
pruefung.beende("Test") # Panel wird nach der ersten Karte geschlossen
|
|
return '{"probleme": []}'
|
|
|
|
monkeypatch.setattr(agents, "frage", zaehle)
|
|
await pruefung.starte(projekt)
|
|
for _ in range(50):
|
|
await asyncio.sleep(0.01)
|
|
assert len(aufrufe) == 1 # zweite Karte wird nicht mehr angefragt
|
|
|
|
|
|
async def test_parallel_begrenzt(projekt, monkeypatch):
|
|
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
|
|
monkeypatch.setattr(agents.config, "PRUEFUNG_PARALLEL", 1)
|
|
gleichzeitig = 0
|
|
hoechststand = 0
|
|
|
|
async def langsam(system, inhalt):
|
|
nonlocal gleichzeitig, hoechststand
|
|
gleichzeitig += 1
|
|
hoechststand = max(hoechststand, gleichzeitig)
|
|
await asyncio.sleep(0.02)
|
|
gleichzeitig -= 1
|
|
return '{"probleme": []}'
|
|
|
|
monkeypatch.setattr(agents, "frage", langsam)
|
|
await pruefung.starte(projekt)
|
|
await warte_bis_fertig(projekt)
|
|
assert hoechststand == 1
|
|
assert pruefung.status(projekt)["fertig"] == 2
|
|
|
|
|
|
async def test_status_ohne_lauf():
|
|
assert pruefung.status("Fremd") == {
|
|
"laeuft": False, "fertig": 0, "gesamt": 0, "fehler": 0,
|
|
"probleme": [], "meldung": "",
|
|
}
|
|
|
|
|
|
def test_antwort_lesen_ohne_zaun():
|
|
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": [{"feld": "regeln", "original": "- alt", "vorschlag": "- neu"}]}')
|
|
assert probleme[0]["original"] == "alt"
|
|
assert probleme[0]["vorschlag"] == "neu"
|
|
|
|
|
|
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")
|