This commit is contained in:
team3
2026-07-29 15:19:41 +02:00
parent 2e86d682eb
commit 5d42b4cfc5
155 changed files with 14482 additions and 1736 deletions

View File

@@ -1,60 +0,0 @@
import inhalt
import quellcode
def test_unformatierte_prompts_ohne_doppelklammern():
"""Was nicht durch .format() geht, darf keine Escape-Klammern tragen —
das Modell schrieb sie sonst wörtlich in die Antwort und das JSON war kaputt."""
for name in ("ABDECKUNG_SYSTEM",):
text = getattr(inhalt, name)
assert "{{" not in text and "}}" not in text, name
def test_formatierte_prompts_ueberstehen_format():
punkt = {"art": "Bedienung", "datei": "a.vue", "zeile": 1,
"umfeld": "x", "rumpf": ""}
fertig = inhalt.ABDECKUNG.format(funktionen="—", **punkt)
assert '{"abgedeckt": true' in fertig
assert "{{" not in fertig
def test_beleg_muss_woertlich_dastehen():
quelle = {"a.py": "def eins():\n return 1\n"}
assert quellcode.belegt(quelle, "a.py", "def eins():")
assert quellcode.belegt(quelle, "a.py", " def eins(): ") # Einrückung egal
assert quellcode.belegt(quelle, "a.py", "def eins():\nreturn 1")
assert not quellcode.belegt(quelle, "a.py", "def zwei():")
assert not quellcode.belegt(quelle, "b.py", "def eins():")
assert not quellcode.belegt(quelle, "a.py", "")
def test_interaktionen_sind_deterministisch():
quelle = {
"frontend/src/X.vue": (
'<button @click="starte(\'ziel\')">a</button>\n'
'<button @click="starte(\'regeln\')">b</button>\n'
'<div @contextmenu="menue">c</div>\n'
'<input @blur="fertig" @keydown.esc="abbrechen">\n'
'<span @click="offen = false">zu</span>\n'
),
"backend/routes.py": '@router.get("/projekte")\n@router.post("/projekte")\n',
"frontend/src/m.js": " { text: '+ Projekt erstellen', aktion: () => x() },\n",
}
schluessel = [p["schluessel"] for p in quellcode.interaktionen(quelle)]
# Zwölf Aufrufe von starte() sind eine Fähigkeit, nicht zwölf
assert schluessel.count("bedienen:starte") == 1
assert "bedienen:menue" in schluessel
# Routen: der einzige Weg an Parser, Dateiverwaltung und Prüflogik
assert "route:get /projekte" in schluessel
assert "route:post /projekte" in schluessel
assert "menue:+ Projekt erstellen" in schluessel
# Technische Aufhänger und Zuweisungen sind keine Fähigkeiten
assert not any(s.startswith("bedienen:fertig") for s in schluessel)
assert not any(s.startswith("bedienen:abbrechen") for s in schluessel)
assert not any(s.startswith("bedienen:offen") for s in schluessel)
# Zweiter Aufruf liefert exakt dasselbe
assert schluessel == [p["schluessel"] for p in quellcode.interaktionen(quelle)]
def test_tests_sind_kein_quelltext():
assert not any("tests/" in p for p in quellcode.dateien())

View File

