""" verify_supabase.py — one-command check that your real Supabase project is wired up correctly for MyeAI. Run it locally (or in the Space's terminal) after creating the tables: export SUPABASE_URL="https://YOUR-PROJECT-REF.supabase.co" export SUPABASE_KEY="YOUR-SERVICE-ROLE-KEY" python3 verify_supabase.py It writes a throwaway row for verify+@myeai.invalid, reads it back, checks that a returning participant would be restored correctly, then deletes everything it created. It touches no real participant data. """ import os import sys import time from supabase_store import (SupabaseStore, is_valid_email, normalize_email, is_valid_user_id, user_ids_match, CONFLICT) PASS, FAIL = " PASS ", " FAIL " failures = [] def check(name, ok, detail=""): print((PASS if ok else FAIL) + name + (f" [{detail}]" if detail and not ok else "")) if not ok: failures.append(name) return ok def main(): store = SupabaseStore() print("\n1. Configuration") if not check("SUPABASE_URL and SUPABASE_KEY are set", store.enabled, store.config_hint()): print("\nSet them first:\n" ' export SUPABASE_URL="https://YOUR-PROJECT-REF.supabase.co"\n' ' export SUPABASE_KEY="YOUR-SERVICE-ROLE-KEY"') return 2 print(f" url = {store.url}") print(f" key = {store.key[:12]}...{store.key[-6:]} ({len(store.key)} chars)") print(f" tables = {store.sessions_table}, {store.messages_table}") print("\n2. Connectivity and schema") ok, err = store.ping() if not check(f"can read table '{store.sessions_table}'", ok, str(err)): print("\n -> If this says 'relation does not exist', run supabase_schema.sql\n" " in Supabase Studio > SQL Editor.\n" " -> If this says 'permission denied' or 'JWT', you are probably using\n" " the anon key. Use the service_role key (Project Settings > API).") return 1 msgs, err = store.load_messages("nobody@myeai.invalid", limit=1) check(f"can read table '{store.messages_table}'", err is None, str(err)) email = normalize_email(f"verify+{int(time.time())}@myeai.invalid") print(f"\n3. Round-trip with a throwaway row ({email})") check("email validator accepts it", is_valid_email(email)) row, err = store.load_session(email) check("unseen email reads back as new", row is None and err is None, str(err)) row, err = store.ensure_session(email, user_id="P3-VERIFY") check("row can be created", row is not None, str(err)) check("User ID validator accepts the study ID", is_valid_user_id("P3-VERIFY")) check("User ID stored with the session", (row or {}).get("user_id") == "P3-VERIFY", str((row or {}).get("user_id"))) partial = ["Yes", "No", "Yes"] + [None] * 13 ok, err = store.save_answers_monotonic(email, partial) check("a half-finished questionnaire can be auto-saved", ok, str(err)) answers = ["Yes", "No"] * 8 convo = [{"role": "user", "content": "seed profile"}, {"role": "assistant", "content": "Thanks, I have your profile."}] display = [{"role": "assistant", "content": "Thanks, I have your profile."}] ok, rev, err = store.save_session(email, answers=answers, convo=convo, chat_display=display, followups=0, expected_revision=0) check("answers + chat can be saved", ok, str(err)) check("revision advanced to 1", rev == 1, str(rev)) ok, err = store.save_answers_monotonic(email, partial) row, _ = store.load_session(email) check("a stale auto-save cannot undo a completed intake", (row or {}).get("answers") == answers, str((row or {}).get("answers"))[:60]) convo = convo + [{"role": "user", "content": "What is CAR T?"}, {"role": "assistant", "content": "Per the documents..."}] display = display + [{"role": "user", "content": "What is CAR T?"}, {"role": "assistant", "content": "Per the documents..."}] ok, rev, err = store.save_session(email, convo=convo, chat_display=display, followups=1, expected_revision=rev) check("a chat turn can be appended", ok, str(err)) ok, stale_rev, err = store.save_session(email, followups=99, expected_revision=0) check("a stale second tab is refused, not silently applied", ok is False and err == CONFLICT, f"{ok} {err}") ok, err = store.append_messages(email, [ {"role": "user", "content": "What is CAR T?", "turn": 1}, {"role": "assistant", "content": "Per the documents...", "turn": 1}, ]) check("turns can be written to the message log", ok, str(err)) print("\n4. What a returning participant would get back") row, err = store.load_session(email) if check("row reads back", row is not None, str(err)): check("all 16 answers restored", row.get("answers") == answers, str(row.get("answers"))[:70]) check("full chat restored", row.get("chat_display") == display, f"{len(row.get('chat_display') or [])} messages") check("model conversation restored", row.get("convo") == convo, f"{len(row.get('convo') or [])} turns") check("follow-up counter restored", row.get("followups") == 1, str(row.get("followups"))) check("mid-chat save did not clobber answers", row.get("answers") == answers) check("revision column present (run the latest supabase_schema.sql)", "revision" in row, str(sorted(row))[:120]) check("answers_count column present", "answers_count" in row, str(sorted(row))[:120]) check("user_id column present", "user_id" in row, str(sorted(row))[:120]) check("User ID survives the round trip", row.get("user_id") == "P3-VERIFY", str(row.get("user_id"))) check("a returning participant's User ID would match", user_ids_match(row.get("user_id"), "p3-verify")) check("a wrong User ID would not match", not user_ids_match(row.get("user_id"), "P3-SOMEONE-ELSE")) msgs, err = store.load_messages(email) check("message log readable", err is None and len(msgs) == 2, f"{len(msgs)} rows, {err}") print("\n5. Cleanup") ok, err = store.delete_session(email) check("throwaway session deleted", ok, str(err)) ok, err = store.delete_messages(email) check("throwaway log rows deleted", ok, str(err)) row, _ = store.load_session(email) check("nothing left behind", row is None) print("\n" + "=" * 60) if failures: print(f"{len(failures)} check(s) FAILED:") for f in failures: print(" - " + f) return 1 print("All checks passed — Supabase is correctly wired up for MyeAI.") return 0 if __name__ == "__main__": sys.exit(main())