import pytest import agents import projekte import recherche @pytest.fixture def projekt(datenordner): projekte.anlegen("P") return "P" def test_leeres_projekt_hat_keine_faeden(projekt): assert recherche.liste(projekt) == [] def test_anlegen_gibt_faden_mit_id(projekt): faden = recherche.anlegen(projekt) assert faden["id"] and faden["verlauf"] == [] assert [f["id"] for f in recherche.liste(projekt)] == [faden["id"]] def test_faeden_haben_eigene_ids(projekt): a, b = recherche.anlegen(projekt), recherche.anlegen(projekt) assert a["id"] != b["id"] assert len(recherche.liste(projekt)) == 2 def test_loeschen_trifft_nur_den_einen(projekt): a, b = recherche.anlegen(projekt), recherche.anlegen(projekt) recherche.loeschen(projekt, a["id"]) assert [f["id"] for f in recherche.liste(projekt)] == [b["id"]] def test_loeschen_ohne_faden_ist_404(projekt): with pytest.raises(FileNotFoundError): recherche.loeschen(projekt, "gibtsnicht") def test_faeden_liegen_im_projektstand_nicht_in_der_planung(projekt): """Nachgeschlagenes überlebt einen Planungsabbruch — es gehört zum Projekt.""" recherche.anlegen(projekt) (projekte.stand(projekt) / projekte.PLANUNG).mkdir() assert len(recherche.liste(projekt)) == 1 assert (projekte.stand(projekt) / recherche.DATEI).is_file() def _antwort(satz="Laut Quelle X: ja."): """Eine Modellantwort — Markdown, nicht JSON.""" async def antworte(system, inhalt, kommando=None, ordner=None): return satz return antworte async def test_frage_haengt_beide_zeilen_an(projekt, monkeypatch): faden = recherche.anlegen(projekt) monkeypatch.setattr(agents, "frage_lokal", _antwort(" Laut Quelle X: ja. ")) erg = await recherche.frage(projekt, faden["id"], "Wie geht das?") assert [(e["rolle"], e["text"]) for e in erg["verlauf"]] == [ ("person", "Wie geht das?"), ("ki", "Laut Quelle X: ja.")] async def test_erste_frage_benennt_den_faden(projekt, monkeypatch): faden = recherche.anlegen(projekt) monkeypatch.setattr(agents, "frage_lokal", _antwort("ok")) erg = await recherche.frage(projekt, faden["id"], "Wie lädt man\nzu S3 hoch?") assert erg["titel"] == "Wie lädt man zu S3 hoch?" # Die zweite Frage lässt den Namen stehen: der Reiter soll nicht wandern erg = await recherche.frage(projekt, faden["id"], "Und die Grenzen?") assert erg["titel"] == "Wie lädt man zu S3 hoch?" async def test_langer_titel_wird_gekuerzt(projekt, monkeypatch): faden = recherche.anlegen(projekt) monkeypatch.setattr(agents, "frage_lokal", _antwort("ok")) erg = await recherche.frage(projekt, faden["id"], "w" * 80) assert len(erg["titel"]) == recherche.TITELLAENGE + 1 assert erg["titel"].endswith("…") async def test_recherche_bekommt_die_websuche(projekt, monkeypatch): """Der Werkzeugsatz ist der ganze Unterschied zum Planungsgespräch.""" faden = recherche.anlegen(projekt) gesehen = {} async def antworte(system, inhalt, kommando=None, ordner=None): gesehen.update(kommando=kommando, inhalt=inhalt) return "ok" monkeypatch.setattr(agents, "frage_lokal", antworte) await recherche.frage(projekt, faden["id"], "Was gilt draußen?") assert gesehen["kommando"] == agents.RECHERCHE_KOMMANDO async def test_die_karten_liegen_im_prompt(projekt, monkeypatch): """Ohne Bestand ist „was fehlt noch" nicht zu beantworten — dann rät das Modell.""" (projekte.stand(projekt) / "gold.md").write_text( "\nGold\n\nkern\n" "\nSpieler verdient Gold pro Runde.\n" "\nZinsen erhalten\n\noffen\n" "\n- Runde spielen\n") faden = recherche.anlegen(projekt) gesehen = {} async def antworte(system, inhalt, kommando=None, ordner=None): gesehen["system"] = system return "ok" monkeypatch.setattr(agents, "frage_lokal", antworte) await recherche.frage(projekt, faden["id"], "Was fehlt noch?") assert "Zinsen erhalten" in gesehen["system"] # Der Stand ist der Punkt: er unterscheidet Gebautes von Bestelltem assert '"stand": "offen"' in gesehen["system"] # Testabläufe blähen den Prompt und tragen keine Recherche assert "Runde spielen" not in gesehen["system"] async def test_vision_kommt_mit(projekt, monkeypatch): (projekte.stand(projekt) / "vision.md").write_text( "\nVision\n\nvision\n" "\nTFT im Browser nachbauen.\n") faden = recherche.anlegen(projekt) gesehen = {} async def antworte(system, inhalt, kommando=None, ordner=None): gesehen["system"] = system return "ok" monkeypatch.setattr(agents, "frage_lokal", antworte) await recherche.frage(projekt, faden["id"], "Und nun?") assert "TFT im Browser nachbauen." in gesehen["system"] async def test_markdown_bleibt_wie_es_kam(projekt, monkeypatch): """Die Antwort wird als Markdown angezeigt — der Server fasst sie nicht an.""" roh = "Kurz gesagt.\n\n## Festgelegt\n- **5 Gold** pro Runde\n\n## Quellen\n- [Wiki](http://x)" faden = recherche.anlegen(projekt) monkeypatch.setattr(agents, "frage_lokal", _antwort(f"\n{roh}\n")) erg = await recherche.frage(projekt, faden["id"], "Wie geht Gold?") assert erg["verlauf"][-1]["text"] == roh async def test_leere_frage_wird_abgelehnt(projekt): faden = recherche.anlegen(projekt) with pytest.raises(ValueError): await recherche.frage(projekt, faden["id"], " ") async def test_geschlossener_faden_verwirft_die_antwort(projekt, monkeypatch): """Antworten dauern Minuten. Wer inzwischen schließt, will das Ergebnis nicht.""" faden = recherche.anlegen(projekt) async def antworte(system, inhalt, kommando=None, ordner=None): recherche.loeschen(projekt, faden["id"]) return "zu spät" monkeypatch.setattr(agents, "frage_lokal", antworte) with pytest.raises(FileNotFoundError): await recherche.frage(projekt, faden["id"], "Was gilt?") assert recherche.liste(projekt) == [] async def test_paralleler_faden_bleibt_erhalten(projekt, monkeypatch): """Der zweite Faden entsteht, während der erste noch wartet.""" a = recherche.anlegen(projekt) neu = {} async def antworte(system, inhalt, kommando=None, ordner=None): neu["b"] = recherche.anlegen(projekt) return "fertig" monkeypatch.setattr(agents, "frage_lokal", antworte) await recherche.frage(projekt, a["id"], "Frage") faeden = recherche.liste(projekt) assert [f["id"] for f in faeden] == [a["id"], neu["b"]["id"]] # Was der eine Faden erfährt, bleibt beim einen assert len(faeden[0]["verlauf"]) == 2 and faeden[1]["verlauf"] == [] def test_kaputte_datei_gilt_als_leer(projekt): (projekte.stand(projekt) / recherche.DATEI).write_text("{kein json") assert recherche.liste(projekt) == [] async def test_tiefe_haelt_den_faden_kurz(projekt, monkeypatch): faden = recherche.anlegen(projekt) monkeypatch.setattr(agents, "frage_lokal", _antwort("ok")) for i in range(recherche.TIEFE): await recherche.frage(projekt, faden["id"], f"Frage {i}") verlauf = recherche.liste(projekt)[0]["verlauf"] assert len(verlauf) == recherche.TIEFE assert verlauf[-2]["text"] == f"Frage {recherche.TIEFE - 1}"