@@ -1,8 +1,8 @@
"""Das Gedächtnis der Inhaltsprüfungen: was gilt weiter, was wird neu gefragt.
"""Das Gedächtnis der Stilprüfung: was gilt weiter, was wird neu gefragt.
Die Prüfungen 2 und 3 fragen ein Modell. Dieselbe Frage kippt bei Grenzfällen auch
bei Temperatur 0 — vorher meldete Runde 1 einen Befund, Runde 2 keinen, Runde 3
wieder einen. Hier steht, unter welchen Bedingungen ein Urteil stehen bleibt.
Die Prüfung fragt ein Modell. Dieselbe Frage kippt bei Grenzfällen auch bei
Temperatur 0 — vorher meldete Runde 1 einen Befund, Runde 2 keinen, Runde 3 wieder
einen. Hier steht, unter welchen Bedingungen ein Urteil stehen bleibt.
"""
import json
@@ -11,7 +11,6 @@ import pytest
import agents
import features
import inhalt
import ledger
import projekte
@@ -33,9 +32,6 @@ def karte(projekt, titel, *funktionen):
return features.anlegen(projekt, text)
QUELLE = {"frontend/src/App.vue": '<button @click="starte">los</button>\n'}
class Zaehler:
"""Ersetzt das Modell und zählt mit, wie oft überhaupt gefragt wurde."""
@@ -82,431 +78,6 @@ def test_gedaechtnis_ist_kein_feature(projekt):
assert [f["sektionen"]["titel"] for f in features.liste(projekt)] == ["Eins"]
# ---------------------------------------------------------------- Prüfung 2
GEFUNDEN = json.dumps({"gefunden": True, "datei": "frontend/src/App.vue",
"beleg": '<button @click="starte">los</button>'})
async def test_beleg_wird_gemerkt_und_beim_zweiten_lauf_nicht_gefragt(
projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(GEFUNDEN)
monkeypatch.setattr(agents, "frage", modell)
alle = features.liste(projekt)
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
assert await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text) == []
erste_runde = modell.rufe
assert erste_runde >= len(inhalt.SUCHE_TEMPERATUREN)
assert await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text) == []
assert modell.rufe == erste_runde # keine einzige neue Anfrage
async def test_beleg_verfaellt_wenn_die_zeile_verschwindet(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(GEFUNDEN)
monkeypatch.setattr(agents, "frage", modell)
alle = features.liste(projekt)
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text)
vorher = modell.rufe
anders = {"frontend/src/App.vue": "<div>nichts mehr</div>\n"}
await inhalt.pruefe_feature_im_code(projekt, alle, anders, "### x\n<div>")
assert modell.rufe > vorher # der Beleg trägt nicht mehr, also neu fragen
async def test_geaenderte_funktion_wird_neu_gefragt(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(GEFUNDEN)
monkeypatch.setattr(agents, "frage", modell)
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
await inhalt.pruefe_feature_im_code(projekt, features.liste(projekt), QUELLE, text)
vorher = modell.rufe
datei = projekte.wurzel(projekt) / f"{features.liste(projekt)[0]['id']}.md"
datei.write_text(datei.read_text().replace("- Knopf", "- Ganz anderer Inhalt"))
await inhalt.pruefe_feature_im_code(projekt, features.liste(projekt), QUELLE, text)
assert modell.rufe > vorher
async def test_fehlanzeige_gilt_nur_bei_unveraendertem_quelltext(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(json.dumps({"gefunden": False, "grund": "gibt es nicht"}))
monkeypatch.setattr(agents, "frage", modell)
alle = features.liste(projekt)
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
erste = await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text)
assert len(erste) == 1
vorher = modell.rufe
zweite = await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text)
assert [p["vorschlag"] for p in zweite] == [p["vorschlag"] for p in erste]
assert modell.rufe == vorher
# Neuer Quelltext: eine Löschempfehlung muss frisch belegt werden
mehr = {**QUELLE, "backend/neu.py": "def starte():\n pass\n"}
await inhalt.pruefe_feature_im_code(projekt, alle, mehr, text)
assert modell.rufe > vorher
# ---------------------------------------------------------------- Dubletten
def punkt(schluessel="bedienen:starte", beleg='<button @click="starte">los</button>'):
return {"schluessel": schluessel, "art": "Bedienung",
"datei": "frontend/src/App.vue", "zeile": 1, "beleg": beleg,
"umfeld": beleg, "rumpf": ""}
async def test_gleicher_beleg_macht_zwei_funktionen_zu_kandidaten(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
karte(projekt, "Zwei", ("Lauf beginnen", "Knopf"))
monkeypatch.setattr(agents, "frage", Zaehler(json.dumps({"gegenstand_gleich": True, "ergebnis_gleich": True})))
alle = features.liste(projekt)
stand = ledger.lade(projekt)
for f in alle:
stand["belege"][ledger.schluessel(f["id"], f["sektionen"]["funktionen"][0]["text"])] = {
"abdruck": "egal", "datei": "frontend/src/App.vue",
"beleg": '<button @click="starte">los</button>'}
gefunden = await inhalt.dubletten(alle, stand)
assert len(gefunden) == 1
# Gleich stark: der Titel entscheidet, damit der Vorschlag nicht springt
assert gefunden[0]["titel"] == "Etwas starten"
assert "Lauf beginnen" in gefunden[0]["grund"]
async def test_uneiniges_urteil_ist_keine_dublette_und_wird_gemerkt(
projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
karte(projekt, "Zwei", ("Lauf beginnen", "Knopf"))
def wechselhaft(nachricht, ruf):
return json.dumps({"gegenstand_gleich": True, "ergebnis_gleich": ruf == 1})
modell = Zaehler(wechselhaft)
monkeypatch.setattr(agents, "frage", modell)
alle = features.liste(projekt)
stand = ledger.lade(projekt)
for f in alle:
stand["belege"][ledger.schluessel(f["id"], f["sektionen"]["funktionen"][0]["text"])] = {
"abdruck": "egal", "datei": "a.vue", "beleg": "x"}
assert await inhalt.dubletten(alle, stand) == []
vorher = modell.rufe
assert await inhalt.dubletten(alle, stand) == []
assert modell.rufe == vorher # Ablehnung wird gemerkt, nicht neu bezahlt
def test_gegenstueck_faellt_nicht_ueber_den_titel():
"""anlegen/entfernen und öffnen/schließen dürfen den Titelkanal nicht auslösen —
sonst kostet jedes Gegensatzpaar drei Anfragen für nichts."""
for a, b in (("Feature anlegen", "Feature entfernen"),
("Panel öffnen", "Panel schließen")):
assert inhalt._aehnlich(a, b) < inhalt.TITEL_KANAL
ENTFERNEN = ["Vollmodus öffnen", "RMB auf die Funktion",
'Klick auf "Funktion entfernen"', "Funktion entfernt"]
def test_umschriebene_dublette_wird_kandidat(projekt):
"""Anderer Titel, andere Karte, dieselbe Sache. Weder Titel noch Beleg fangen
das — nur der Inhaltskanal."""
karte(projekt, "Funktionen",
("Funktion entfernen", "x-Icon", ENTFERNEN),
("Funktion anlegen", "+-Icon", ["Vollmodus öffnen", "RMB auf die Karte",
'Klick auf "Funktion erstellen"',
"Funktion angelegt"]),
("Testschritte anpassen", "Ein Schritt je Zeile",
["Vollmodus öffnen", "LMB auf Testen", "Text ändern", "Schritte angepasst"]))
karte(projekt, "Features", ("Funktion löschen", "x-Icon", ENTFERNEN))
alle = inhalt._funktionen(features.liste(projekt))
paare = {frozenset((alle[a]["titel"], alle[b]["titel"])) for _, a, b, _ in
inhalt._kandidaten(alle, {})}
assert frozenset(("Funktion entfernen", "Funktion löschen")) in paare
assert frozenset(("Funktion anlegen", "Funktion entfernen")) not in paare
def test_floskeln_zaehlen_nicht_mit(projekt):
"""Was fast überall steht, unterscheidet nichts und fliegt aus dem Vergleich."""
schritte = ["Vollmodus öffnen", "Text ändern", "Angepasst"]
karte(projekt, "Eins",
*[(f"{feld} anpassen", "eine Regel", schritte)
for feld in ("Titel", "Ziel", "Regeln", "Testen", "Badge")])
kerne = inhalt._kerne(inhalt._funktionen(features.liste(projekt)))
gemeinsam = set.intersection(*kerne.values())
assert gemeinsam == set() # „vollmodus", „ändern", „angepasst" sind raus
assert all(k for k in kerne.values()) # jede Funktion behält ihr eigenes Wort
# ---------------------------------------------------------------- Prüfung 3
ABGEDECKT = json.dumps({"abgedeckt": True, "funktion": "Etwas starten"})
FEHLT = json.dumps({"abgedeckt": False, "karte": "Eins", "titel": "Neu bedienen"})
async def test_abdeckung_wird_gemerkt_solange_die_funktion_dasteht(
projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(ABGEDECKT)
monkeypatch.setattr(agents, "frage", modell)
stand = ledger.lade(projekt)
alle = features.liste(projekt)
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
vorher = modell.rufe
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
assert modell.rufe == vorher
# Funktion weg: die Abdeckung trägt nicht mehr
ohne = features.liste(projekt)
ohne[0]["sektionen"]["funktionen"] = []
await inhalt.code_im_feature(ohne, [punkt()], stand)
assert modell.rufe > vorher
async def test_geaenderte_codestelle_wird_neu_beurteilt(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(ABGEDECKT)
monkeypatch.setattr(agents, "frage", modell)
stand = ledger.lade(projekt)
alle = features.liste(projekt)
await inhalt.code_im_feature(alle, [punkt()], stand)
vorher = modell.rufe
await inhalt.code_im_feature(alle, [punkt(beleg="ganz anderer Code")], stand)
assert modell.rufe > vorher
async def test_fehlmeldung_bleibt_wortgleich_bis_die_funktion_da_ist(
projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(FEHLT)
monkeypatch.setattr(agents, "frage", modell)
stand = ledger.lade(projekt)
alle = features.liste(projekt)
erste = await inhalt.code_im_feature(alle, [punkt()], stand)
assert [x["titel"] for x in erste] == ["Neu bedienen"]
vorher = modell.rufe
zweite = await inhalt.code_im_feature(alle, [punkt()], stand)
assert zweite == erste
assert modell.rufe == vorher # kein Flackern, keine neue Anfrage
async def test_angenommener_vorschlag_loest_die_fehlmeldung_ab(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler(FEHLT)
monkeypatch.setattr(agents, "frage", modell)
stand = ledger.lade(projekt)
await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand)
vorher = modell.rufe
# Der Nutzer nimmt den Vorschlag an — jetzt muss neu beurteilt werden
modell.antwort = ABGEDECKT
karte(projekt, "Zwei", ("Neu bedienen", "Knopf"))
assert await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand) == []
assert modell.rufe > vorher
async def test_unentschiedenes_urteil_wird_festgehalten(projekt, monkeypatch):
"""Drei zu drei ist ein Münzwurf. Der darf einmal fallen, nicht jede Runde."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
def haelfte(nachricht, ruf):
return ABGEDECKT if ruf % 2 else FEHLT
modell = Zaehler(haelfte)
monkeypatch.setattr(agents, "frage", modell)
stand = ledger.lade(projekt)
alle = features.liste(projekt)
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
vorher = modell.rufe
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
assert modell.rufe == vorher
async def test_verschwundene_stelle_raeumt_ihren_eintrag_weg(projekt, monkeypatch):
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
monkeypatch.setattr(agents, "frage", Zaehler(ABGEDECKT))
stand = ledger.lade(projekt)
alle = features.liste(projekt)
await inhalt.code_im_feature(alle, [punkt()], stand)
assert stand["abdeckung"]
await inhalt.code_im_feature(alle, [punkt(schluessel="bedienen:anderes")], stand)
assert "bedienen:starte" not in stand["abdeckung"]
def test_fehlende_stelle_landet_auch_auf_erfundener_karte(projekt):
"""Sechs Stimmen sagen „fehlt", nennen aber Karten, die es nicht gibt. Vorher
fiel der Befund lautlos weg."""
karte(projekt, "Projekte", ("Projekt anlegen", "+-Icon"))
karten = {f["sektionen"]["titel"]: f for f in features.liste(projekt)}
urteile = [{"abgedeckt": False, "karte": "Datenverwaltung", "titel": "x"}] * 4
assert inhalt._zielkarte(urteile, karten) is karten["Projekte"]
def test_erster_aufloesbarer_kartenname_gewinnt(projekt):
karte(projekt, "Projekte", ("Projekt anlegen", "+-Icon"))
karte(projekt, "Darkmode", ("Darkmode umschalten", "Icon"))
karten = {f["sektionen"]["titel"]: f for f in features.liste(projekt)}
urteile = ([{"abgedeckt": False, "karte": "Export", "titel": "x"}] * 3
+ [{"abgedeckt": False, "karte": "Darkmode", "titel": "x"}] * 2)
assert inhalt._zielkarte(urteile, karten) is karten["Darkmode"]
def test_ohne_karten_kein_ziel(projekt):
assert inhalt._zielkarte([{"abgedeckt": False, "karte": "X"}], {}) is None
def test_grund_trennt_dublette_von_fehlendem_code():
"""Beides heißt streichen — aber aus verschiedenen Gründen, und der Grund
entscheidet, ob man zustimmt."""
assert inhalt._wegfall_grund([{"art": "dublette"}]) == "1 Funktion doppelt"
assert inhalt._wegfall_grund([{"art": "ohne_code"}] * 2) \
== "2 Funktionen ohne eigenen Code"
assert inhalt._wegfall_grund([{"art": "dublette"}, {"art": "ohne_code"}]) \
== "1 Funktion doppelt, 1 Funktion ohne eigenen Code"
def test_halbe_datei_ueberschreibt_nicht_die_ganze(projekt, monkeypatch):
"""Absturz beim Schreiben: die alte Datei muss stehen bleiben."""
stand = ledger.lade(projekt)
stand["belege"]["a"] = {"abdruck": "x", "datei": "a.py", "beleg": "z"}
ledger.sichere(projekt, stand)
echt = ledger.json.dumps
def platzt(*a, **k):
raise MemoryError("mittendrin")
monkeypatch.setattr(ledger.json, "dumps", platzt)
with pytest.raises(MemoryError):
ledger.sichere(projekt, {"belege": {"b": {}}})
monkeypatch.setattr(ledger.json, "dumps", echt)
assert ledger.lade(projekt)["belege"] == stand["belege"]
async def test_ausgefallene_stimmen_merken_nichts(projekt, monkeypatch):
"""Fällt der Anbieter aus, darf keine Löschempfehlung im Gedächtnis landen."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
async def tot(system, nachricht, temperatur=0.0):
raise RuntimeError("429")
monkeypatch.setattr(agents, "frage", tot)
stand = ledger.lade(projekt)
gemeldet = []
assert await inhalt.feature_im_code(features.liste(projekt), QUELLE, "x", stand,
melde=lambda t, n=0: gemeldet.append(n)) == []
assert gemeldet == [1] # laut gescheitert, nicht still
assert stand["ohne_code"] == {}
assert stand["belege"] == {}
async def test_ein_ausfall_rettet_die_anderen_urteile(projekt, monkeypatch):
"""Eine kaputte Stelle darf die bezahlten Urteile der anderen nicht wegwerfen."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
async def launisch(system, nachricht, temperatur=0.0):
if "bedienen:kaputt" in nachricht:
raise RuntimeError("429")
return ABGEDECKT
monkeypatch.setattr(agents, "frage", launisch)
stand = ledger.lade(projekt)
gemeldet = []
await inhalt.code_im_feature(
features.liste(projekt),
[punkt(), punkt(schluessel="bedienen:kaputt", beleg="bedienen:kaputt")],
stand, melde=lambda t, n=0: gemeldet.append(n))
assert gemeldet == [1]
assert "bedienen:starte" in stand["abdeckung"]
assert "bedienen:kaputt" not in stand["abdeckung"]
async def test_projekt_ohne_karten_stuerzt_nicht_ab(projekt, monkeypatch):
monkeypatch.setattr(agents, "frage", Zaehler(FEHLT))
stand = ledger.lade(projekt)
assert await inhalt.code_im_feature([], [punkt()], stand) == []
assert await inhalt.dubletten([], stand) == []
assert await inhalt.feature_im_code([], QUELLE, "x", stand) == []
async def test_feststehendes_urteil_braucht_keine_vollen_stimmen(projekt, monkeypatch):
"""Vier von sechs sagen „abgedeckt": die zwei fehlenden könnten die Schwelle
nicht mehr reißen. Daraus einen Fehlschlag zu machen wäre Verschwendung."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
ausfaelle = {"n": 0}
async def zwei_platzen(system, nachricht, temperatur=0.0):
ausfaelle["n"] += 1
if ausfaelle["n"] <= 2:
raise RuntimeError("429")
return ABGEDECKT
monkeypatch.setattr(agents, "frage", zwei_platzen)
stand = ledger.lade(projekt)
gemeldet = []
assert await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand,
melde=lambda t, n=0: gemeldet.append(n)) == []
assert gemeldet == [] # kein Fehler: das Ergebnis stand fest
assert "bedienen:starte" in stand["abdeckung"]
async def test_offenes_urteil_bei_ausfall_wird_gemeldet(projekt, monkeypatch):
"""Vier Stimmen sagen „fehlt", zwei fehlen — mit ihnen wäre die Schwelle
erreicht. Das ist offen und muss laut scheitern, nicht still durchgehen."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
ausfaelle = {"n": 0}
async def zwei_platzen(system, nachricht, temperatur=0.0):
ausfaelle["n"] += 1
if ausfaelle["n"] <= 2:
raise RuntimeError("429")
return FEHLT
monkeypatch.setattr(agents, "frage", zwei_platzen)
stand = ledger.lade(projekt)
gemeldet = []
assert await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand,
melde=lambda t, n=0: gemeldet.append(n)) == []
assert gemeldet == [1]
assert stand["abdeckung"] == {} and stand["ohne_karte"] == {}
async def test_zwei_ueberlebende_stimmen_urteilen_nicht(projekt, monkeypatch):
"""Rechnerisch könnten zwei Stimmen die Schwelle unerreichbar machen. Als
dauerhaft gemerktes Urteil ist das zu wenig."""
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
ausfaelle = {"n": 0}
async def vier_platzen(system, nachricht, temperatur=0.0):
ausfaelle["n"] += 1
if ausfaelle["n"] <= 4:
raise RuntimeError("429")
return ABGEDECKT
monkeypatch.setattr(agents, "frage", vier_platzen)
stand = ledger.lade(projekt)
gemeldet = []
await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand,
melde=lambda t, n=0: gemeldet.append(n))
assert gemeldet == [1]
assert stand["abdeckung"] == {}
# ---------------------------------------------------------------- Vision
VISION = "<!-- titel -->\nVision\n<!-- art -->\nvision\n<!-- ziel -->\nSo soll es werden\n"
@@ -530,3 +101,65 @@ def test_keine_pruefung_fasst_die_vision_an(projekt):
assert len(features.liste(projekt)) == 2
titel = [f["sektionen"]["titel"] for f in features.pruefbar(projekt)]
assert titel == ["Eins"]
# ---------------------------------------------------------------- Prüfung 1
async def test_stilurteil_wird_gemerkt_und_nicht_neu_gefragt(projekt, monkeypatch):
"""176 Anfragen je Lauf brachten den Anbieter zum Drosseln. Unveränderter
Ausschnitt bei unverändertem Katalog heißt: das Urteil von vorhin gilt."""
import pruefung
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler('{"felder": {}}')
monkeypatch.setattr(agents, "frage", modell)
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
for a in liste:
assert await arbeite(a, None) == []
abschluss()
erste = modell.rufe
assert erste >= len(pruefung.TEMPERATUREN)
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
for a in liste:
assert await arbeite(a, None) == []
assert modell.rufe == erste # keine einzige neue Anfrage
async def test_geaenderter_text_wird_neu_gefragt(projekt, monkeypatch):
import pruefung
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
modell = Zaehler('{"felder": {}}')
monkeypatch.setattr(agents, "frage", modell)
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
for a in liste:
await arbeite(a, None)
abschluss()
vorher = modell.rufe
datei = projekte.wurzel(projekt) / f"{features.liste(projekt)[0]['id']}.md"
datei.write_text(datei.read_text().replace("- Knopf", "- Ganz anderer Text"))
liste, arbeite, _ = await pruefung._stil_aufgaben(projekt)
for a in liste:
await arbeite(a, None)
assert modell.rufe > vorher
async def test_gemessener_verstoss_kommt_nie_aus_dem_gedaechtnis(projekt, monkeypatch):
"""Sonst bliebe eine neu hinzugekommene Messregel an alten Karten wirkungslos."""
import pruefung
karte(projekt, "Eins", ("Etwas starten", "Auch Passwort prüfen."))
modell = Zaehler('{"felder": {"regeln": {"neu": "- Passwort prüfen",'
' "grund": "Füllwort", "regel": "x"}}}')
monkeypatch.setattr(agents, "frage", modell)
for lauf in range(2):
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
gefunden = [p for a in liste for p in await arbeite(a, None)]
abschluss()
assert [p["vorschlag"] for p in gefunden] == ["Passwort prüfen"], lauf
assert modell.rufe > len(pruefung.TEMPERATUREN) # zweimal wirklich gefragt

