191 lines · 6.5 KB · TypeScript
RawDownload
// Cloud: connection refresh semantics over the wire. Tools are produced// per-connection by the owning plugin; `connections.refresh` re-runs that// discovery — for MCP it re-dials the LIVE server, so a server-side tool// change replaces the stamped tool rows. `canRefresh` on the integration says// whether the catalog row can be refreshed at all: a spec registered from a// URL can be re-fetched, a pasted blob cannot.//// Ported from apps/cloud/src/api/sources-refresh.node.test.ts. The upstream// MCP server is a real HTTP server started inside the scenario on 127.0.0.1.import { randomBytes } from "node:crypto";import { createServer } from "node:http"; import { expect } from "@effect/vitest";import { Effect } from "effect";import { composePluginApi } from "@executor-js/api/server";import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api";import { makeGreetingMcpServer, serveMcpServer } from "@executor-js/plugin-mcp/testing";import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared"; import { scenario } from "../src/scenario";import { Api, Target } from "../src/services"; const api = composePluginApi([openApiHttpPlugin(), mcpHttpPlugin()] as const); const newSlug = (prefix: string) =>  IntegrationSlug.make(`${prefix}_${randomBytes(4).toString("hex")}`); const MAIN = ConnectionName.make("main");const NONE = AuthTemplateSlug.make("none"); /** Minimal OpenAPI 3 spec with a single GET /ping operation. */const pingSpec = JSON.stringify({  openapi: "3.0.3",  info: { title: "Refresh Fixture", version: "1.0.0" },  paths: {    "/ping": {      get: {        operationId: "ping",        summary: "Return a pong",        tags: ["default"],        responses: { "200": { description: "pong" } },      },    },  },}); /** A real node:http server on 127.0.0.1 that serves `body` on every request, *  closed by the scope's finalizer. */const serveJson = (body: string) =>  Effect.acquireRelease(    Effect.callback<{ readonly url: string; readonly close: () => void }>((resume) => {      const server = createServer((_request, response) => {        response.writeHead(200, { "content-type": "application/json" });        response.end(body);      });      server.listen(0, "127.0.0.1", () => {        const address = server.address();        const port = typeof address === "object" && address ? address.port : 0;        resume(          Effect.succeed({            url: `http://127.0.0.1:${port}/spec.json`,            close: () => {              server.close();              server.closeAllConnections();            },          }),        );      });    }),    (server) => Effect.sync(server.close),  ); scenario(  "Connections · refreshing an MCP connection re-dials the live server and replaces its tools",  {},  Effect.scoped(    Effect.gen(function* () {      const target = yield* Target;      const apiSurface = yield* Api;      // The factory is re-invoked per MCP session, so flipping `toolName`      // changes what the NEXT discovery dial sees.      let toolName = "before_refresh";      const server = yield* serveMcpServer(() =>        makeGreetingMcpServer({ name: "refresh-mcp", toolName, text: "ok" }),      );      const identity = yield* target.newIdentity();      const client = yield* apiSurface.client(api, identity);      const slug = newSlug("refmcp");       yield* client.mcp.addServer({        payload: {          transport: "remote",          name: "Refresh MCP",          endpoint: server.endpoint,          remoteTransport: "streamable-http",          slug,        },      });      yield* client.connections.create({        payload: {          owner: "org",          name: MAIN,          integration: slug,          template: NONE,          value: "unused",        },      });       const before = yield* client.tools.list({ query: { integration: slug } });      expect(        before.map((tool) => String(tool.address)),        "the connection stamped the server's current tool",      ).toContain(`tools.${slug}.org.main.before_refresh`);       // The server's catalog changes; only a refresh may pick that up.      toolName = "after_refresh";      const refreshed = yield* client.connections.refresh({        params: { owner: "org", integration: slug, name: MAIN },      });      expect(        refreshed.map((tool) => String(tool.address)),        "refresh returns the re-discovered tool set",      ).toContain(`tools.${slug}.org.main.after_refresh`);       const after = yield* client.tools.list({ query: { integration: slug } });      const addresses = after.map((tool) => String(tool.address));      expect(addresses, "the stale tool row is gone").not.toContain(        `tools.${slug}.org.main.before_refresh`,      );      expect(addresses, "the new tool row is persisted").toContain(        `tools.${slug}.org.main.after_refresh`,      );    }),  ),); scenario(  "Integrations · a spec registered from a URL is refreshable (canRefresh)",  {},  Effect.scoped(    Effect.gen(function* () {      const target = yield* Target;      const apiSurface = yield* Api;      const specServer = yield* serveJson(pingSpec);      const identity = yield* target.newIdentity();      const client = yield* apiSurface.client(api, identity);      const slug = newSlug("refurl");       yield* client.openapi.addSpec({        payload: {          spec: { kind: "url", url: specServer.url },          slug,          baseUrl: "http://127.0.0.1:59999",        },      });       const integration = yield* client.integrations.get({ params: { slug } });      expect(        integration?.canRefresh,        "a URL-sourced spec can be re-fetched, so the integration is refreshable",      ).toBe(true);    }),  ),); scenario(  "Integrations · a spec pasted as a blob is not refreshable (canRefresh)",  {},  Effect.gen(function* () {    const target = yield* Target;    const apiSurface = yield* Api;    const identity = yield* target.newIdentity();    const client = yield* apiSurface.client(api, identity);    const slug = newSlug("refblob");     yield* client.openapi.addSpec({      payload: {        spec: { kind: "blob", value: pingSpec },        slug,        baseUrl: "https://api.example.test",      },    });     const integration = yield* client.integrations.get({ params: { slug } });    expect(      integration?.canRefresh,      "a pasted blob has no upstream to re-poll, so the integration is not refreshable",    ).toBe(false);  }),);