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", "\nEins\n") features.anlegen("Test", "\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 = ( "\nLogin\n\noptional\n\nAnmelden\n" "\n- Kartenregel\n" "\nAnmelden\n\n- Formular öffnen\n" "\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 "\noptional" in inhalt[""] assert "\n- Kartenregel" in inhalt[""] assert "\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 "\nAnmelden" in inhalt["Anmelden"] assert "\n- Formular öffnen" in inhalt["Anmelden"] assert "Kartenregel" not in inhalt["Anmelden"] # Leere Felder werden gar nicht erst mitgeschickt assert inhalt["Abmelden"] == "\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")