View File

@@ -33,8 +33,10 @@ def test_alle_sektionen():
"ziel": "",
"regeln": "- Passwort nie loggen\n- Sperre nach 5 Fehlversuchen",
"testen": "- Formular öffnen\n- Daten eingeben",
"stand": "gebaut",
},
{"text": "Abmeldung", "ziel": "", "regeln": "", "testen": ""},
{"text": "Abmeldung", "ziel": "", "regeln": "", "testen": "",
"stand": "gebaut"},
]
@@ -114,3 +116,22 @@ def test_marker_mit_leerraum():
def test_titel_erste_nicht_leere_zeile():
s = zerlege("<!-- titel -->\n\nErste\nZweite\n")
assert s["titel"] == "Erste"
def test_stand_offen_wird_gelesen():
"""Bestellt, aber noch nicht gebaut — sonst klagt die Vorwärtsprüfung sie an."""
s = zerlege("<!-- titel -->\nX\n<!-- funktion -->\nA\n<!-- stand -->\noffen\n"
"<!-- funktion -->\nB\n")
assert [(f["text"], f["stand"]) for f in s["funktionen"]] == [("A", "offen"),
("B", "gebaut")]
assert s["sonstiges"] == "" # der Marker landet nicht im Rest
def test_unbekannter_stand_gilt_als_gebaut():
s = zerlege("<!-- titel -->\nX\n<!-- funktion -->\nA\n<!-- stand -->\nirgendwas\n")
assert s["funktionen"][0]["stand"] == "gebaut"
def test_stand_ohne_funktion_faellt_in_sonstiges():
s = zerlege("<!-- titel -->\nX\n<!-- stand -->\noffen\n")
assert s["sonstiges"] == "offen"

