148 lines · 5.9 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Regression coverage for the transient DO-relocation retry in the worker-side// session-store dispatcher.//// Cloudflare may relocate a live Durable Object between machines; an in-flight// `init` then throws "cannot access storage because object has moved to a// different machine". Before the retry, that rejection became an unrecoverable// defect the envelope rendered as a -32603 — exactly the prod incident on// 2026-06-15 (one `mcp.do.init` failure a reconnect cleared). These tests inject// that rejection at the `McpSessionDOStub` seam and pin the recovery behavior.// --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest";import { Cause, Effect, Exit } from "effect"; import { McpSessionStore, type McpDispatchResult, type Principal } from "@executor-js/host-mcp"; import {  DO_RELOCATION_MAX_RETRIES,  makeDurableObjectMcpSessionStore,  type McpSessionDOStub,} from "./session-store"; const RELOCATION_ERROR = "cannot access storage because object has moved to a different machine"; const TEST_PRINCIPAL: Principal = {  accountId: "user_test",  organizationId: "org_test",  organizationName: "Test Org",  email: "test@example.com",  name: "Test",  avatarUrl: null,  roles: ["user"],}; /** A POST with no session id — routes dispatch through the create/init path. */const initializeRequest = (): Request =>  new Request("https://mcp.test/mcp", {    method: "POST",    headers: { authorization: "Bearer x", "content-type": "application/json" },    body: JSON.stringify({ jsonrpc: "2.0", id: 0, method: "initialize" }),  }); /** The JSON-RPC body the faked DO returns once `init` succeeds. */const okResponse = (): Response =>  new Response(JSON.stringify({ jsonrpc: "2.0", id: 0, result: {} }), {    status: 200,    headers: { "content-type": "application/json" },  }); /** Drive `dispatch` for a create over a store built from one fake stub. */const dispatchCreate = (stub: McpSessionDOStub): Effect.Effect<McpDispatchResult> =>  Effect.gen(function* () {    const store = yield* McpSessionStore;    return yield* store.dispatch({      request: initializeRequest(),      principal: TEST_PRINCIPAL,      sessionId: null,      method: "POST",    });  }).pipe(    Effect.provide(makeDurableObjectMcpSessionStore({ newStub: () => stub, getStub: () => stub })),  ); /** Rendered cause of a failed dispatch (empty for a success) — keeps the *  message assertion unconditional, off the `Exit.isFailure` branch. */const failureText = (exit: Exit.Exit<McpDispatchResult>): string =>  Exit.isFailure(exit) ? Cause.pretty(exit.cause) : ""; describe("makeDurableObjectMcpSessionStore — DO-relocation retry", () => {  it.live("retries mcp.do.init past a relocation, then returns the DO response", () =>    Effect.gen(function* () {      let initCalls = 0;      let handleCalls = 0;      const stub: McpSessionDOStub = {        init: () => {          initCalls += 1;          if (initCalls === 1) {            // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: fake DO stub rejects to simulate a Cloudflare relocation throw            return Promise.reject(new Error(RELOCATION_ERROR));          }          return Promise.resolve();        },        handleRequest: () => {          handleCalls += 1;          return Promise.resolve(okResponse());        },        clearSession: () => Promise.resolve(),      };       const result = yield* dispatchCreate(stub);       expect(initCalls, "init retried once after the relocation").toBe(2);      expect(handleCalls, "handleRequest runs once, after init recovers").toBe(1);      expect(result).toBeInstanceOf(Response);      expect((result as Response).status).toBe(200);    }),  );   it.live("gives up after the retry budget when relocation never clears", () =>    Effect.gen(function* () {      let initCalls = 0;      let handleCalls = 0;      const stub: McpSessionDOStub = {        init: () => {          initCalls += 1;          // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: fake DO stub rejects to simulate a Cloudflare relocation throw          return Promise.reject(new Error(RELOCATION_ERROR));        },        handleRequest: () => {          handleCalls += 1;          return Promise.resolve(okResponse());        },        clearSession: () => Promise.resolve(),      };       const exit = yield* Effect.exit(dispatchCreate(stub));       expect(Exit.isFailure(exit), "exhausted retries surface as a defect").toBe(true);      expect(initCalls, "one initial attempt plus the full retry budget").toBe(        1 + DO_RELOCATION_MAX_RETRIES,      );      expect(handleCalls, "handleRequest is never reached when init never succeeds").toBe(0);      expect(failureText(exit), "the relocation cause is preserved").toContain(RELOCATION_ERROR);    }),  );   it.live("does not retry a non-relocation failure — surfaces it immediately", () =>    Effect.gen(function* () {      let initCalls = 0;      const stub: McpSessionDOStub = {        init: () => {          initCalls += 1;          // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: fake DO stub rejects to simulate a non-transient init failure          return Promise.reject(new Error("schema spread bug — not transient"));        },        handleRequest: () => Promise.resolve(okResponse()),        clearSession: () => Promise.resolve(),      };       const exit = yield* Effect.exit(dispatchCreate(stub));       expect(Exit.isFailure(exit), "a non-transient rejection still dies").toBe(true);      expect(initCalls, "non-relocation errors are not retried").toBe(1);      expect(failureText(exit), "the original failure is preserved").toContain("schema spread bug");    }),  );});