182 lines · 6.0 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Layer, Option, Schema } from "effect";import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import {  AuthTemplateSlug,  ConnectionName,  IntegrationSlug,  ToolAddress,  createExecutor,} from "@executor-js/sdk";import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing"; import { mcpPlugin } from "./plugin"; const FILE_ID = "F012ABC3456";const IMAGE_BYTES = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);const seenRpcMethods: string[] = []; const JsonRpcRequest = Schema.Struct({  id: Schema.optional(Schema.Union([Schema.String, Schema.Number, Schema.Null])),  method: Schema.String,});const decodeJsonRpcRequest = Schema.decodeUnknownOption(Schema.fromJsonString(JsonRpcRequest)); const jsonRpcResponse = (request: typeof JsonRpcRequest.Type, result: unknown): Response =>  Response.json({ jsonrpc: "2.0", id: request.id ?? null, result }); const slackFallbackHttpClientLayer = Layer.succeed(HttpClient.HttpClient)(  HttpClient.make((request: HttpClientRequest.HttpClientRequest) =>    Effect.gen(function* () {      const webRequest = yield* HttpClientRequest.toWeb(request).pipe(Effect.orDie);      const url = new URL(webRequest.url);       if (url.hostname === "slack.com" && url.pathname === "/api/files.info") {        return HttpClientResponse.fromWeb(          request,          Response.json({            ok: true,            file: {              id: FILE_ID,              name: "external-screenshot.png",              mimetype: "image/png",              size: IMAGE_BYTES.byteLength,              url_private_download: `https://files.slack.com/files-pri/T000-${FILE_ID}/download/external-screenshot.png`,            },          }),        );      }       if (url.hostname === "files.slack.com") {        return HttpClientResponse.fromWeb(          request,          new Response(IMAGE_BYTES, { status: 200, headers: { "content-type": "image/png" } }),        );      }       if (url.hostname !== "mcp.slack.com") {        return HttpClientResponse.fromWeb(          request,          new Response("unexpected host", { status: 500 }),        );      }      if (webRequest.method === "GET") {        return HttpClientResponse.fromWeb(request, new Response("SSE disabled", { status: 405 }));      }       const rpc = Option.getOrUndefined(        decodeJsonRpcRequest(yield* Effect.promise(() => webRequest.text())),      );      if (rpc === undefined) {        return HttpClientResponse.fromWeb(          request,          new Response("invalid JSON-RPC", { status: 400 }),        );      }      seenRpcMethods.push(rpc.method);      if (rpc.method === "initialize") {        return HttpClientResponse.fromWeb(          request,          jsonRpcResponse(rpc, {            protocolVersion: "2025-06-18",            capabilities: { tools: {} },            serverInfo: { name: "Slack", version: "1.0.0" },          }),        );      }      if (rpc.method === "notifications/initialized") {        return HttpClientResponse.fromWeb(request, new Response("", { status: 202 }));      }      if (rpc.method === "tools/list") {        return HttpClientResponse.fromWeb(          request,          jsonRpcResponse(rpc, {            tools: [              {                name: "slack_read_file",                inputSchema: {                  type: "object",                  properties: { file_id: { type: "string" } },                  required: ["file_id"],                },              },            ],          }),        );      }      if (rpc.method === "tools/call") {        return HttpClientResponse.fromWeb(          request,          jsonRpcResponse(rpc, {            isError: true,            content: [{ type: "text", text: "execution_failed: file_not_found" }],          }),        );      }      return HttpClientResponse.fromWeb(        request,        new Response("unexpected method", { status: 400 }),      );    }),  ),); describe("Slack Connect file fallback", () => {  it.effect("recovers the image through the caller-visible MCP tool", () =>    Effect.scoped(      Effect.gen(function* () {        const config = {          ...makeTestConfig({            plugins: [              memoryCredentialsPlugin(),              mcpPlugin({ httpClientLayer: slackFallbackHttpClientLayer }),            ] as const,          }),          httpClientLayer: slackFallbackHttpClientLayer,        };        const executor = yield* Effect.acquireRelease(createExecutor(config), (executor) =>          Effect.gen(function* () {            yield* executor.close().pipe(Effect.ignore);            yield* Effect.promise(() => config.testDb.close()).pipe(Effect.ignore);          }),        );         yield* executor.mcp.addServer({          name: "Slack",          endpoint: "https://mcp.slack.com/mcp",          slug: "slack_connect_fixture",          remoteTransport: "streamable-http",          auth: { kind: "oauth2" },        });        yield* executor.connections.create({          owner: "org",          name: ConnectionName.make("main"),          integration: IntegrationSlug.make("slack_connect_fixture"),          template: AuthTemplateSlug.make("oauth2"),          value: "xoxp-test-token",        });         const toolAddresses = (yield* executor.tools.list()).map((tool) => String(tool.address));        expect(seenRpcMethods).toContain("tools/list");        expect(toolAddresses).toContain("tools.slack_connect_fixture.org.main.slack_read_file");         const result = yield* executor.execute(          ToolAddress.make("tools.slack_connect_fixture.org.main.slack_read_file"),          { file_id: FILE_ID },          { onElicitation: "accept-all" },        );         expect(result).toMatchObject({          ok: true,          data: {            content: [              { type: "text", text: expect.stringContaining(FILE_ID) },              { type: "image", mimeType: "image/png" },            ],          },        });      }),    ),  );});