View File

@@ -0,0 +1,201 @@
"""Die Planung: eine Arbeitskopie, die den Projektstand nicht anfasst.
Geprüft wird die eine Zusage — solange die Planung läuft, bleibt die Übersicht,
was sie war. Und dass sie einen Neustart übersteht, weil sie auf der Platte liegt.
"""
import features
import parser
import planung
import projekte
import pytest
def karte(projekt: str, titel: str, *funktionen: str) -> str:
roh = f"<!-- titel -->\n{titel}\n" + "".join(
f"{'-' * 40}\n<!-- funktion -->\n{f}\n<!-- regeln -->\n- Etwas gilt\n"
for f in funktionen)
return features.anlegen(projekt, roh)["id"]
def titel(projekt: str) -> list:
return sorted(f["sektionen"]["titel"] for f in features.liste(projekt))
@pytest.fixture
def projekt(datenordner):
projekte.anlegen("P")
karte("P", "Galerie", "Bild wählen")
karte("P", "Shop", "Karte kaufen")
return "P"
def test_ohne_planung_arbeitet_alles_im_projektordner(projekt):
assert not planung.laeuft(projekt)
assert projekte.wurzel(projekt) == projekte.stand(projekt)
assert planung.aenderungen(projekt) == {}
def test_planung_leitet_das_schreiben_um(projekt):
planung.starte(projekt)
assert projekte.wurzel(projekt) != projekte.stand(projekt)
karte(projekt, "Neu", "Etwas tun")
# Die Karte steht in der Kopie …
assert "Neu" in titel(projekt)
# … und nicht im Projektstand
assert not (projekte.stand(projekt) / "neu.md").exists()
def test_zweite_planung_wird_abgelehnt(projekt):
planung.starte(projekt)
with pytest.raises(ValueError):
planung.starte(projekt)
def test_planung_ueberlebt_einen_neustart(projekt):
planung.starte(projekt)
karte(projekt, "Neu", "Etwas tun")
# Nichts im Speicher — der nächste Prozess sieht nur die Platte
assert planung.laeuft(projekt)
assert "Neu" in planung.aenderungen(projekt)
def test_abbruch_laesst_den_projektstand_unberuehrt(projekt):
vorher = titel(projekt)
planung.starte(projekt)
karte(projekt, "Neu", "Etwas tun")
planung.brich_ab(projekt)
assert not planung.laeuft(projekt)
assert titel(projekt) == vorher
def test_umsetzen_macht_die_kopie_zum_stand(projekt):
planung.starte(projekt)
karte(projekt, "Neu", "Etwas tun")
planung.setze_um(projekt)
assert not planung.laeuft(projekt)
assert "Neu" in titel(projekt)
assert not (projekte.stand(projekt) / projekte.PLANUNG).exists()
def test_umsetzen_ohne_planung_ist_ein_fehler(projekt):
with pytest.raises(ValueError):
planung.setze_um(projekt)
# ---------------------------------------------------------------- Änderungen
def test_neue_karte_ist_neu(projekt):
planung.starte(projekt)
karte(projekt, "Neu", "Etwas tun")
a = planung.aenderungen(projekt)
assert a["Neu"]["art"] == "neu"
assert a["Neu"]["funktionen"] == {"Etwas tun": "neu"}
assert "Galerie" not in a
def test_entfernte_karte_ist_weg(projekt):
planung.starte(projekt)
features.loeschen(projekt, "shop")
a = planung.aenderungen(projekt)
assert a["Shop"]["art"] == "weg"
assert a["Shop"]["funktionen"] == {"Karte kaufen": "weg"}
def test_neue_funktion_wird_markiert(projekt):
planung.starte(projekt)
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Galerie")
features.speichern(projekt, f["id"],
f["roh"] + f"{'-' * 40}\n<!-- funktion -->\nBild löschen\n")
a = planung.aenderungen(projekt)
assert a["Galerie"]["art"] == "geaendert"
assert a["Galerie"]["funktionen"] == {"Bild löschen": "neu"}
def test_geaenderte_regel_wird_markiert(projekt):
planung.starte(projekt)
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Galerie")
features.speichern(projekt, f["id"], f["roh"].replace("Etwas gilt", "Anderes gilt"))
assert planung.aenderungen(projekt)["Galerie"]["funktionen"] == {
"Bild wählen": "geaendert"}
def test_umbenannte_karte_ist_neu_und_weg(projekt):
"""Verglichen wird über den Titel. Eine Umbenennung ist zwei Änderungen."""
planung.starte(projekt)
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Shop")
features.speichern(projekt, f["id"], f["roh"].replace("Shop", "Laden", 1))
a = planung.aenderungen(projekt)
assert a["Shop"]["art"] == "weg" and a["Laden"]["art"] == "neu"
def test_gleicher_inhalt_ist_keine_aenderung(projekt):
planung.starte(projekt)
f = features.liste(projekt)[0]
features.speichern(projekt, f["id"], f["roh"])
assert planung.aenderungen(projekt) == {}
# ---------------------------------------------------------------- Zurücksetzen
def test_zuruecksetzen_loescht_eine_neue_karte(projekt):
planung.starte(projekt)
karte(projekt, "Neu", "Etwas tun")
planung.zuruecksetzen(projekt, "Neu")
assert planung.aenderungen(projekt) == {}
assert "Neu" not in titel(projekt)
def test_zuruecksetzen_holt_eine_geloeschte_karte_zurueck(projekt):
planung.starte(projekt)
features.loeschen(projekt, "shop")
planung.zuruecksetzen(projekt, "Shop")
assert planung.aenderungen(projekt) == {}
assert "Shop" in titel(projekt)
def test_zuruecksetzen_stellt_den_alten_inhalt_her(projekt):
planung.starte(projekt)
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Galerie")
features.speichern(projekt, f["id"], f["roh"].replace("Etwas gilt", "Anderes gilt"))
planung.zuruecksetzen(projekt, "Galerie")
assert planung.aenderungen(projekt) == {}
def test_zuruecksetzen_laesst_die_uebrigen_stehen(projekt):
planung.starte(projekt)
karte(projekt, "Eins", "A tun")
karte(projekt, "Zwei", "B tun")
planung.zuruecksetzen(projekt, "Eins")
assert set(planung.aenderungen(projekt)) == {"Zwei"}
def test_zuruecksetzen_ohne_planung_ist_ein_fehler(projekt):
with pytest.raises(ValueError):
planung.zuruecksetzen(projekt, "Galerie")
def test_unbekannte_karte_zuruecksetzen_meldet_es(projekt):
planung.starte(projekt)
with pytest.raises(FileNotFoundError):
planung.zuruecksetzen(projekt, "Gibt es nicht")
# ---------------------------------------------------------------- Nachbarschaft
def test_die_kopie_ist_kein_eigenes_projekt(projekt):
planung.starte(projekt)
assert [p["name"] for p in projekte.liste()] == ["P"]
def test_gespraech_und_gedaechtnis_wandern_mit(projekt):
"""Was während des Planens beurteilt wurde, gilt für den geplanten Stand."""
(projekte.stand(projekt) / ".pruefung.json").write_text('{"stil": {}}')
planung.starte(projekt)
assert (projekte.wurzel(projekt) / ".pruefung.json").is_file()
def test_die_kopie_kopiert_sich_nicht_selbst(projekt):
planung.starte(projekt)
assert not (projekte.wurzel(projekt) / projekte.PLANUNG).exists()

