160 lines
6.1 KiB
Python
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()
|