Files
planer2/backend/tests/test_recherche.py
2026-09-07 07:44:00 +02:00

194 lines
7.3 KiB
Python

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(
"<!-- titel -->\nGold\n<!-- art -->\nkern\n"
"<!-- ziel -->\nSpieler verdient Gold pro Runde.\n"
"<!-- funktion -->\nZinsen erhalten\n<!-- stand -->\noffen\n"
"<!-- testen -->\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(
"<!-- titel -->\nVision\n<!-- art -->\nvision\n"
"<!-- ziel -->\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}"