View File

@@ -197,8 +197,9 @@ async def test_herkunft_kommt_vom_code_nicht_vom_modell(datenordner, monkeypatch
async def luegt(system, inhalt, temperatur=0.0):
# Das Modell behauptet eine fremde Funktion — das muss ignoriert werden
# „Anmelden" ist selbst ein gültiger Titel — sonst weist die Messung ihn ab
return ('{"felder": {"titel": {"regel": "Sprache: Deutsch, Präsens, keine Anrede",'
' "neu": "Anders", "grund": "x"},'
' "neu": "Einloggen", "grund": "x"},'
' "funktion": {"neu": "Erfunden", "grund": "y"}}}')
monkeypatch.setattr(agents, "frage", luegt)

View File

@@ -166,3 +166,96 @@ def test_gemessene_befunde_haengen_am_ausschnitt(datenordner):
text = pruefung.inhalt(karte, befunde)
assert "Gemessen, bitte neu schreiben:" in text
assert "Mehr als 3 Wörter" in text
def test_funktionstitel_braucht_ein_verb():
"""Ohne Verb steht dort ein Bereich oder ein Zustand — nichts Testbares."""
for titel in ("Galerie im Modal", "Preis aktualisiert", "In den Warenkorb",
"Modal tastaturgesteuert"):
assert _probleme(_pruefe("funktion", "titel", titel)) \
== ["Kein Verb im Infinitiv am Ende"], titel
def test_gute_funktionstitel_bleiben_unberuehrt():
for titel in ("Feature anlegen", "Anzeigemodus umschalten", "Option wählen",
"Nächsten Rückschritt anzeigen", "Karten prüfen", "Stand umschalten",
"Projekt aufrufen", "Bildergalerie bedienen", "PDF hochladen"):
assert _pruefe("funktion", "titel", titel) == [], titel
def test_kartentitel_darf_ohne_verb_stehen():
"""Eine Karte ist ein Gegenstand, keine Handlung."""
assert _pruefe("feature", "titel", "Karten prüfen") == []
assert _pruefe("feature", "titel", "Darkmode") == []
ABLAUF = "- Vollmodus öffnen\n- LMB auf Regeln\n- Text ändern\n- Regeln angepasst"
def test_ein_ablauf_geht_durch():
assert _pruefe("funktion", "testen", ABLAUF) == []
def test_zweiter_ablauf_wird_gemeldet():
"""So entstand „Bildergalerie bedienen": blättern, Miniatur, Großansicht in einem."""
zwei = ("- Zurück-Button klicken\n- Vorheriges Bild erscheint\n"
"- Miniaturbild klicken\n- Zugehöriges Bild erscheint")
b = _pruefe("funktion", "testen", zwei)
assert _probleme(b) == ["Zweiter Ablauf in einer Funktion"]
assert b[0]["original"] == "Miniaturbild klicken"
def test_nominalstil_ist_eine_handlung():
"""„Bestätigen im Modal" hat das Verb vorn — sonst gälte es als Ergebnis."""
text = ("- RMB auf Featurecard\n- Klick auf \"Feature entfernen\"\n"
"- Bestätigen im Modal\n- Feature entfernt")
assert _pruefe("funktion", "testen", text) == []
def test_mehrzahl_auf_en_ist_kein_verb():
"""Deutsche Mehrzahl endet auch auf -en — nur Anfang und Ende zählen."""
text = "- LMB auf Icon\n- Fehlerhafte Karten rot markiert"
assert _pruefe("funktion", "testen", text) == []
def test_zeilenfehler_haben_vorrang():
"""Erst die Zeilen in Ordnung bringen; zwei Vorschläge je Feld wären nicht
unabhängig anwendbar."""
text = "- Auch klicken.\n- Ergebnis da\n- Weiter klicken"
assert _probleme(_pruefe("funktion", "testen", text)) \
== ["Satzzeichen am Zeilenende, Füllwort „auch“"]
def test_funktion_ohne_jedes_feld_ist_leer():
"""Ein bloßer Titel beschreibt nichts — der Prüfer stülpt ihn über alles."""
b = struktur.befunde("funktion", {"titel": "Modal steuern", "ziel": "",
"regeln": "", "testen": ""})
assert _probleme(b) == ["Funktion ohne Ziel, Regeln und Testen"]
def test_ein_einziges_feld_genuegt():
for feld in ("ziel", "regeln", "testen"):
felder = {"titel": "Modal steuern", "ziel": "", "regeln": "", "testen": ""}
felder[feld] = "Etwas tun" if feld == "ziel" else "- Etwas tun"
assert struktur.befunde("funktion", felder) == [], feld
def test_einzelnes_feld_loest_nichts_aus():
"""Beim Prüfen eines Feldes sagt das Fehlen der anderen nichts."""
assert struktur.befunde("funktion", {"titel": "Modal steuern"}) == []
def test_kartenebene_darf_leer_sein():
assert struktur.befunde("feature", {"titel": "Darkmode", "badge": "kern",
"ziel": "", "regeln": ""}) == []
def test_platzhalter_ist_kein_inhalt():
"""`<testschritt>` erfüllt jede Formregel und sagt trotzdem nichts."""
assert struktur._umschreiben("funktion", "testen", "<testschritt>")
assert struktur._umschreiben("funktion", "regeln", "Noch offen ...")
assert struktur._umschreiben("funktion", "regeln", "TODO klären")
def test_kleiner_als_ist_kein_platzhalter():
assert not struktur._umschreiben("funktion", "regeln", "Wert 3 < 5")