"""Das Gedächtnis der Inhaltsprüfungen: 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. """ import json import pytest import agents import features import inhalt import ledger import projekte @pytest.fixture def projekt(datenordner): projekte.anlegen("Test") return "Test" def karte(projekt, titel, *funktionen): """funktionen sind (name, regel) oder (name, regel, [testschritte]).""" text = f"\n{titel}\n" for eintrag in funktionen: name, regel, schritte = (*eintrag, [])[:3] text += f"\n{name}\n\n- {regel}\n" if schritte: text += "\n" + "".join(f"- {s}\n" for s in schritte) return features.anlegen(projekt, text) QUELLE = {"frontend/src/App.vue": '\n'} class Zaehler: """Ersetzt das Modell und zählt mit, wie oft überhaupt gefragt wurde.""" def __init__(self, antwort): self.antwort = antwort self.rufe = 0 async def __call__(self, system, nachricht, temperatur=0.0): self.rufe += 1 wert = self.antwort return wert(nachricht, self.rufe) if callable(wert) else wert # ---------------------------------------------------------------- Datei def test_leeres_gedaechtnis_hat_alle_faecher(projekt): stand = ledger.lade(projekt) assert set(stand) == set(ledger.FAECHER) assert all(v == {} for v in stand.values()) def test_kaputte_datei_wird_wie_leer_behandelt(projekt): (projekte.wurzel(projekt) / ledger.DATEI).write_text("{kein json") assert ledger.lade(projekt)["belege"] == {} def test_gemerktes_ueberlebt_den_lauf(projekt): stand = ledger.lade(projekt) stand["belege"][ledger.schluessel("a", "Tun")] = {"abdruck": "x", "datei": "a.py", "beleg": "def tun():"} ledger.sichere(projekt, stand) assert ledger.lade(projekt)["belege"] == stand["belege"] def test_schluessel_bleibt_zerlegbar(projekt): """Titel dürfen alles enthalten — auch das, was als Trennzeichen taugen würde.""" fies = 'Klick auf "x", dann | und \\ und ["a","b"]' assert ledger.teile(ledger.schluessel("karte", fies)) == ["karte", fies] def test_gedaechtnis_ist_kein_feature(projekt): karte(projekt, "Eins") ledger.sichere(projekt, ledger.lade(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": ''}) 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": "
nichts mehr
\n"} await inhalt.pruefe_feature_im_code(projekt, alle, anders, "### x\n
") 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=''): 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": ''} 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 = "\nVision\n\nvision\n\nSo soll es werden\n" def test_vision_ist_eine_eigene_kartenart(projekt): f = features.anlegen(projekt, VISION) assert f["sektionen"]["art"] == "vision" assert f["sektionen"]["ziel"] == "So soll es werden" def test_unbekannte_art_bleibt_kern(projekt): f = features.anlegen(projekt, "\nX\n\nwichtig\n") assert f["sektionen"]["art"] == "kern" def test_keine_pruefung_fasst_die_vision_an(projekt): """Die Vision ist die Vorlage, nicht der Prüfling.""" karte(projekt, "Eins", ("Etwas starten", "Knopf")) features.anlegen(projekt, VISION) assert len(features.liste(projekt)) == 2 titel = [f["sektionen"]["titel"] for f in features.pruefbar(projekt)] assert titel == ["Eins"]