"""Smoke test for the local product frame. Run from backend/: python tests/test_frame.py""" from __future__ import annotations import json import os import sys import tempfile 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-frame-test.sqlite") os.environ["KANSHO_PROVIDER_KEY"] = "" os.environ["KANSHO_DETECT_PROVIDER_KEY"] = "" Path(os.environ["KANSHO_DB_PATH"]).unlink(missing_ok=True) 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 auth_header(token: str) -> dict: return {"X-Auth-Token": token} def main() -> None: seed_prompts = json.loads((ROOT / "config" / "prompts.seed.json").read_text(encoding="utf-8")) slugs = {item["slug"] for item in seed_prompts} expect(slugs == {"mvp.dialogue_turn", "mvp.journal_generate", "mvp.entity_detect", "mvp.profile_review"}, "mvp prompts seeded") with TestClient(app) as client: health = client.get("/api/health") expect(health.status_code == 200 and health.json()["service"] == "kansho", "health") status = client.get("/api/auth/status") expect(status.json()["needs_setup"] is True, "needs setup before first profile") denied = client.get("/api/auth/me") expect(denied.status_code == 401, "me without session is 401") setup = client.post( "/api/auth/setup", json={"email": "lars@example.test", "name": "Lars", "password": "test-pass"}, ) expect(setup.status_code == 200, f"setup {setup.text}") token = setup.json()["token"] headers = auth_header(token) expect(setup.json()["role"] == "admin", "first profile is admin") again = client.post( "/api/auth/setup", json={"email": "other@example.test", "name": "Other", "password": "test-pass"}, ) expect(again.status_code == 409, "second setup is blocked") me = client.get("/api/auth/me", headers=headers) expect(me.status_code == 200 and me.json()["email"] == "lars@example.test", "me with session") spoof = client.get( "/api/auth/me", headers={**headers, "X-Profile-Id": "someone-else"}, ) expect(spoof.json()["id"] == me.json()["id"], "identity comes from session, not profile header") login = client.post( "/api/auth/login", json={"email": "lars@example.test", "password": "test-pass"}, ) expect(login.status_code == 200, "login") token = login.json()["token"] headers = auth_header(token) admin = client.get("/api/admin/health", headers=headers) expect(admin.status_code == 200, "admin health") expect(admin.json()["inventory"]["prompts"] == 3, "three mvp prompts") providers = client.get("/api/admin/providers", headers=headers) expect(providers.status_code == 200, "admin providers") roles = {item["role"]: item for item in providers.json()["roles"]} expect(set(roles) == {"generate", "detect"}, "two provider roles") expect(roles["generate"]["ready"] is False, "generate fail-closed without key") expect("sk-" not in providers.text, "provider status has no secret") expect("KANSHO_PROVIDER_KEY=" not in providers.text, "env assignment not leaked") saved = client.put( "/api/admin/providers/generate", headers=headers, json={ "name": "openrouter", "url": "https://openrouter.ai/api/v1/chat/completions", "model": "openai/gpt-4o", "zdr": True, "no_train": True, }, ) expect(saved.status_code == 200, f"save generate settings {saved.text}") saved_roles = {item["role"]: item for item in saved.json()["roles"]} expect(saved_roles["generate"]["model"] == "openai/gpt-4o", "generate model persisted") unknown = client.put( "/api/admin/providers/training", headers=headers, json={"url": "https://example.test", "model": "x"}, ) expect(unknown.status_code == 404, "unknown provider role") os.environ["KANSHO_PROVIDER_KEY"] = "sk-test-not-a-real-key" try: from providers import generate_provider, detect_provider expect(generate_provider() is not None, "generate ready after key") expect(detect_provider() is not None, "detect reuses generate key") ready = client.get("/api/admin/providers", headers=headers) expect("sk-test-not-a-real-key" not in ready.text, "secret never in status") expect(ready.json()["roles"][0]["ready"] is True, "status ready after key") finally: os.environ.pop("KANSHO_PROVIDER_KEY", None) missing = client.post( "/api/prompts/execute", headers=headers, json={"prompt_id": "demo", "data_class": "B"}, ) expect(missing.status_code == 404, "execute requires a stored prompt") created = client.post( "/api/prompts", headers=headers, json={ "slug": "frame-echo", "name": "Frame Echo", "template": "Zeit {{now}} für [[SELF]] am [[PLACE:HOME]].", }, ) expect(created.status_code == 200, f"create prompt {created.text}") prompt_id = created.json()["id"] bad_key = client.post( "/api/prompts", headers=headers, json={"slug": "bad", "name": "Bad", "template": "Hallo {{journal_summary}}"}, ) expect(bad_key.status_code == 400, "unknown context key rejected") expect(bad_key.json()["detail"]["code"] == "unknown_placeholder", "unknown key code") bad_privacy = client.post( "/api/prompts", headers=headers, json={"slug": "bad-privacy", "name": "Bad privacy", "template": "[[INDIAN_WIFE]]"}, ) expect(bad_privacy.status_code == 400, "speaking privacy token rejected") preview = client.post("/api/prompts/preview", headers=headers, json={"prompt_id": prompt_id}) expect(preview.status_code == 200, f"preview {preview.text}") expect("{{now}}" not in preview.json()["rendered"], "context key resolved") expect("[[SELF]]" in preview.json()["rendered"], "privacy token left for gateway") expect(preview.json()["llm"] is False, "preview does not call LLM") blocked = client.post( "/api/prompts/execute", headers=headers, json={"prompt_id": prompt_id, "data_class": "B"}, ) expect(blocked.status_code == 503, f"gateway fail-closed {blocked.text}") expect(blocked.json()["detail"]["code"] == "no_egress_provider_configured", "gateway reason") class_a = client.post( "/api/prompts/execute", headers=headers, json={"prompt_id": prompt_id, "data_class": "A"}, ) expect(class_a.status_code == 503, "class A never leaves") expect(class_a.json()["detail"]["code"] == "class_a_never_leaves_local_zone", "class A reason") created_user = client.post( "/api/users", headers=headers, json={"email": "user@example.test", "name": "Ute", "password": "user-pass", "role": "user"}, ) expect(created_user.status_code == 200, f"admin creates user {created_user.text}") expect(created_user.json()["role"] == "user", "created role is user") users = client.get("/api/users", headers=headers) expect(len(users.json()) == 2, "two profiles") user_login = client.post( "/api/auth/login", json={"email": "user@example.test", "password": "user-pass"}, ) expect(user_login.status_code == 200, "user login") user_headers = auth_header(user_login.json()["token"]) forbidden_users = client.get("/api/users", headers=user_headers) expect(forbidden_users.status_code == 403, "user cannot list users") sub = client.get("/api/subscription", headers=user_headers) expect(sub.status_code == 200, "subscription payload") expect(sub.json()["tier"]["id"] == "local", "local tier") expect(sub.json()["billing"] is None, "no billing provider") expect(any(item["feature_id"] == "ai_calls" and item["allowed"] for item in sub.json()["features"]), "ai_calls allowed") placeholders = client.get("/api/placeholders", headers=user_headers) expect(placeholders.status_code == 200, "placeholder catalog") keys = {item["key"] for item in placeholders.json()["context"]} expect( keys == { "now", "dialogue_context", "existing_text", "known_labels", "register_hint", "review_package", "source_text", "space_title", "writing_profile", "interaction_hint", }, "system and mvp context keys", ) last_admin = client.patch( f"/api/users/{me.json()['id']}", headers=headers, json={"role": "user"}, ) expect(last_admin.status_code == 400, "last admin cannot be demoted") disabled = client.patch( f"/api/users/{created_user.json()['id']}", headers=headers, json={"status": "disabled"}, ) expect(disabled.status_code == 200, "disable user") disabled_login = client.post( "/api/auth/login", json={"email": "user@example.test", "password": "user-pass"}, ) expect(disabled_login.status_code == 401, "disabled user cannot login") bad_login = client.post( "/api/auth/login", json={"email": "lars@example.test", "password": "wrong"}, ) expect(bad_login.status_code == 401, "bad password") print("All frame tests passed.") if __name__ == "__main__": main()