bifroest/tests/test_chat_comfort.py
2026-10-27 08:34:17 +01:00

129 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Chat-Komfort: Ergebnistabellen mit CSV und Diagramm, Antwort neu erzeugen, Export, Suche."""
import pytest
from sqlalchemy import func, select
from app.chat.models import Message
from app.core import charts
from app.identity import Principal
from app.tasks.models import TaskRun
from app.tasks.worker import Worker
from app.tools import service as tools
from helpers import create_workspace
CSV = b"region,umsatz,monat\nNord,100,2025-01\nSued,250,2025-01\nNord,50,2025-02\n"
QUERY = "SELECT region, sum(umsatz) AS summe FROM umsatz GROUP BY 1 ORDER BY 1"
@pytest.fixture
def ws(browser, people, db):
anna = browser("anna")
workspace_id = create_workspace(anna, people["team_demo"], "Analyse")
anna.post(f"/workspaces/{workspace_id}/members", {"username": "ben", "role": "member"})
anna.post(f"/workspaces/{workspace_id}/uploads", files={"file": ("umsatz.csv", CSV)})
admin = Principal(user_id=people["anna"], username="anna", display_name="Anna", is_admin=True,
team_ids=frozenset({people["team_demo"]}))
with db.session() as session:
tools.classify_suggested(session, admin)
chat_id = int(anna.post(f"/workspaces/{workspace_id}/chats").headers["location"].rsplit("/", 1)[1])
return {"anna": anna, "ben": browser("ben"), "dora": browser("dora"), "id": workspace_id, "chat_id": chat_id}
def ask(ws, text: str = "Umsatz je Region?"):
ws["anna"].post(f"/chats/{ws['chat_id']}/messages", {"content": text}, headers={"HX-Request": "true"})
def test_ergebnis_mit_tabelle_csv_und_diagramm(ws, db, settings, fake_provider):
fake_provider.script = [[("data_query", {"sql": QUERY}), ("data_query", {"sql": "DELETE FROM umsatz"})],
"Nord 150, Sued 250."]
ask(ws)
Worker(db, settings).run_once()
page = ws["ben"].get(f"/chats/{ws['chat_id']}").text
# Nur die erfolgreiche Abfrage hat ein Ergebnis
assert page.count("Abfrageergebnis:") == 1
assert "Abfrageergebnis: 2 Zeilen, 2 Spalten · mit Diagramm" in page
assert '<svg class="aiw-chart-svg"' in page and "Nord: summe 150" in page
call_id = int(page.split('href="/tool-calls/')[1].split(".csv")[0])
response = ws["ben"].get(f"/tool-calls/{call_id}.csv")
assert response.headers["content-type"].startswith("text/csv")
assert response.content.decode("utf-8-sig").splitlines() == ["region;summe", "Nord;150", "Sued;250"]
assert ws["dora"].get(f"/tool-calls/{call_id}.csv").status_code == 404
def test_antwort_neu_erzeugen(ws, db, settings, fake_provider):
fake_provider.script = ["Erste Antwort."]
ask(ws)
Worker(db, settings).run_once()
page = ws["ben"].get(f"/chats/{ws['chat_id']}").text
assert "Antwort neu erzeugen" in page
fake_provider.script = ["Zweite Antwort."]
response = ws["ben"].post(f"/chats/{ws['chat_id']}/regenerate")
assert response.status_code == 303
# Während der neue Run läuft: kein zweites Mal
assert ws["ben"].post(f"/chats/{ws['chat_id']}/regenerate").status_code == 400
Worker(db, settings).run_once()
page = ws["ben"].get(f"/chats/{ws['chat_id']}").text
assert "Zweite Antwort." in page and "Erste Antwort." not in page
with db.session() as session:
assert session.scalar(select(func.count()).select_from(Message)) == 2
assert session.scalar(select(func.count()).select_from(TaskRun)) == 2
# Die Frage ging erneut an das Modell, ohne die verworfene Antwort
last_input = fake_provider.calls[-1]["input"]
assert last_input[-1] == {"role": "user", "content": "Umsatz je Region?"}
assert ws["dora"].post(f"/chats/{ws['chat_id']}/regenerate").status_code == 404
def test_erneut_versuchen_nach_fehler(ws, db, settings, fake_provider):
ask(ws)
with db.session() as session:
run = session.scalar(select(TaskRun))
run.status = "failed"
page = ws["anna"].get(f"/chats/{ws['chat_id']}").text
assert "Erneut versuchen" in page
fake_provider.script = ["Jetzt geht es."]
ws["anna"].post(f"/chats/{ws['chat_id']}/regenerate")
Worker(db, settings).run_once()
assert "Jetzt geht es." in ws["anna"].get(f"/chats/{ws['chat_id']}").text
def test_export_als_markdown(ws, db, settings, fake_provider):
fake_provider.script = [[("data_query", {"sql": QUERY})], "Nord **150**."]
ask(ws)
Worker(db, settings).run_once()
response = ws["ben"].get(f"/chats/{ws['chat_id']}/export.md")
assert response.headers["content-type"].startswith("text/markdown")
assert 'filename="Umsatz-je-Region.md"' in response.headers["content-disposition"]
text = response.text
assert text.startswith("# Umsatz je Region?") and "Workspace: Analyse" in text
assert "## Frage – Anna" in text and "Nord **150**." in text and "`data_query`" in text
assert ws["dora"].get(f"/chats/{ws['chat_id']}/export.md").status_code == 404
def test_suche_in_chats(ws, db, settings, fake_provider):
fake_provider.script = ["Die Marge liegt bei 12 %."]
ask(ws, "Wie hoch ist die Marge?")
Worker(db, settings).run_once()
other = int(ws["anna"].post(f"/workspaces/{ws['id']}/chats").headers["location"].rsplit("/", 1)[1])
ws["anna"].post(f"/chats/{other}/rename", {"title": "Lagerbestand"})
base = f"/workspaces/{ws['id']}"
page = ws["ben"].get(f"{base}?q=12 %").text.split('id="chats"')[1]
assert "Wie hoch ist die Marge?" in page and "Lagerbestand" not in page and "Die Marge liegt bei 12 %" in page
page = ws["ben"].get(f"{base}?q=lager").text
assert "Lagerbestand" in page and "Marge" not in page.split('id="chats"')[1]
# % ist kein Platzhalter
assert "Kein Chat enthält" in ws["ben"].get(f"{base}?q=%25%25%25").text
def test_diagramm_auswahl():
assert charts.plan(["region", "summe"], [["Nord", 1], ["Süd", 2]]).kind == "bar"
assert charts.plan(["monat", "a", "b"], [["2025-01", 1, 2], ["2025-02", 2, 3], ["2025-03", 4, 1]]).kind == "line"
assert charts.plan(["jahr", "umsatz"], [[2023, 1], [2024, 2], [2025, 3]]).kind == "line"
# Ungeeignet: nur Text, nur eine Spalte, zu viele Zeilen
assert charts.plan(["a", "b"], [["x", "y"]]) is None
assert charts.plan(["a"], [[1]]) is None
assert charts.plan(["k", "v"], [[str(i), i] for i in range(charts.MAX_ROWS + 1)]) is None
# Höchstens drei Reihen; negative Werte erlaubt
found = charts.plan(["k", "a", "b", "c", "d"], [["x", 1, -2, 3, 4]])
assert found.values == [1, 2, 3]
svg = charts.render(["name", "wert"], [["<script>alert(1)</script>", 5], ["b", -3]])
assert "<script>" not in svg and "&lt;script&gt;" in svg