"""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 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()