( mcpUrl: string, bearer: string, label: string, recordSession: (sessionId: string) => void, )
| 130 | * orphaning a real session on the target isolate. |
| 131 | */ |
| 132 | const openSession = async ( |
| 133 | mcpUrl: string, |
| 134 | bearer: string, |
| 135 | label: string, |
| 136 | recordSession: (sessionId: string) => void, |
| 137 | ): Promise<string> => { |
| 138 | // The platform can reset a session Durable Object while its initialize |
| 139 | // is in flight, and the server answers that with the documented restart |
| 140 | // envelope (503, -32001, "MCP session is restarting, please retry") — the |
| 141 | // same contract a streamable-http client follows: same request, after the |
| 142 | // advertised delay. Treat it as transient here instead of failing the |
| 143 | // scenario on a retryable platform blip. |
| 144 | const RESTART_ATTEMPTS = 8; |
| 145 | const RESTART_DELAY_MS = 2_000; // The host advertises Retry-After: 2. |
| 146 | let minted: { readonly response: Response; readonly sessionId: string } | undefined; |
| 147 | for (let attempt = 0; attempt < RESTART_ATTEMPTS; attempt += 1) { |
| 148 | const response = await postJson(mcpUrl, bearer, { |
| 149 | jsonrpc: "2.0" as const, |
| 150 | id: "initialize", |
| 151 | method: "initialize", |
| 152 | params: { |
| 153 | protocolVersion: PROTOCOL_VERSION, |
| 154 | capabilities: {}, |
| 155 | clientInfo: { name: `executor-e2e-cap-eviction-${label}`, version: "0.0.1" }, |
| 156 | }, |
| 157 | }); |
| 158 | const candidate = response.headers.get("mcp-session-id"); |
| 159 | if (candidate !== null && candidate.length > 0) { |
| 160 | minted = { response, sessionId: candidate }; |
| 161 | break; |
| 162 | } |
| 163 | const body = await response.text(); |
| 164 | const isRestart = isRestartResponse(response.status, body); |
| 165 | if (!isRestart) break; |
| 166 | if (attempt === RESTART_ATTEMPTS - 1) break; |
| 167 | await new Promise((resolve) => setTimeout(resolve, RESTART_DELAY_MS)); |
| 168 | } |
| 169 | if (!minted) { |
| 170 | // oxlint-disable-next-line executor/no-error-constructor -- boundary: e2e setup precondition. |
| 171 | throw new Error(`openSession (${label}): no mcp-session-id header`); |
| 172 | } |
| 173 | const { response: initialized, sessionId } = minted; |
| 174 | // Recorded the moment the id exists — BEFORE the body read and status |
| 175 | // assertion below, either of which can throw with the session already live |
| 176 | // on the server. The cleanup finalizer needs the id on every one of those |
| 177 | // paths, not just a fully successful return. |
| 178 | recordSession(sessionId); |
| 179 | await initialized.text(); |
| 180 | expect(initialized.status, `initialize (${label}) opens a session`).toBe(200); |
| 181 | const notification = await postJson( |
| 182 | mcpUrl, |
| 183 | bearer, |
| 184 | { jsonrpc: "2.0" as const, method: "notifications/initialized" }, |
| 185 | sessionId, |
| 186 | ); |
| 187 | await notification.text(); |
| 188 | expect(notification.status, `(${label}) completes the handshake`).toBe(202); |
| 189 | return sessionId; |
no test coverage detected