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

112 lines
3.9 KiB
Python

"""Was das Planungsgespräch darf — und was es nicht sieht.
Geprüft wird nicht die Antwort des Modells, sondern womit es losgeschickt wird:
Werkzeugsatz, Arbeitsverzeichnis und der Absatz im Systemprompt gehören zusammen.
Fällt eines auseinander, behauptet das Modell nachgesehen zu haben, ohne zu können.
"""
import json
import pytest
import agents
import chat
import projekte
import verzeichnis
KARTE = ("<!-- titel -->\nUpload\n<!-- art -->\nkern\n"
"<!-- ziel -->\nDateien landen im Speicher.\n"
"<!-- funktion -->\nDatei wählen\n<!-- testen -->\n- Dialog öffnen\n")
@pytest.fixture
def projekt(datenordner):
projekte.anlegen("P")
(datenordner / "P" / "upload.md").write_text(KARTE)
return "P"
@pytest.fixture
def mitschrift(monkeypatch):
"""Fängt den Aufruf ab und gibt eine gültige Antwort zurück."""
gesehen = {}
async def antworte(system, inhalt, kommando=None, ordner=None):
gesehen.update(system=system, inhalt=inhalt, kommando=kommando, ordner=ordner)
return json.dumps({"antwort": "Verstanden.", "karten": []})
monkeypatch.setattr(agents, "frage_lokal", antworte)
return gesehen
async def test_ohne_verzeichnis_kein_lesen(projekt, mitschrift):
await chat.frage(projekt, "Was kann das Projekt?")
assert mitschrift["kommando"] == agents.PLANUNG_OHNE_CODE
assert mitschrift["ordner"] is None
assert chat.CODE_OHNE in mitschrift["system"]
async def test_mit_verzeichnis_darf_lesen(projekt, mitschrift, tmp_path):
code = tmp_path / "quelltext"
code.mkdir()
verzeichnis.setze(projekt, str(code))
await chat.frage(projekt, "Gibt es den Upload schon?")
assert mitschrift["kommando"] == agents.PLANUNG_KOMMANDO
assert mitschrift["ordner"] == str(code.resolve())
assert chat.CODE_MIT in mitschrift["system"]
async def test_toter_pfad_zaehlt_wie_keiner(projekt, mitschrift, tmp_path):
"""Sonst startet der Prozess nicht, und die Meldung zeigt auf die CLI."""
code = tmp_path / "weg"
code.mkdir()
verzeichnis.setze(projekt, str(code))
code.rmdir()
await chat.frage(projekt, "Und jetzt?")
assert mitschrift["kommando"] == agents.PLANUNG_OHNE_CODE
assert mitschrift["ordner"] is None
async def test_planung_sucht_nicht_im_web(projekt, mitschrift, tmp_path):
"""Fremdes Wissen kommt aus der Recherche, nicht nebenbei in einen Entwurf."""
code = tmp_path / "quelltext"
code.mkdir()
verzeichnis.setze(projekt, str(code))
await chat.frage(projekt, "Wie macht man das üblicherweise?")
for satz in (agents.PLANUNG_KOMMANDO, agents.PLANUNG_OHNE_CODE):
assert "WebSearch" not in _erlaubt(satz)
assert "WebFetch" not in _erlaubt(satz)
def test_recherche_sieht_keinen_quelltext():
"""Umgekehrt: aus dem Projekt darf nichts in eine Suchanfrage geraten."""
erlaubt = _erlaubt(agents.RECHERCHE_KOMMANDO)
assert "WebSearch" in erlaubt and "WebFetch" in erlaubt
for werkzeug in ("Read", "Glob", "Grep"):
assert werkzeug not in erlaubt
@pytest.mark.parametrize("kommando", [
agents.PLANUNG_KOMMANDO, agents.PLANUNG_OHNE_CODE, agents.RECHERCHE_KOMMANDO])
def test_kein_satz_kann_etwas_anstossen(kommando):
"""Fremder Text im Kontext darf nichts auslösen, egal woher er kommt."""
gesperrt = _gesperrt(kommando)
for werkzeug in ("Write", "Edit", "Bash", "CronCreate", "PushNotification",
"SendMessage", "Workflow", "Task"):
assert werkzeug in gesperrt
assert "--safe-mode" in kommando
def _abschnitt(kommando: tuple, flagge: str) -> list:
rest = list(kommando[kommando.index(flagge) + 1:])
ende = next((i for i, w in enumerate(rest) if w.startswith("--")), len(rest))
return rest[:ende]
def _erlaubt(kommando: tuple) -> list:
return _abschnitt(kommando, "--allowed-tools")
def _gesperrt(kommando: tuple) -> list:
return _abschnitt(kommando, "--disallowed-tools")