update
This commit is contained in:
@@ -7,10 +7,11 @@ import agents
|
||||
import features
|
||||
import projekte
|
||||
import pruefung
|
||||
import struktur
|
||||
|
||||
ANTWORT = """```json
|
||||
{"probleme": [{"feld": "regeln", "original": "Zwei Aussagen, und noch eine",
|
||||
"problem": "Zwei Aussagen je Regel", "vorschlag": "Zwei Aussagen"}]}
|
||||
{"felder": {"titel": {"regel": "Sprache: Deutsch, Präsens, keine Anrede",
|
||||
"neu": "Anmeldung", "grund": "Titel nennt Tätigkeit"}}}
|
||||
```"""
|
||||
|
||||
|
||||
@@ -39,7 +40,7 @@ async def warte_bis_fertig(projekt):
|
||||
|
||||
async def test_lauf_sammelt_probleme(projekt, monkeypatch):
|
||||
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
|
||||
monkeypatch.setattr(agents, "frage", lambda system, inhalt: _antwort())
|
||||
monkeypatch.setattr(agents, "frage", lambda system, inhalt, temperatur=0.0: _antwort())
|
||||
await pruefung.starte(projekt)
|
||||
await warte_bis_fertig(projekt)
|
||||
|
||||
@@ -49,7 +50,8 @@ async def test_lauf_sammelt_probleme(projekt, monkeypatch):
|
||||
assert s["meldung"] == ""
|
||||
assert len(s["probleme"]) == 2
|
||||
assert s["probleme"][0]["feature"] in ("eins", "zwei")
|
||||
assert s["probleme"][0]["vorschlag"] == "Zwei Aussagen"
|
||||
assert s["probleme"][0]["vorschlag"] == "Anmeldung"
|
||||
assert s["probleme"][0]["problem"] == "Titel nennt Tätigkeit"
|
||||
|
||||
|
||||
async def _antwort():
|
||||
@@ -66,7 +68,7 @@ async def test_ohne_konfiguration_kein_lauf(projekt, monkeypatch):
|
||||
async def test_providerfehler_landet_in_meldung(projekt, monkeypatch):
|
||||
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
|
||||
|
||||
async def kaputt(system, inhalt):
|
||||
async def kaputt(system, inhalt, temperatur=0.0):
|
||||
raise RuntimeError("Provider antwortet mit HTTP 500")
|
||||
|
||||
monkeypatch.setattr(agents, "frage", kaputt)
|
||||
@@ -84,16 +86,17 @@ async def test_verworfener_lauf_stoppt(projekt, monkeypatch):
|
||||
monkeypatch.setattr(agents.config, "PRUEFUNG_PARALLEL", 1)
|
||||
aufrufe = []
|
||||
|
||||
async def zaehle(system, inhalt):
|
||||
async def zaehle(system, inhalt, temperatur=0.0):
|
||||
aufrufe.append(inhalt)
|
||||
pruefung.beende("Test") # Panel wird nach der ersten Karte geschlossen
|
||||
return '{"probleme": []}'
|
||||
return '{"felder": {}}'
|
||||
|
||||
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
|
||||
# Nur die erste Karte wird angefragt — mit ihren drei Stimmen
|
||||
assert len(aufrufe) == len(pruefung.TEMPERATUREN)
|
||||
|
||||
|
||||
async def test_parallel_begrenzt(projekt, monkeypatch):
|
||||
@@ -102,18 +105,19 @@ async def test_parallel_begrenzt(projekt, monkeypatch):
|
||||
gleichzeitig = 0
|
||||
hoechststand = 0
|
||||
|
||||
async def langsam(system, inhalt):
|
||||
async def langsam(system, inhalt, temperatur=0.0):
|
||||
nonlocal gleichzeitig, hoechststand
|
||||
gleichzeitig += 1
|
||||
hoechststand = max(hoechststand, gleichzeitig)
|
||||
await asyncio.sleep(0.02)
|
||||
gleichzeitig -= 1
|
||||
return '{"probleme": []}'
|
||||
return '{"felder": {}}'
|
||||
|
||||
monkeypatch.setattr(agents, "frage", langsam)
|
||||
await pruefung.starte(projekt)
|
||||
await warte_bis_fertig(projekt)
|
||||
assert hoechststand == 1
|
||||
# Die Schranke gilt je Prüfstelle; deren Stimmen laufen gemeinsam
|
||||
assert hoechststand == len(pruefung.TEMPERATUREN)
|
||||
assert pruefung.status(projekt)["fertig"] == 2
|
||||
|
||||
|
||||
@@ -124,94 +128,6 @@ async def test_status_ohne_lauf():
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
@@ -258,16 +174,16 @@ async def test_lauf_zaehlt_alle_stellen(datenordner, monkeypatch):
|
||||
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
|
||||
gesehen = []
|
||||
|
||||
async def merke(system, inhalt):
|
||||
async def merke(system, inhalt, temperatur=0.0):
|
||||
gesehen.append((system, inhalt))
|
||||
return '{"probleme": []}'
|
||||
return '{"felder": {}}'
|
||||
|
||||
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
|
||||
assert len(gesehen) == 3 * len(pruefung.TEMPERATUREN)
|
||||
# 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
|
||||
@@ -279,10 +195,11 @@ async def test_herkunft_kommt_vom_code_nicht_vom_modell(datenordner, monkeypatch
|
||||
features.anlegen("Test", KARTE)
|
||||
monkeypatch.setattr(agents, "pruefe_konfiguration", lambda: None)
|
||||
|
||||
async def luegt(system, inhalt):
|
||||
async def luegt(system, inhalt, temperatur=0.0):
|
||||
# Das Modell behauptet eine fremde Funktion — das muss ignoriert werden
|
||||
return ('{"probleme": [{"feld": "titel", "funktion": "Erfunden",'
|
||||
' "original": "x", "vorschlag": "y"}]}')
|
||||
return ('{"felder": {"titel": {"regel": "Sprache: Deutsch, Präsens, keine Anrede",'
|
||||
' "neu": "Anders", "grund": "x"},'
|
||||
' "funktion": {"neu": "Erfunden", "grund": "y"}}}')
|
||||
|
||||
monkeypatch.setattr(agents, "frage", luegt)
|
||||
await pruefung.starte("Test")
|
||||
@@ -301,6 +218,9 @@ def test_stil_katalog_je_bereich():
|
||||
assert stil.startswith("# Stil-Katalog")
|
||||
assert "## Allgemein" in stil
|
||||
|
||||
for stil in (feature, funktion):
|
||||
assert pruefung.GEMESSEN in stil # messbare Vorgaben stehen beiden zur Verfügung
|
||||
|
||||
for ueberschrift in (
|
||||
"## Titel der Karte", "## Badge der Karte",
|
||||
"## Ziel der Karte", "## Regeln der Karte",
|
||||
@@ -330,3 +250,183 @@ 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")
|
||||
|
||||
|
||||
def _stellen(feld, alt, neu):
|
||||
return [(s["original"], s["vorschlag"])
|
||||
for s in pruefung._stellen(feld, alt, neu, "grund")]
|
||||
|
||||
|
||||
TESTEN = "- RMB auf Hauptbereich\n- Klick auf \"Feature erstellen\"\n- Bestätigen mit LMB"
|
||||
|
||||
|
||||
def test_ergaenzte_zeile_haengt_an_der_davor():
|
||||
# Der Endzustand fehlt: die letzte Zeile bleibt, der neue Schritt kommt dahinter
|
||||
assert _stellen("testen", TESTEN, TESTEN + "\n- Feature erstellt") == [
|
||||
("Bestätigen mit LMB", "Bestätigen mit LMB\nFeature erstellt")]
|
||||
|
||||
|
||||
def test_ergaenzung_am_anfang_haengt_an_der_dahinter():
|
||||
assert _stellen("testen", TESTEN, "- Vollmodus öffnen\n" + TESTEN) == [
|
||||
("RMB auf Hauptbereich", "Vollmodus öffnen\nRMB auf Hauptbereich")]
|
||||
|
||||
|
||||
def test_ersetzte_zeile():
|
||||
neu = TESTEN.replace("- Bestätigen mit LMB", "- Bestätigen")
|
||||
assert _stellen("testen", TESTEN, neu) == [("Bestätigen mit LMB", "Bestätigen")]
|
||||
|
||||
|
||||
def test_gestrichene_zeile():
|
||||
neu = "- RMB auf Hauptbereich\n- Bestätigen mit LMB"
|
||||
assert _stellen("testen", TESTEN, neu) == [('Klick auf "Feature erstellen"', "")]
|
||||
|
||||
|
||||
def test_mehrere_aenderungen_werden_ein_umbau():
|
||||
# Einzeln angenommen ergäben sie Dubletten oder Lücken — also nur zusammen
|
||||
neu = "- RMB auf Hauptbereich\n- Feature anlegen"
|
||||
stellen = pruefung._stellen("testen", TESTEN, neu, "grund")
|
||||
assert len(stellen) == 1
|
||||
assert stellen[0]["ganzes_feld"] is True
|
||||
assert stellen[0]["original"] == "\n".join(struktur._zeilen("testen", TESTEN))
|
||||
assert stellen[0]["vorschlag"] == "RMB auf Hauptbereich\nFeature anlegen"
|
||||
|
||||
|
||||
def test_unveraendert_ergibt_keine_stelle():
|
||||
assert _stellen("testen", TESTEN, TESTEN) == []
|
||||
assert _stellen("titel", "Login", "Login") == []
|
||||
assert _stellen("titel", "Login", " Login ") == []
|
||||
|
||||
|
||||
def test_leeres_listenfeld_wird_ganz_gesetzt():
|
||||
assert _stellen("testen", "", "- Eins\n- Zwei") == [("", "Eins\nZwei")]
|
||||
|
||||
|
||||
def test_einzelfeld_wird_ganz_ersetzt():
|
||||
assert _stellen("titel", "Login", "Anmeldung") == [("Login", "Anmeldung")]
|
||||
assert _stellen("ziel", "Alt", "") == [("Alt", "")]
|
||||
|
||||
|
||||
def test_einzelne_aenderung_zeigt_auf_eine_echte_zeile():
|
||||
stellen = pruefung._stellen("testen", TESTEN, TESTEN + "\n- Fertig", "grund")
|
||||
assert len(stellen) == 1
|
||||
assert stellen[0]["ganzes_feld"] is False
|
||||
assert stellen[0]["original"] in struktur._zeilen("testen", TESTEN)
|
||||
|
||||
|
||||
def test_erfundenes_feld_wird_verworfen():
|
||||
erlaubt = pruefung.BEREICHE["funktion"]["felder"]
|
||||
antwort = ('{"felder": {"struktur": {"neu": "x", "grund": "y"},'
|
||||
' "testen": {"neu": "- a", "grund": "z"}}}')
|
||||
assert list(pruefung._felder_lesen(antwort, erlaubt)) == ["testen"]
|
||||
|
||||
|
||||
def test_feld_darf_nicht_leer_werden_wenn_es_pflicht_ist():
|
||||
erlaubt = pruefung.BEREICHE["feature"]["felder"]
|
||||
leer = '{"felder": {"titel": {"neu": "", "grund": "x"}, "ziel": {"neu": "", "grund": "y"}}}'
|
||||
assert list(pruefung._felder_lesen(leer, erlaubt)) == ["ziel"]
|
||||
|
||||
|
||||
def test_mehrzeilig_nur_bei_listenfeldern():
|
||||
erlaubt = pruefung.BEREICHE["funktion"]["felder"]
|
||||
antwort = ('{"felder": {"titel": {"neu": "a\\nb", "grund": "x"},'
|
||||
' "regeln": {"neu": "- a\\n- b", "grund": "y"}}}')
|
||||
assert list(pruefung._felder_lesen(antwort, erlaubt)) == ["regeln"]
|
||||
|
||||
|
||||
def test_neu_darf_liste_oder_string_sein():
|
||||
erlaubt = pruefung.BEREICHE["funktion"]["felder"]
|
||||
als_liste = '{"felder": {"testen": {"neu": ["- a", "- b"], "grund": "x"}}}'
|
||||
als_string = '{"felder": {"testen": {"neu": "- a\\n- b", "grund": "x"}}}'
|
||||
assert pruefung._felder_lesen(als_liste, erlaubt) == \
|
||||
pruefung._felder_lesen(als_string, erlaubt)
|
||||
|
||||
|
||||
def test_feld_ohne_huelle_wird_angenommen():
|
||||
# Manche Antworten kürzen {"neu": ...} auf den reinen Wert ab
|
||||
erlaubt = pruefung.BEREICHE["funktion"]["felder"]
|
||||
gelesen = pruefung._felder_lesen('{"felder": {"titel": "Anmelden"}}', erlaubt)
|
||||
assert gelesen == {"titel": {"neu": "Anmelden", "grund": "", "regel": ""}}
|
||||
|
||||
|
||||
KATALOG = pruefung.katalogzeilen(pruefung._stil_lesen("funktion"))
|
||||
|
||||
|
||||
def _mit_regel(regel):
|
||||
antwort = ('{"felder": {"titel": {"regel": "%s", "neu": "Anmelden", "grund": "x"}}}'
|
||||
% regel)
|
||||
return pruefung._felder_lesen(
|
||||
antwort, pruefung.BEREICHE["funktion"]["felder"], KATALOG)
|
||||
|
||||
|
||||
def test_aenderung_braucht_eine_echte_katalogzeile():
|
||||
assert _mit_regel("Genau eine Tätigkeit") != {} # steht so im Katalog
|
||||
assert _mit_regel("genau eine tätigkeit") != {} # Groß-/Kleinschreibung egal
|
||||
assert _mit_regel("- Genau eine Tätigkeit") != {} # Listenstrich egal
|
||||
|
||||
|
||||
def test_gemessene_regeln_sind_nicht_zitierbar():
|
||||
# Sonst beruft sich das Modell auf die Füllwortliste und wendet sie frei an
|
||||
assert _mit_regel("Titel einer Funktion: höchstens 5 Wörter") == {}
|
||||
assert _mit_regel("Keine Füllwörter. Die Liste ist abschließend:"
|
||||
" auch, noch, einfach, jeweils, ggf.") == {}
|
||||
assert "## Vom System gemessen" in pruefung._stil_lesen("funktion")
|
||||
|
||||
|
||||
def test_erfundene_regel_wird_verworfen():
|
||||
# Genau daran ist der Lauf vorher nicht konvergiert
|
||||
assert _mit_regel("Platzhaltertext ist nicht erlaubt") == {}
|
||||
assert _mit_regel("Ziel muss konkret sein") == {}
|
||||
assert _mit_regel("") == {}
|
||||
assert _mit_regel("Kein Geschmack") == {}
|
||||
|
||||
|
||||
def test_gemessener_verstoss_braucht_keine_regel():
|
||||
antwort = '{"felder": {"titel": {"neu": "Anmelden", "grund": "zu lang"}}}'
|
||||
erlaubt = pruefung.BEREICHE["funktion"]["felder"]
|
||||
assert pruefung._felder_lesen(antwort, erlaubt, KATALOG) == {}
|
||||
assert pruefung._felder_lesen(antwort, erlaubt, KATALOG, ("titel",)) != {}
|
||||
|
||||
|
||||
def test_katalogzeilen_sind_nur_die_aufzaehlung():
|
||||
zeilen = pruefung.katalogzeilen("## Titel\n\nEinleitung\n\n- Erste Regel\n- Zweite\n")
|
||||
assert zeilen == {"erste regel", "zweite"}
|
||||
|
||||
|
||||
def test_json_mit_nachsatz_und_zaun():
|
||||
for text in (
|
||||
'{"felder": {}}\nDas war alles.',
|
||||
'```json\n{"felder": {}}\n```',
|
||||
'Hier: {"felder": {}} {"noch": "was"}',
|
||||
):
|
||||
assert pruefung._json_lesen(text) == {"felder": {}}
|
||||
|
||||
|
||||
def test_json_ohne_objekt():
|
||||
with pytest.raises(ValueError):
|
||||
pruefung._json_lesen("keine Ahnung")
|
||||
|
||||
|
||||
def _proben(*fassungen):
|
||||
"""Eine Stimme je Eintrag; None heißt „diese Stimme sieht nichts"."""
|
||||
return [f or {} for f in fassungen]
|
||||
|
||||
|
||||
def test_uneinigkeit_faellt_weg():
|
||||
# Grenzfälle beurteilt ein Modell mal so, mal so — die fliegen raus
|
||||
eins = {"titel": {"neu": "A", "grund": "x"}}
|
||||
assert pruefung._mehrheit(_proben(eins, eins, eins, None)) == {}
|
||||
assert pruefung._mehrheit(_proben(eins, None, None, None)) == {}
|
||||
|
||||
|
||||
def test_einstimmiges_bleibt():
|
||||
eins = {"titel": {"neu": "A", "grund": "x"}}
|
||||
assert pruefung._mehrheit(_proben(eins, eins, eins, eins)) == eins
|
||||
|
||||
|
||||
def test_konsens_nimmt_die_mittlere_fassung():
|
||||
# Der Ausreißer zieht das Ergebnis nicht mit
|
||||
def f(text):
|
||||
return {"titel": {"neu": text, "grund": "x"}}
|
||||
proben = _proben(f("Anmeldung starten"), f("Anmeldung startet"),
|
||||
f("Anmeldung beginnen"), f("Völlig anderer Text"))
|
||||
assert pruefung._mehrheit(proben)["titel"]["neu"].startswith("Anmeldung")
|
||||
|
||||
Reference in New Issue
Block a user