1202 lines · 42.2 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Result } from "effect"; import {  ConnectionId,  CreateConnectionInput,  SecretId,  Scope,  ScopeId,  TokenMaterial,  OAUTH2_PROVIDER_KEY,  createExecutor,  definePlugin,  type SecretProvider,} from "@executor-js/sdk";import { makeTestConfig } from "@executor-js/sdk/testing"; import { mcpPlugin, userFacingProbeMessage } from "./plugin";import {  MCP_OAUTH_CLIENT_ID_SLOT,  MCP_OAUTH_CLIENT_SECRET_SLOT,  MCP_OAUTH_CONNECTION_SLOT,} from "./types";import { extractManifestFromListToolsResult, deriveMcpNamespace, joinToolPath } from "./manifest";import { makeAnnotationsMcpServer, makeGreetingMcpServer, serveMcpServer } from "../testing"; const mcpOAuth2Config = {  kind: "oauth2" as const,  securitySchemeName: "OAuth2",  flow: "authorizationCode" as const,  tokenUrl: "https://auth.example.test/token",  authorizationUrl: "https://auth.example.test/authorize",  clientIdSlot: MCP_OAUTH_CLIENT_ID_SLOT,  clientSecretSlot: MCP_OAUTH_CLIENT_SECRET_SLOT,  connectionSlot: MCP_OAUTH_CONNECTION_SLOT,  scopes: [],}; // ---------------------------------------------------------------------------// Memory secrets plugin — without a writable provider in the stack,// `executor.connections.create` has nowhere to persist its owned// access/refresh-token secret rows, so the per-user sign-in test below// can't mint a connection.// --------------------------------------------------------------------------- const makeMemorySecretsPlugin = () => {  const store = new Map<string, string>();  const provider: SecretProvider = {    key: "memory",    writable: true,    get: (id, scope) => Effect.sync(() => store.get(`${scope}${id}`) ?? null),    set: (id, value, scope) =>      Effect.sync(() => {        store.set(`${scope}${id}`, value);      }),    delete: (id, scope) => Effect.sync(() => store.delete(`${scope}${id}`)),    list: () =>      Effect.sync(() =>        Array.from(store.keys()).map((k) => {          const name = k.split("", 2)[1] ?? k;          return { id: name, name };        }),      ),  };  return definePlugin(() => ({    id: "memory-secrets" as const,    storage: () => ({}),    secretProviders: [provider],  }));}; // ---------------------------------------------------------------------------// Manifest extraction// --------------------------------------------------------------------------- describe("extractManifestFromListToolsResult", () => {  it.effect("extracts tools from a valid listTools response", () =>    Effect.sync(() => {      const result = extractManifestFromListToolsResult({        tools: [          {            name: "get_weather",            description: "Get weather for a location",            inputSchema: {              type: "object",              properties: { location: { type: "string" } },            },          },          { name: "search", description: "Search the web" },        ],      });       expect(result.tools).toHaveLength(2);      expect(result.tools[0]!.toolName).toBe("get_weather");      expect(result.tools[0]!.toolId).toBe("get_weather");      expect(result.tools[0]!.description).toBe("Get weather for a location");      expect(result.tools[1]!.toolName).toBe("search");    }),  );   it.effect("sanitizes tool IDs", () =>    Effect.sync(() => {      const result = extractManifestFromListToolsResult({        tools: [          { name: "My Tool!!", description: null },          { name: "My Tool!!", description: null },        ],      });       expect(result.tools[0]!.toolId).toBe("my_tool");      expect(result.tools[1]!.toolId).toBe("my_tool_2");    }),  );   it.effect("handles empty tools list", () =>    Effect.sync(() => {      const result = extractManifestFromListToolsResult({ tools: [] });      expect(result.tools).toHaveLength(0);    }),  );   it.effect("extracts server metadata", () =>    Effect.sync(() => {      const result = extractManifestFromListToolsResult(        { tools: [] },        { serverInfo: { name: "test-server", version: "1.0.0" } },      );      expect(result.server?.name).toBe("test-server");      expect(result.server?.version).toBe("1.0.0");    }),  );   it.effect("decodes upstream tool annotations", () =>    Effect.sync(() => {      const result = extractManifestFromListToolsResult({        tools: [          { name: "delete", annotations: { destructiveHint: true } },          { name: "list", annotations: { readOnlyHint: true } },          { name: "ping" },        ],      });       expect(result.tools[0]!.annotations?.destructiveHint).toBe(true);      expect(result.tools[1]!.annotations?.readOnlyHint).toBe(true);      expect(result.tools[2]!.annotations).toBeUndefined();    }),  );}); // ---------------------------------------------------------------------------// Namespace derivation// --------------------------------------------------------------------------- describe("deriveMcpNamespace", () => {  it.effect("derives from name", () =>    Effect.sync(() => {      expect(deriveMcpNamespace({ name: "GitHub MCP" })).toBe("github_mcp");    }),  );   it.effect("derives from endpoint", () =>    Effect.sync(() => {      expect(deriveMcpNamespace({ endpoint: "https://api.example.com/mcp" })).toBe(        "api_example_com",      );    }),  );   it.effect("derives from command", () =>    Effect.sync(() => {      expect(deriveMcpNamespace({ command: "/usr/local/bin/my-mcp-server" })).toBe("my_mcp_server");    }),  );   it.effect("falls back to 'mcp'", () =>    Effect.sync(() => {      expect(deriveMcpNamespace({})).toBe("mcp");    }),  );}); // ---------------------------------------------------------------------------// joinToolPath// --------------------------------------------------------------------------- describe("joinToolPath", () => {  it.effect("joins namespace and toolId", () =>    Effect.sync(() => {      expect(joinToolPath("github", "search")).toBe("github.search");    }),  );   it.effect("returns toolId when namespace is undefined", () =>    Effect.sync(() => {      expect(joinToolPath(undefined, "search")).toBe("search");    }),  );}); // ---------------------------------------------------------------------------// Plugin lifecycle// --------------------------------------------------------------------------- describe("mcpPlugin", () => {  it.effect("creates executor with mcp plugin", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({          plugins: [mcpPlugin()] as const,        }),      );       expect(executor.mcp).toBeDefined();      expect(executor.mcp.addSource).toBeTypeOf("function");      expect(executor.mcp.removeSource).toBeTypeOf("function");      expect(executor.mcp.refreshSource).toBeTypeOf("function");      expect(executor.mcp.probeEndpoint).toBeTypeOf("function");      expect(executor.mcp.getSource).toBeTypeOf("function");      expect((yield* executor.tools.list()).map((tool) => tool.id)).toContain(        "executor.mcp.configureSource",      );      expect(executor.oauth.start).toBeTypeOf("function");      expect(executor.oauth.complete).toBeTypeOf("function");    }),  );   it.effect("sources list has no configured MCP sources initially", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [mcpPlugin()] as const }));      const sources = yield* executor.sources.list();      expect(sources.filter((source) => !source.runtime)).toHaveLength(0);    }),  );   it.effect("tools list has no configured MCP source tools initially", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [mcpPlugin()] as const }));      const tools = yield* executor.tools.list();      expect(tools.filter((tool) => !tool.id.startsWith("executor.mcp."))).toHaveLength(0);    }),  );   // When discovery fails (auth, network, etc.) we still want the source  // row to land in the DB so users see it in the catalog — they can  // retry via refresh once they fix the underlying problem. The error  // still propagates to the caller so boot-time sync logs the reason.  it.effect("registers source with 0 tools when discovery fails", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [mcpPlugin()] as const }));       const result = yield* executor.mcp        .addSource({          transport: "remote",          scope: "test-scope",          name: "broken",          // Port 1 is reserved — will connection-refused immediately,          // giving us a deterministic discovery failure without any          // server mocks.          endpoint: "http://127.0.0.1:1/mcp",          remoteTransport: "auto",          namespace: "broken_source",        })        .pipe(Effect.result);       expect(Result.isFailure(result)).toBe(true);       const sources = yield* executor.sources.list();      const broken = sources.find((s) => s.id === "broken_source");      expect(broken).toBeDefined();      expect(broken?.kind).toBe("mcp");      expect(broken?.pluginId).toBe("mcp");       const tools = yield* executor.tools.list();      expect(tools.filter((t) => t.sourceId === "broken_source")).toHaveLength(0);      const inspected = yield* executor.tools.invoke(        "executor.mcp.getSource",        { namespace: "broken_source", scope: "test-scope" },        { onElicitation: "accept-all" },      );      expect(inspected).toMatchObject({        ok: true,        data: { source: { namespace: "broken_source", scope: "test-scope" } },      });    }),  );   it.effect("static addSource reports saved remote source when discovery fails", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [mcpPlugin()] as const }));       const result = yield* executor.tools.invoke(        "executor.mcp.addSource",        {          transport: "remote",          name: "broken static",          endpoint: "http://127.0.0.1:1/mcp",          remoteTransport: "auto",          namespace: "broken_static_source",        },        { onElicitation: "accept-all" },      );       expect(result).toMatchObject({        ok: true,        data: {          namespace: "broken_static_source",          source: { id: "broken_static_source", scope: "test-scope" },          toolCount: 0,          discovery: {            status: "failed",          },        },      });       const source = yield* executor.mcp.getSource("broken_static_source", "test-scope");      expect(source?.namespace).toBe("broken_static_source");    }),  );   it.effect("static probeEndpoint returns actionable tool failures", () =>    Effect.gen(function* () {      const config = makeTestConfig({ plugins: [mcpPlugin()] as const });      const executor = yield* createExecutor(config);       const result = yield* executor.tools.invoke("executor.mcp.probeEndpoint", {        endpoint: "http://127.0.0.1:1/mcp",      });       expect(result).toMatchObject({        ok: false,        error: {          code: "mcp_connection_failed",        },      });       yield* executor.close();      yield* Effect.promise(() => config.testDb.close());    }),  );   it.effect("uses initial credential bindings for add-time tool discovery", () =>    Effect.gen(function* () {      const server = yield* serveMcpServer(makeGreetingMcpServer, {        auth: {          validateAuthorization: (authorization) =>            Effect.succeed(authorization === "Bearer secret-token"),        },      });      const executor = yield* createExecutor(        makeTestConfig({ plugins: [makeMemorySecretsPlugin()(), mcpPlugin()] as const }),      );      yield* executor.secrets.set({        id: SecretId.make("mcp-token"),        scope: ScopeId.make("test-scope"),        name: "MCP token",        value: "secret-token",        provider: "memory",      });       const result = yield* executor.mcp.addSource({        transport: "remote",        scope: "test-scope",        name: "authenticated",        endpoint: server.endpoint,        namespace: "authenticated_mcp",        headers: {          Authorization: { kind: "secret", prefix: "Bearer " },        },        credentials: {          scope: "test-scope",          headers: {            Authorization: { kind: "secret", secretId: "mcp-token" },          },        },      });       expect(result).toEqual({ toolCount: 1, namespace: "authenticated_mcp" });      const requests = yield* server.requests;      expect(requests.some((request) => request.authorization === "Bearer secret-token")).toBe(        true,      );    }),  );   it.effect("marks source oauth-backed when add-time credentials include oauth", () =>    Effect.gen(function* () {      const server = yield* serveMcpServer(makeGreetingMcpServer, {        auth: {          validateAuthorization: (authorization) =>            Effect.succeed(authorization === "Bearer oauth-token"),        },      });      const executor = yield* createExecutor(        makeTestConfig({ plugins: [makeMemorySecretsPlugin()(), mcpPlugin()] as const }),      );      const connectionId = ConnectionId.make("mcp-oauth2-initial");      yield* executor.connections.create(        CreateConnectionInput.make({          id: connectionId,          scope: ScopeId.make("test-scope"),          provider: OAUTH2_PROVIDER_KEY,          identityLabel: "Initial MCP OAuth",          accessToken: TokenMaterial.make({            secretId: SecretId.make(`${connectionId}.access_token`),            name: "Initial MCP OAuth Access Token",            value: "oauth-token",          }),          refreshToken: null,          expiresAt: null,          oauthScope: null,          providerState: null,        }),      );       const result = yield* executor.mcp.addSource({        transport: "remote",        scope: "test-scope",        name: "initial oauth",        endpoint: server.endpoint,        namespace: "initial_oauth_mcp",        credentials: {          scope: "test-scope",          auth: {            oauth2: {              connection: { kind: "connection", connectionId },            },          },        },      });       expect(result).toEqual({ toolCount: 1, namespace: "initial_oauth_mcp" });      const stored = yield* executor.mcp.getSource("initial_oauth_mcp", "test-scope");      expect(stored?.config.transport).toBe("remote");      if (stored?.config.transport !== "remote") return;      expect(stored.config.auth.kind).toBe("oauth2");      if (stored.config.auth.kind !== "oauth2") return;      expect(stored.config.auth.connectionSlot).toBe(MCP_OAUTH_CONNECTION_SLOT);    }),  );   // -------------------------------------------------------------------------  // Multi-scope shadowing — regression suite covering the bug class where  // store reads/writes that don't pin scope_id collapse onto whichever visible  // row wins first. Each  // scenario is reproducible against the pre-fix store.  //  // MCP discovery runs on addSource against an unreachable endpoint so  // both addSource calls fail discovery but still persist the source row  // (the behavior the "registers source with 0 tools" test above relies  // on). This gives us two rows at the same namespace across two scopes  // without needing an in-test MCP server.  // -------------------------------------------------------------------------   const ORG_SCOPE = ScopeId.make("org-scope");  const USER_SCOPE = ScopeId.make("user-scope");   const stackedScopes = [    Scope.make({ id: USER_SCOPE, name: "user", createdAt: new Date() }),    Scope.make({ id: ORG_SCOPE, name: "org", createdAt: new Date() }),  ] as const;   // `seedShadowed` wraps `executor.mcp.addSource` at a given scope with  // a broken endpoint. Discovery fails (port 1 is reserved) so the call  // returns Failure, but the source row still lands — exactly the  // "registers source with 0 tools when discovery fails" behavior above.  // We use `Effect.result` so the outer `yield*` never fails the test.  const seedShadowed = (    addSource: (c: {      readonly transport: "remote";      readonly scope: string;      readonly name: string;      readonly endpoint: string;      readonly remoteTransport: "auto";      readonly namespace: string;    }) => Effect.Effect<unknown, unknown>,    args: {      readonly scope: string;      readonly name: string;      readonly endpoint: string;    },  ) =>    addSource({      transport: "remote",      scope: args.scope,      name: args.name,      endpoint: args.endpoint,      remoteTransport: "auto",      namespace: "shared",    }).pipe(Effect.result);   it.effect("shadowed addSource does not wipe the outer-scope source", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({          scopes: stackedScopes,          plugins: [mcpPlugin()] as const,        }),      );       // Org-level base source — discovery fails but row persists.

Lines 1–500 of 1202.