"""Architecture correction pass. Run from backend/: python tests/test_architecture_correction.py""" from __future__ import annotations import json import os import sys import tempfile import uuid from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) os.environ["KANSHO_DB_PATH"] = str(Path(tempfile.gettempdir()) / "kansho-arch-correction-test.sqlite") os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1" Path(os.environ["KANSHO_DB_PATH"]).unlink(missing_ok=True) from fastapi.testclient import TestClient from db import get_db, migrate_journal_source_refs from main import app from privacy_gateway import reset_debug 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: reset_debug() with TestClient(app) as client: setup = client.post( "/api/auth/setup", json={"email": "lars@example.test", "name": "Lars", "password": "test-pass"}, ) headers = header(setup.json()["token"]) profile_id = setup.json()["profile_id"] space = client.post("/api/journal/spaces", headers=headers, json={"title": "Alltag"}) expect(space.status_code == 200, "space created") space_id = space.json()["id"] day = client.post( f"/api/journal/spaces/{space_id}/days", headers=headers, json={"calendar_date": "2026-08-20"}, ) day_id = day.json()["day"]["id"] conv = client.post( f"/api/journal/days/{day_id}/conversations", headers=headers, json={"title": "Quelle"}, ) conv_id = conv.json()["id"] turn = client.post( f"/api/journal/conversations/{conv_id}/turn", headers=headers, json={"body": "Heute war der Markt voll. Ich habe Kirschen gekauft."}, ) expect(turn.status_code == 200, "source turn") gen = client.post( f"/api/journal/days/{day_id}/generate", headers=headers, json={"conversation_ids": [conv_id]}, ) expect(gen.status_code == 200, "draft from conversation") draft = gen.json() expect(draft["source_conversation_ids"] == [conv_id], "draft reconstructs conversation ids") expect(len(draft["source_message_ids"]) >= 2, "draft reconstructs message ids") with get_db() as conn: refs = conn.execute( "SELECT source_kind, source_id FROM journal_draft_source_refs WHERE draft_id = ?", (draft["id"],), ).fetchall() kinds = {row["source_kind"] for row in refs} expect("conversation" in kinds and "message" in kinds, "draft provenance is relational") saved = client.post( "/api/journal/entries", headers=headers, json={ "journal_day_id": day_id, "title": draft.get("title") or "Markttag", "body": draft.get("body") or "Kirschen", "origin": "accepted_draft", "entry_id": None, "source_conversation_ids": draft["source_conversation_ids"], "source_message_ids": draft["source_message_ids"], }, ) expect(saved.status_code == 200, f"save entry {saved.text}") entry = saved.json() expect(entry["source_conversation_ids"] == [conv_id], "entry version reconstructs conversation ids") expect(entry["source_message_ids"] == draft["source_message_ids"], "entry version reconstructs message ids") versions = client.get(f"/api/journal/entries/{entry['id']}/versions", headers=headers) expect(versions.status_code == 200, "list versions") expect(versions.json()[0]["source_conversation_ids"] == [conv_id], "version list keeps source links") legacy_draft_id = str(uuid.uuid4()) legacy_version_id = str(uuid.uuid4()) legacy_entry_id = str(uuid.uuid4()) mid = draft["source_message_ids"][0] with get_db() as conn: conn.execute( """ INSERT INTO journal_drafts (id, profile_id, journal_day_id, title, body, source_conversation_ids, source_message_ids, as_of, superseded_at) VALUES (?, ?, ?, 'Legacy', 'alt', ?, ?, datetime('now'), datetime('now')) """, (legacy_draft_id, profile_id, day_id, json.dumps([conv_id]), json.dumps([mid])), ) conn.execute( """ INSERT INTO journal_entries (id, profile_id, journal_day_id, space_id) VALUES (?, ?, ?, ?) """, (legacy_entry_id, profile_id, day_id, space_id), ) conn.execute( """ INSERT INTO journal_entry_versions (id, profile_id, entry_id, title, body, source_conversation_ids, source_message_ids, origin) VALUES (?, ?, ?, 'Legacy', 'alt', ?, ?, 'user_edit') """, (legacy_version_id, profile_id, legacy_entry_id, json.dumps([conv_id]), json.dumps([mid])), ) migrate_journal_source_refs(conn) draft_refs = [ (row["source_kind"], row["source_id"]) for row in conn.execute( "SELECT source_kind, source_id FROM journal_draft_source_refs WHERE draft_id = ? ORDER BY sort_order", (legacy_draft_id,), ).fetchall() ] version_refs = [ (row["source_kind"], row["source_id"]) for row in conn.execute( "SELECT source_kind, source_id FROM journal_entry_version_source_refs WHERE version_id = ? ORDER BY sort_order", (legacy_version_id,), ).fetchall() ] expect(("conversation", conv_id) in draft_refs, "legacy draft conversation migrated") expect(("message", mid) in draft_refs, "legacy draft message migrated") expect(("conversation", conv_id) in version_refs, "legacy version conversation migrated") expect(("message", mid) in version_refs, "legacy version message migrated") usage = client.post("/api/dialogue/sessions", headers=headers, json={"intent": "space-only"}) space_conv = client.post( "/api/dialogue/conversations", headers=headers, json={"usage_session_id": usage.json()["id"], "title": "Plan", "space_id": space_id}, ) expect(space_conv.status_code == 200, f"space conversation without day {space_conv.text}") expect(space_conv.json()["space_id"] == space_id, "conversation assigned to space") expect(not space_conv.json().get("journal_day_id"), "conversation has no journal day") plan_turn = client.post( f"/api/dialogue/conversations/{space_conv.json()['id']}/turn", headers=headers, json={"body": "Wir wollen morgen zum Hafen, das ist geplant."}, ) expect(plan_turn.status_code == 200, "space-only turn works without journal day") later = client.post( f"/api/journal/spaces/{space_id}/days", headers=headers, json={"calendar_date": "2026-08-21"}, ) later_id = later.json()["day"]["id"] later_conv = client.post( f"/api/journal/days/{later_id}/conversations", headers=headers, json={"title": "Heute"}, ) reset_debug() later_turn = client.post( f"/api/journal/conversations/{later_conv.json()['id']}/turn", headers=headers, json={"body": "Heute früh aufgestanden."}, ) expect(later_turn.status_code == 200, "later day turn") rendered = (later_turn.json().get("trace") or {}).get("intern") or "" expect("Hafen" in rendered, "earlier space source can supply a known plan") expect("Heute früh aufgestanden" in rendered, "current conversation remains primary") sibling = client.post( f"/api/journal/days/{later_id}/conversations", headers=headers, json={"title": "Anderes"}, ) reset_debug() sibling_turn = client.post( f"/api/journal/conversations/{sibling.json()['id']}/turn", headers=headers, json={"body": "Nur ein kurzer Einkauf."}, ) expect(sibling_turn.status_code == 200, "sibling turn") sibling_rendered = (sibling_turn.json().get("trace") or {}).get("intern") or "" expect("Heute früh aufgestanden" not in sibling_rendered, "same-day sibling is not fully loaded") expect("Nur ein kurzer Einkauf" in sibling_rendered, "current sibling conversation is primary") from context_builder import assemble_text, build_internal_context ctx = build_internal_context(profile_id, conversation_id=later_conv.json()["id"], purpose="dialogue_turn") assembled = assemble_text(ctx) expect(not assembled.get("writing_profile"), "dialogue turn has no writing profile brief") expect("interaction_hint" in assembled, "dialogue turn exposes interaction hint slot") expect(assembled.get("interaction_hint"), "interaction hint is filled from labeled defaults") expect("Produktdefault" in (assembled.get("interaction_hint") or ""), "defaults are labeled, not anonymous") expect(any(item.get("type") == "interaction_hint" for item in ctx["items"]), "interaction slot is a context item") expect( not any(item.get("type") == "writing_profile" for item in ctx["items"]), "writing profile is not a dialogue kernel dependency", ) expect("space_recent_sources" in {item.get("type") for item in ctx["items"]}, "space source spec is selected") journal_ctx = build_internal_context( profile_id, conversation_id=conv_id, journal_day_id=day_id, space_id=space_id, purpose="journal_generate", conversation_ids=[conv_id], ) expect( any(item.get("type") == "writing_profile" for item in journal_ctx["items"]), "journal generate still uses writing profile", ) expect( not any(item.get("type") == "space_recent_sources" for item in journal_ctx["items"]), "journal generate does not pull space conversation excerpts", ) from conversation_signals import infer_signals, similar_enough from journal_policy import consolidation_offer chronicle = infer_signals(["Heute Vormittag zum Markt, danach Kirschen gekauft."]) deep = infer_signals(["Tiefer: der Spaziergang hat mich berührt."]) expect(chronicle["narrative_mode"] == "chronicle", "chronicle mode from user text") expect(deep["narrative_mode"] == "reflective", "reflective mode from user text") expect(not similar_enough([chronicle, deep]), "different modes are not similar") expect(consolidation_offer([chronicle, deep]) is False, "policy does not offer unlike modes") twin = infer_signals(["Nach dem Markt noch Brot geholt, dann heimgegangen."]) expect(similar_enough([chronicle, twin]), "two chronicles can be offered") nav = Path(ROOT).parent / "frontend" / "src" / "config" / "appNav.js" nav_text = nav.read_text(encoding="utf-8") expect("/dialog" not in nav_text, "/dialog is not in normal product navigation") expect("'/journal'" in nav_text, "journal remains in product navigation") print("architecture correction tests passed.") if __name__ == "__main__": main()