Kansho/backend/tests/test_dialogue_memory.py
Lars 36b9a2e424
Some checks failed
Deploy Development / deploy (push) Successful in 55s
Test Suite / backend-postgres (push) Failing after 7s
Test Suite / frontend-build (push) Successful in 16s
Run the backend suite on Dev Postgres instead of isolated SQLite.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 14:28:17 +02:00

160 lines
6.1 KiB
Python

"""Dialogue memory contracts. Run from backend/: python tests/test_dialogue_memory.py"""
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
from fastapi.testclient import TestClient
from main import app
def expect(ok: bool, message: str) -> None:
if not ok:
raise SystemExit(f"FAIL: {message}")
print(f"OK {message}")
def header(token: str) -> dict:
return {"X-Auth-Token": token}
def main() -> None:
with TestClient(app) as client:
setup = client.post(
"/api/auth/setup",
json={"email": "lars@example.test", "name": "Lars", "password": "test-pass"},
)
token = setup.json()["token"]
headers = header(token)
usage = client.post("/api/dialogue/sessions", headers=headers, json={"intent": "festhalten"})
expect(usage.status_code == 200, f"start usage session {usage.text}")
usage_id = usage.json()["id"]
conv = client.post(
"/api/dialogue/conversations",
headers=headers,
json={"usage_session_id": usage_id, "title": "Testgespräch"},
)
expect(conv.status_code == 200, f"create conversation {conv.text}")
cid = conv.json()["id"]
msg = client.post(
"/api/dialogue/conversations/{}/messages".format(cid),
headers=headers,
json={"body": "Heute war der Tag anstrengend.", "id": "msg-fixed-1"},
)
expect(msg.status_code == 200 and msg.json()["seq"] == 1, f"append message {msg.text}")
mid = msg.json()["id"]
again = client.post(
"/api/dialogue/conversations/{}/messages".format(cid),
headers=headers,
json={"body": "Heute war der Tag anstrengend.", "id": "msg-fixed-1"},
)
expect(again.status_code == 200 and again.json()["id"] == mid, "idempotent message id")
thread = client.post("/api/dialogue/threads", headers=headers, json={"title": "Offener Faden", "status": "open"})
expect(thread.status_code == 200, "create thread")
tid = thread.json()["id"]
thread_status = thread.json()["status"]
client.post(f"/api/dialogue/conversations/{cid}/threads/{tid}", headers=headers)
derived_bad = client.post(
"/api/dialogue/derived",
headers=headers,
json={
"kind": "thread_memory",
"subject_type": "conversation",
"subject_id": cid,
"source_message_ids": [],
"body": "ohne quelle",
},
)
expect(derived_bad.status_code == 400, "derived without provenance rejected")
expect(derived_bad.json()["detail"]["code"] == "provenance_required", "provenance code")
unknown = client.post(
"/api/dialogue/derived",
headers=headers,
json={
"kind": "journal_summary",
"subject_type": "conversation",
"subject_id": cid,
"source_message_ids": [mid],
"body": "nein",
},
)
expect(unknown.status_code == 400, "unregistered kind rejected")
derived = client.post(
"/api/dialogue/derived",
headers=headers,
json={
"kind": "working_context_snapshot",
"subject_type": "conversation",
"subject_id": cid,
"source_message_ids": [mid],
"body": "Arbeitsstand, nicht Erkenntnis",
},
)
expect(derived.status_code == 200, f"derived with provenance {derived.text}")
ended = client.post(f"/api/dialogue/sessions/{usage_id}/end", headers=headers)
expect(ended.status_code == 200, f"end usage session {ended.text}")
expect(ended.json()["threads_unchanged"] is True, "checkpoint reports threads unchanged")
expect(ended.json()["llm"] is False, "no llm on checkpoint")
messages = client.get(f"/api/dialogue/conversations/{cid}/messages", headers=headers)
expect(len(messages.json()) == 1, "original survives session end")
expect(messages.json()[0]["body"].startswith("Heute"), "original body intact")
from db import get_db, row_to_dict
with get_db() as conn:
row = row_to_dict(conn.execute("SELECT status FROM threads WHERE id = ?", (tid,)).fetchone())
expect(row["status"] == thread_status, "session end did not change thread status")
other = client.post(
"/api/users",
headers=headers,
json={"email": "ute@example.test", "name": "Ute", "password": "user-pass", "role": "user"},
)
login = client.post("/api/auth/login", json={"email": "ute@example.test", "password": "user-pass"})
other_headers = header(login.json()["token"])
stolen = client.get(f"/api/dialogue/conversations/{cid}", headers=other_headers)
expect(stolen.status_code == 404, "other profile cannot read conversation")
ctx = client.get(f"/api/dialogue/conversations/{cid}/context", headers=headers)
expect(ctx.status_code == 200 and ctx.json()["egress"] is False, "internal context does not egress")
admin = client.get("/api/admin/dialogue", headers=headers)
expect(admin.status_code == 200, "admin dialogue inventory")
expect(admin.json()["instance"]["messages"] >= 1, "admin sees message count")
detail = client.get(f"/api/admin/dialogue/conversations/{cid}", headers=headers)
expect(len(detail.json()["original_messages"]) == 1, "admin original vs derived split")
expect(len(detail.json()["derived_records"]) == 1, "derived listed separately")
spoof = client.get(
f"/api/dialogue/conversations/{cid}",
headers={**headers, "X-Profile-Id": other.json()["id"]},
)
expect(spoof.status_code == 200, "identity from session not profile header")
print("All dialogue memory tests passed.")
if __name__ == "__main__":
main()