|
| 1 | +# Backend Connection Resilience — Manual Test Playbook |
| 2 | + |
| 3 | +**For:** any agent (or human) verifying `c09e569` / `root-resolution` |
| 4 | +**Last Updated:** Aug 20, 2026 |
| 5 | +**Implements:** [`backend-connection-resilience.md`](./backend-connection-resilience.md) |
| 6 | +**Do not implement code.** Execute the cases below, record pass/fail, stop. |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## Answer first: new sessions in different roots? |
| 11 | + |
| 12 | +**No. Do not open a new Cursor chat, do not switch workspace roots, do not call `mcpmux_set_workspace_root`.** |
| 13 | + |
| 14 | +That was the old workaround. It opens a fresh *inbound* rmcp session and hides the outbound-pool bug. Using it here makes Case B/C inconclusive. |
| 15 | + |
| 16 | +| Action | Use for this test? | |
| 17 | +| --- | --- | |
| 18 | +| Same Cursor chat, same workspace (`/Users/joe/Desktop/Repos/Personal/mcp-mux`) | **Yes — required** | |
| 19 | +| Reload Cursor MCP tools (once, before starting, if the gateway just rebuilt) | Yes, once | |
| 20 | +| New chat / different root / `set_workspace_root` | **No** (confounds reconnect) | |
| 21 | +| Bind a real FeatureSet onto another repo | **No** (Case A uses a fake UUID; no write) | |
| 22 | + |
| 23 | +Bindings are keyed by exact `workspace_root` + `machine_id`, not by chat. This repo already has two rows for the same path (Gondor vs Rohan), both on FeatureSet `All`. Stay here. |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Snapshot (live DB, Aug 20 2026) |
| 28 | + |
| 29 | +``` |
| 30 | +DB=~/Library/Application\ Support/com.mcpmux.desktop/mcpmux.db |
| 31 | +LOG=~/Library/Application\ Support/com.mcpmux.desktop/logs/mcpmux.2026-08-20.log |
| 32 | +``` |
| 33 | + |
| 34 | +| Fact | Value | |
| 35 | +| --- | --- | |
| 36 | +| Space | `00000000-0000-0000-0000-000000000001` | |
| 37 | +| This root | `/Users/joe/Desktop/Repos/Personal/mcp-mux` | |
| 38 | +| Binding (Gondor `ec211deb…`) | `5a588b93-5ed1-4ada-b7cb-8e32a9f11058` → FeatureSet `All` (`fs_default_00000000-0000-0000-0000-000000000001`) | |
| 39 | +| Binding (Rohan `5d581ac9…`) | `8e2b36b6-eeff-4818-9259-948c1b9c3b6b` → `All` | |
| 40 | +| HA backend | `home-assistant-new` (HTTP, enabled, 95 features) | |
| 41 | +| Binding row count | 38 parents / 74 junction rows (re-count before Case A) | |
| 42 | +| Pool stats | **in-memory only** — `consecutive_failures` will not appear in SQLite | |
| 43 | + |
| 44 | +Re-count before Case A (numbers drift): |
| 45 | + |
| 46 | +```bash |
| 47 | +sqlite3 "$HOME/Library/Application Support/com.mcpmux.desktop/mcpmux.db" \ |
| 48 | + "SELECT COUNT(*) FROM workspace_bindings; |
| 49 | + SELECT COUNT(*) FROM workspace_binding_feature_sets;" |
| 50 | +``` |
| 51 | + |
| 52 | +--- |
| 53 | + |
| 54 | +## Preconditions |
| 55 | + |
| 56 | +1. Debug gateway is the listener on `:45818` (ancestor = `launchd`, not Cursor Helper). Health: `curl -sf http://127.0.0.1:45818/health` → `{"status":"ok","version":"0.5.0"}`. |
| 57 | +2. Cursor MCP `user-mcpmux` points at `http://localhost:45818/mcp`. Reload tools **once** if the binary was just rebuilt, then do not reload again. |
| 58 | +3. Only call mux via `user-mcpmux` (`mcpmux_search_tools` → `mcpmux_get_tool_schema` if needed → `mcpmux_invoke_tool`). No direct backend MCP servers. |
| 59 | +4. Do not run `pnpm dev:stop` / rebuild mid-test (evicts the pool and invalidates Case B/C). |
| 60 | + |
| 61 | +--- |
| 62 | + |
| 63 | +## Case A — bind FK guard (~2 min) |
| 64 | + |
| 65 | +**Goal:** a nonexistent `feature_set_id` returns `invalid_argument`, not `FOREIGN KEY constraint failed`, and the DB does not grow. |
| 66 | + |
| 67 | +1. Snapshot counts (query above). Call them `B0` / `J0`. |
| 68 | +2. Call `mcpmux_bind_current_workspace` with `feature_set_id` = `00000000-0000-0000-0000-00000000dead` (or any other unused UUID). Do **not** approve anything — the guard runs before consent. |
| 69 | +3. **Pass** if the tool error JSON has `"error":"invalid_argument"` and the message contains `mcpmux_list_feature_sets`. |
| 70 | +4. **Fail** if the message contains `FOREIGN KEY`, `internal_error`, or `constraint`. |
| 71 | +5. Re-count. `B0` and `J0` must be unchanged. |
| 72 | +6. Grep the log: |
| 73 | + |
| 74 | +```bash |
| 75 | +rg "bind_current_workspace rejected" \ |
| 76 | + "$HOME/Library/Application Support/com.mcpmux.desktop/logs/mcpmux.$(date +%Y-%m-%d).log" |
| 77 | +``` |
| 78 | + |
| 79 | +Expect a `warn` with the fake `feature_set_id` and this Space id. |
| 80 | + |
| 81 | +--- |
| 82 | + |
| 83 | +## Case B — reconnect after a killed stdio child (~5 min) |
| 84 | + |
| 85 | +**Goal:** a transport-closed error triggers `reconnect_fresh` (not OAuth `reconnect_instance`) and the retry succeeds. Same session. |
| 86 | + |
| 87 | +Pick a **stdio** server that is already invokable under `All`. Cheap options on this machine: `wakatime`, `markitdown`, `chrome-devtools`. Confirm first: |
| 88 | + |
| 89 | +``` |
| 90 | +mcpmux_search_tools({ "server_id": "wakatime", "mode": "browse", "limit": 5 }) |
| 91 | +``` |
| 92 | + |
| 93 | +If `server_readiness` is not `ready`, pick another stdio id from that browse, or `mcpmux_list_servers` and take one with `ready`. |
| 94 | + |
| 95 | +1. Invoke a cheap read-only tool once so the instance is live. Example (only if search returned it): `mcpmux_invoke_tool` `server_id=wakatime` `tool=wakatime_wakatime_summaries` with explicit `start`/`end` dates for today. Any successful call is enough. |
| 96 | +2. Note the time (`date -u +%H:%M:%S`). |
| 97 | +3. Kill **only the child**, not McpMux: |
| 98 | + |
| 99 | +```bash |
| 100 | +# find the stdio child (example: wakatime). Do not kill target/debug/mcpmux. |
| 101 | +pgrep -lf wakatime |
| 102 | +# then: kill <child-pid> |
| 103 | +``` |
| 104 | + |
| 105 | +If you cannot identify a safe child, stop and report INCONCLUSIVE. Do not `pkill -f mcpmux`. |
| 106 | + |
| 107 | +4. In **this same chat**, invoke the same tool again. Do not call `set_workspace_root`. |
| 108 | +5. **Pass** if the invoke succeeds (or returns a normal tool error, not `-32000` / `Connection closed`). |
| 109 | +6. Grep from the timestamp in step 2: |
| 110 | + |
| 111 | +```bash |
| 112 | +LOG="$HOME/Library/Application Support/com.mcpmux.desktop/logs/mcpmux.$(date +%Y-%m-%d).log" |
| 113 | +rg "backend call_tool failed|reconnect attempted after call_tool failure|reconnect_fresh completed" "$LOG" |
| 114 | +``` |
| 115 | + |
| 116 | +**Pass logs:** |
| 117 | + |
| 118 | +- `backend call_tool failed` with `trigger=transport_closed` (or `auth` only if the error was actually 401) |
| 119 | +- `reconnect attempted after call_tool failure` with `ok=true` |
| 120 | +- `reconnect_fresh completed` with `ok=true` (this is the new path; `reconnect_instance` / `Reconnecting instance ... after OAuth` must **not** be the line for this failure) |
| 121 | + |
| 122 | +**Fail:** invoke returns `MCP error -32000: Connection closed` to the agent, or the log shows OAuth reconnect for a stdio kill. |
| 123 | + |
| 124 | +--- |
| 125 | + |
| 126 | +## Case C — original HA idle (optional, 15–20 min) |
| 127 | + |
| 128 | +**Goal:** reproduce the reported HTTP shape against `home-assistant-new`. |
| 129 | + |
| 130 | +1. Search: `mcpmux_search_tools({ "server_id": "home-assistant-new", "mode": "browse", "limit": 5 })`. |
| 131 | +2. Invoke one cheap read-only HA tool. Confirm success. |
| 132 | +3. Wait 15–20 minutes. Do not invoke that server. Do not reload MCP. Do not re-pin the workspace. Other mux tools are fine. |
| 133 | +4. Invoke the **same** HA tool again in this chat. |
| 134 | +5. **Pass / fail / log checks** are identical to Case B, except `server_id=home-assistant-new` and the error string historically was `MCP error -32000: Connection closed`. |
| 135 | + |
| 136 | +If you cannot wait, mark Case C SKIPPED and rely on B. |
| 137 | + |
| 138 | +--- |
| 139 | + |
| 140 | +## Case D — unmatched errors still surface (~2 min) |
| 141 | + |
| 142 | +**Goal:** a failure that is neither auth nor transport-closed is not swallowed. |
| 143 | + |
| 144 | +1. `mcpmux_invoke_tool` against a ready server with a tool name that does not exist, e.g. `server_id=wakatime` `tool=definitely_not_a_real_tool`. |
| 145 | +2. **Pass** if the raw error comes back to the caller (not a silent success, not a reconnect). |
| 146 | +3. If the failure is classified unmatched, the log line is `trigger=unmatched` and there is **no** `reconnect attempted after call_tool failure` for that call. |
| 147 | + |
| 148 | +A permission / not-found error that never hits the backend is also acceptable — note it as "never reached classifier" rather than fail. |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## Do not |
| 153 | + |
| 154 | +- Call `mcpmux_set_workspace_root` "to fix" a closed connection. |
| 155 | +- Open a second agent chat in another repo to "compare sessions." |
| 156 | +- `pnpm dev:stop` / rebuild / quit McpMux mid-case. |
| 157 | +- `pkill -f mcpmux`. |
| 158 | +- Write a real FeatureSet bind as part of Case A. |
| 159 | +- Dump `credentials` / OAuth tables from the DB. |
| 160 | + |
| 161 | +--- |
| 162 | + |
| 163 | +## Report back (copy this) |
| 164 | + |
| 165 | +``` |
| 166 | +Case A bind FK: PASS | FAIL | BLOCKED |
| 167 | + error code/message: |
| 168 | + binding counts before/after: |
| 169 | + log line present: yes/no |
| 170 | +
|
| 171 | +Case B stdio reconnect: PASS | FAIL | INCONCLUSIVE | SKIPPED |
| 172 | + server_id / tool: |
| 173 | + first invoke: ok/err |
| 174 | + child kill: pid / skipped why |
| 175 | + second invoke: ok / raw -32000 / other |
| 176 | + trigger= : |
| 177 | + reconnect_fresh ok= : |
| 178 | + oauth reconnect used: yes/no |
| 179 | +
|
| 180 | +Case C HA idle: PASS | FAIL | SKIPPED |
| 181 | + (same fields) |
| 182 | +
|
| 183 | +Case D unmatched: PASS | FAIL | SKIPPED |
| 184 | + error returned: |
| 185 | + trigger= : |
| 186 | +
|
| 187 | +Confounders (reload MCP / set_workspace_root / new chat / rebuild): none | list |
| 188 | +``` |
0 commit comments