|
| 1 | +# Brain Smoke Test |
| 2 | + |
| 3 | +> One-shot harness that probes every live surface of an Open Brain install and reports which ones are healthy, skipped (optional feature not installed), or broken. |
| 4 | +
|
| 5 | +## What It Does |
| 6 | + |
| 7 | +Runs ~30 independent checks across seven categories against your deployed Open Brain and prints a pass/skip/fail dashboard. Optional features (REST API, ob-graph, enhanced-thoughts, smart-ingest) are detected automatically and skipped with a clear reason rather than failing the run, so the same script works on stock core installs and fully-loaded instances. |
| 8 | + |
| 9 | +## Why Use This |
| 10 | + |
| 11 | +Open Brain is a lot of moving parts -- a database, an Edge Function, a secret access key, RLS policies, and optionally more tables and endpoints from recipes and integrations. When something is wrong it is usually one specific thing: a missing `GRANT`, a mismatched access key, a forgotten column, a function that failed to deploy. This harness catches those misconfigurations before you waste an hour wondering why Claude Desktop sees no tools or why semantic search returns nothing. |
| 12 | + |
| 13 | +Run it: |
| 14 | + |
| 15 | +- After initial setup to confirm every surface is wired correctly |
| 16 | +- After adding an extension or recipe that touches the schema |
| 17 | +- Before reporting a bug -- the output tells a maintainer exactly which surface is broken |
| 18 | +- In CI to guard a shared instance from regressing |
| 19 | + |
| 20 | +## Categories Checked |
| 21 | + |
| 22 | +1. **MCP Server** -- The `open-brain-mcp` Edge Function responds, exposes the four canonical tools (`search_thoughts`, `list_thoughts`, `thought_stats`, `capture_thought`), and completes a JSON-RPC `initialize` handshake. |
| 23 | +2. **REST API** -- If you have installed the optional `rest-api` integration or set `REST_API_BASE`, the gateway answers `/health`, `/thoughts`, `/search`, and `/stats`. The `NEXT_PUBLIC_API_URL` env var (the base URL the dashboard is pointed at) is also probed and only passes on a 2xx response. Skipped otherwise. |
| 24 | +3. **DB Schema** -- The canonical `public.thoughts` table exists with `id, content, embedding, metadata, created_at, updated_at`, the dedup fingerprint column is present, and `match_thoughts` + `upsert_thought` RPCs are callable. Optional tables (`graph_nodes`, `graph_edges`, `ingestion_jobs`) and the `search_thoughts_text` RPC are detected and skipped when absent. |
| 25 | +4. **Auth** -- `MCP_ACCESS_KEY` is enforced: requests with no key, with a wrong key, with the header (`x-brain-key`), and with the query string (`?key=`) all produce the expected outcome. |
| 26 | +5. **Core Features** -- **Destructive, opt-in via `--destructive`.** End-to-end capture + search + cleanup. Inserts a uniquely-tagged row via REST (triggers embedding + LLM metadata generation), fetches it back, calls MCP's `capture_thought` and `search_thoughts`, then deletes by tag. Skipped by default so CI can run this harness against shared/prod instances without mutating data or spending external model credits. |
| 27 | +6. **Access Key Enforcement** -- The Supabase PostgREST gateway rejects requests with no `apikey` and with an invalid `apikey`. This runs **before** RLS, so these checks alone do not prove RLS is configured -- see category 7. |
| 28 | +7. **Row-Level Security** -- Actually probes whether RLS is on and policies are restrictive. Tries an optional helper RPC (`pg_class_rls`) to read `pg_catalog.pg_class.relrowsecurity`. Also, if `SUPABASE_ANON_KEY` is set, does an anon-key read of `public.thoughts` and **fails loud** if rows come back (means RLS is off or a permissive `ALL USING (true)` policy is leaking data). Without `SUPABASE_ANON_KEY`, the anon probe is skipped with a clear note that RLS is unverified. |
| 29 | + |
| 30 | +## Prerequisites |
| 31 | + |
| 32 | +- Working Open Brain setup ([guide](../../docs/01-getting-started.md)) |
| 33 | +- Node.js 18 or later (uses the built-in `fetch` and `AbortController`) |
| 34 | +- A local `.env.local` file (or exported environment variables) sitting next to `smoke-all.js`. The script looks for `.env.local` in its own directory first (so `node recipes/brain-smoke-test/smoke-all.js` works from any cwd) and falls back to the current working directory. |
| 35 | + |
| 36 | +## Credential Tracker |
| 37 | + |
| 38 | +Copy this block into a text editor and fill it in. |
| 39 | + |
| 40 | +```text |
| 41 | +BRAIN SMOKE TEST -- CREDENTIAL TRACKER |
| 42 | +-------------------------------------- |
| 43 | +
|
| 44 | +FROM YOUR OPEN BRAIN SETUP |
| 45 | + Project URL: ____________ (e.g. https://abcd1234.supabase.co) |
| 46 | + Service role key: ____________ (Supabase "Secret key") |
| 47 | + MCP access key: ____________ (from Step 5 of the getting-started guide) |
| 48 | +
|
| 49 | +OPTIONAL (unlocks extra checks, safe to leave blank on stock installs) |
| 50 | + REST API base URL: ____________ (e.g. https://<ref>.supabase.co/functions/v1/open-brain-rest) |
| 51 | + Dashboard REST base URL: ____________ (the NEXT_PUBLIC_API_URL the dashboard uses; |
| 52 | + same shape as REST API base URL) |
| 53 | + Anon/publishable key: ____________ (enables a real anon-key RLS probe in the |
| 54 | + Row-Level Security category) |
| 55 | +
|
| 56 | +-------------------------------------- |
| 57 | +``` |
| 58 | + |
| 59 | +## Installation |
| 60 | + |
| 61 | +No build step. Just drop the file in and run it. |
| 62 | + |
| 63 | +1. Copy `smoke-all.js` into a local folder on your machine (any folder is fine -- it does not need to live inside your Supabase project directory). |
| 64 | + |
| 65 | +2. Create `.env.local` next to it: |
| 66 | + |
| 67 | + ```text |
| 68 | + SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co |
| 69 | + SUPABASE_SERVICE_ROLE_KEY=your-service-role-secret-key |
| 70 | + MCP_ACCESS_KEY=your-access-key-from-step-5 |
| 71 | +
|
| 72 | + # Optional -- unlocks the REST API category. Leave unset on stock installs. |
| 73 | + # REST_API_BASE=https://YOUR_PROJECT_REF.supabase.co/functions/v1/open-brain-rest |
| 74 | +
|
| 75 | + # Optional -- same base URL the dashboard uses (NEXT_PUBLIC_API_URL). |
| 76 | + # NEXT_PUBLIC_API_URL=https://YOUR_PROJECT_REF.supabase.co/functions/v1/open-brain-rest |
| 77 | +
|
| 78 | + # Optional -- enables a real RLS probe that reads public.thoughts with |
| 79 | + # the anon key and fails if rows come back. |
| 80 | + # SUPABASE_ANON_KEY=your-anon-publishable-key |
| 81 | + ``` |
| 82 | + |
| 83 | +3. Run it: |
| 84 | + |
| 85 | + ```bash |
| 86 | + node smoke-all.js |
| 87 | + ``` |
| 88 | + |
| 89 | +## Usage |
| 90 | + |
| 91 | +```bash |
| 92 | +# Pretty-printed dashboard (default). Read-only: no data is inserted or |
| 93 | +# deleted, no LLM calls are made. Core Features is SKIPPED. |
| 94 | +node smoke-all.js |
| 95 | + |
| 96 | +# Machine-readable JSON -- pipe into jq, a log aggregator, or CI assertions |
| 97 | +node smoke-all.js --json |
| 98 | + |
| 99 | +# Opt in to the destructive Core Features category. Inserts a uniquely- |
| 100 | +# tagged row via the service-role key, triggers embedding + LLM metadata |
| 101 | +# generation, then deletes by tag. Cleanup runs on normal exit, on a |
| 102 | +# thrown check, and on SIGINT/SIGTERM, so ctrl-c does not leave residue. |
| 103 | +# Use this against a dev/scratch project, NOT a shared/prod instance. |
| 104 | +node smoke-all.js --destructive |
| 105 | +node smoke-all.js --write # alias |
| 106 | + |
| 107 | +# Run only one category (names are case-insensitive) |
| 108 | +node smoke-all.js --category="DB Schema" |
| 109 | +node smoke-all.js --category=Auth |
| 110 | +node smoke-all.js --category="Core Features" --destructive |
| 111 | +node smoke-all.js --category="Row-Level Security" |
| 112 | + |
| 113 | +# Show this usage |
| 114 | +node smoke-all.js --help |
| 115 | +``` |
| 116 | + |
| 117 | +The seven category names are `MCP Server`, `REST API`, `DB Schema`, `Auth`, `Core Features`, `Access Key Enforcement`, and `Row-Level Security`. |
| 118 | + |
| 119 | +## Security Note -- `?key=` Access-Key Logging |
| 120 | + |
| 121 | +One Auth category check (`MCP accepts correct access key (?key=)`) verifies the URL-query-string auth path that OB1 supports for clients that cannot send custom headers (documented in [docs/01-getting-started.md](../../docs/01-getting-started.md) Step 5). **This check puts `MCP_ACCESS_KEY` into the URL**, which means the key ends up in: |
| 122 | + |
| 123 | +- Supabase's function-invocation logs (visible in the Studio UI to anyone with dashboard access) |
| 124 | +- any corporate HTTPS-inspection proxy that logs URLs (common on enterprise networks) |
| 125 | +- shell history if you pipe the output through `tee` or redirect stderr to a file |
| 126 | +- CI run logs if a future Node or `fetch` implementation verbose-logs request URLs |
| 127 | + |
| 128 | +The header-based auth check (`x-brain-key`) does **not** have this problem and is always preferred. |
| 129 | + |
| 130 | +If you run this harness from a network with HTTPS proxying, or ship the output anywhere public, **rotate `MCP_ACCESS_KEY`** afterward (Step 5 of the getting-started guide). To skip the `?key=` check entirely on sensitive networks, run `node smoke-all.js --category=MCP\ Server` and the other categories except `Auth` -- you lose a small amount of coverage but the key never touches a URL. |
| 131 | + |
| 132 | +## Example Output |
| 133 | + |
| 134 | +The harness prints one row per check, grouped by category, then a summary line. Row glyphs: |
| 135 | + |
| 136 | +- `✓` **pass** -- the surface is wired correctly. Detail column shows what the check saw (`HTTP 204`, `rows=1247`, `callable`, etc.). |
| 137 | +- `⚠` **skip** -- an optional dependency is not installed, or a prerequisite env var is unset. Detail column explains why (e.g. `REST API not installed`, `SUPABASE_ANON_KEY unset -- RLS not verified end-to-end`). Skipped checks never fail the run. |
| 138 | +- `✗` **fail** -- the surface exists but answered wrong. Detail column shows the error (`HTTP 500`, `content mismatch`, `RLS is OFF or a permissive ALL USING (true) policy exists`). |
| 139 | + |
| 140 | +Each row ends with the elapsed milliseconds for that check so a slow endpoint is visible at a glance. |
| 141 | + |
| 142 | +The summary line has the form `Summary: <N> pass, <N> skip, <N> fail (<total> total)` where `<total>` matches the number of rows printed above it, followed by `Result: OK` (exit 0) or `Result: FAIL` (exit 1). |
| 143 | + |
| 144 | +A representative shape on a healthy stock install, run without `--destructive` (no REST API, no ob-graph, no smart-ingest, no enhanced-thoughts, no `SUPABASE_ANON_KEY`): |
| 145 | + |
| 146 | +```text |
| 147 | +Open Brain Smoke Test -- 28 checks across 7 categories |
| 148 | +Target: https://abcd1234.supabase.co |
| 149 | +
|
| 150 | +MCP Server: |
| 151 | + ✓ open-brain-mcp endpoint responds 124ms -- HTTP 204 |
| 152 | + ✓ MCP tools/list returns core tools 312ms -- tools=4 (search_thoughts, list_thoughts, thought_stats, capture_thought) |
| 153 | + ✓ MCP initialize handshake 289ms -- server=open-brain |
| 154 | +
|
| 155 | +REST API: |
| 156 | + ⚠ GET /health 92ms -- REST API not installed |
| 157 | + ⚠ GET /thoughts?limit=3 88ms -- REST API not installed |
| 158 | + ⚠ POST /search (text) 91ms -- REST API not installed |
| 159 | + ⚠ GET /stats 87ms -- REST API not installed |
| 160 | + ⚠ REST API base URL (NEXT_PUBLIC_API_URL) responds 2xx 1ms -- NEXT_PUBLIC_API_URL unset |
| 161 | +
|
| 162 | +DB Schema: |
| 163 | + ✓ thoughts table present 178ms -- rows=1247 |
| 164 | + ✓ thoughts has canonical columns 142ms -- id, content, embedding, metadata, created_at, updated_at |
| 165 | + ✓ content_fingerprint column (dedup) 138ms -- present |
| 166 | + ✓ match_thoughts RPC 301ms -- callable |
| 167 | + ✓ upsert_thought RPC 198ms -- callable |
| 168 | + ✓ thoughts recently written (last 7d) 165ms -- rows_7d=84 |
| 169 | + ⚠ graph_nodes table (optional recipe: ob-graph) 142ms -- graph_nodes table not installed |
| 170 | + ⚠ graph_edges table (optional recipe: ob-graph) 141ms -- graph_edges table not installed |
| 171 | + ⚠ ingestion_jobs table (optional integration: smart-ingest) 139ms -- ingestion_jobs table (requires schemas/smart-ingest-tables, not yet on main) not installed |
| 172 | + ⚠ search_thoughts_text RPC (optional schema: enhanced-thoughts) 138ms -- enhanced-thoughts not installed |
| 173 | +
|
| 174 | +Auth: |
| 175 | + ✓ MCP rejects missing access key 96ms -- HTTP 401 (rejected) |
| 176 | + ✓ MCP rejects wrong access key 98ms -- HTTP 401 (rejected) |
| 177 | + ✓ MCP accepts correct access key (header) 197ms -- HTTP 200 |
| 178 | + ✓ MCP accepts correct access key (?key=) 201ms -- HTTP 200 |
| 179 | +
|
| 180 | +Core Features: |
| 181 | + ⚠ Core Features (destructive) 0ms -- pass --destructive to exercise capture + search + cleanup (writes rows, spends LLM credits) |
| 182 | +
|
| 183 | +Access Key Enforcement: |
| 184 | + ✓ PostgREST rejects missing apikey 98ms -- HTTP 401 (rejected before RLS) |
| 185 | + ✓ PostgREST rejects invalid apikey 102ms -- HTTP 401 (rejected before RLS) |
| 186 | + ✓ Service role can read thoughts 154ms -- rows=1247 |
| 187 | +
|
| 188 | +Row-Level Security: |
| 189 | + ⚠ pg_class.relrowsecurity = true for public.thoughts 94ms -- pg_class_rls helper RPC not installed (rely on anon probe) |
| 190 | + ⚠ Anon key cannot read thoughts (real RLS probe) 1ms -- SUPABASE_ANON_KEY unset -- RLS not verified end-to-end |
| 191 | +
|
| 192 | +Summary: 16 pass, 12 skip, 0 fail (28 total) |
| 193 | +Result: OK |
| 194 | +``` |
| 195 | + |
| 196 | +Row counts by category on a stock install without `--destructive`: MCP Server=3, REST API=5, DB Schema=10, Auth=4, Core Features=1 (synthetic skip), Access Key Enforcement=3, Row-Level Security=2 -- **28 total**. Pass/skip split depends on which optional recipes are installed; `16 pass + 12 skip + 0 fail` is the stock-install baseline above. |
| 197 | + |
| 198 | +With `--destructive` the Core Features skip row is replaced by 5 real checks (insert, retrieve, MCP capture, MCP search, cleanup), so the total grows to 32. Installing ob-graph, smart-ingest (once its schema lands), enhanced-thoughts, REST API, and setting `SUPABASE_ANON_KEY` converts skips into passes without changing the row count. |
| 199 | + |
| 200 | +## Exit Codes |
| 201 | + |
| 202 | +- `0` -- all checks passed, or passed-or-skipped |
| 203 | +- `1` -- at least one check failed |
| 204 | +- `2` -- setup error (missing required env var, unknown `--category`) |
| 205 | + |
| 206 | +Skipped checks never fail the run, so you can wire this into CI without it going red every time an optional recipe is absent. **Warning:** a `⚠` on the anon-key RLS probe means RLS was not actually verified -- set `SUPABASE_ANON_KEY` before trusting the run as a safety-rail gate. |
| 207 | + |
| 208 | +## Extending |
| 209 | + |
| 210 | +Each category is a plain array of `{ name, fn }` entries. To add a check: |
| 211 | + |
| 212 | +1. Pick the category array (`mcpChecks`, `restChecks`, `dbChecks`, `authChecks`, `coreChecks`, `accessKeyChecks`, or `rlsChecks`) inside `smoke-all.js`. |
| 213 | +2. Append an entry: |
| 214 | + |
| 215 | + ```js |
| 216 | + { |
| 217 | + name: "My new check", |
| 218 | + fn: async (signal) => { |
| 219 | + const res = await fetch(`${REST_BASE}/my_table?select=id&limit=1`, { |
| 220 | + headers: SVC_HEADERS, signal, |
| 221 | + }); |
| 222 | + if (!res.ok) throw new Error(`HTTP ${res.status}`); |
| 223 | + return "ok"; |
| 224 | + }, |
| 225 | + }, |
| 226 | + ``` |
| 227 | + |
| 228 | +3. For optional features, throw `SkipError` when the dependency is absent rather than letting the check fail: |
| 229 | + |
| 230 | + ```js |
| 231 | + if (res.status === 404) throw new SkipError("my-feature not installed"); |
| 232 | + ``` |
| 233 | + |
| 234 | +Each `fn` gets an `AbortSignal` that fires at 10 seconds by default. Return a short string to show in the dashboard, or throw any other error to fail the check. |
| 235 | + |
| 236 | +## Troubleshooting |
| 237 | + |
| 238 | +**Issue: `ERROR: missing required env var(s): SUPABASE_URL, ...`** |
| 239 | +Solution: Create `.env.local` in the current directory with `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, and `MCP_ACCESS_KEY`, or export them into your shell. The script refuses to start without all three. |
| 240 | + |
| 241 | +**Issue: `Auth: ✗ MCP accepts correct access key` fails with HTTP 401** |
| 242 | +Solution: The `MCP_ACCESS_KEY` in `.env.local` does not match what Supabase has stored. Re-run `supabase secrets set MCP_ACCESS_KEY=<your-key>` and confirm the key in your credential tracker is identical. |
| 243 | + |
| 244 | +**Issue: `DB Schema: ✗ thoughts has canonical columns` fails with HTTP 400** |
| 245 | +Solution: Your `public.thoughts` table is missing one of the canonical columns (most commonly `embedding`). Re-run the SQL in [Step 2.2 of the getting-started guide](../../docs/01-getting-started.md). Additive migrations are safe -- the script only reads, it does not drop anything. |
| 246 | + |
| 247 | +**Issue: `MCP search_thoughts finds test row` fails even though capture succeeded** |
| 248 | +Solution: Embedding generation is asynchronous in some setups and may not land before search runs. The check already retries once with a 1.5 s delay; if it still fails, check the Edge Function logs in the Supabase dashboard for OpenRouter errors (missing or rate-limited key). |
0 commit comments