2647 lines · 95.3 KB · TypeScript
RawDownload
import { existsSync } from "node:fs";import { createServer, type IncomingMessage, type Server } from "node:http";import type { AddressInfo } from "node:net";import { dirname, resolve } from "node:path";import { fileURLToPath } from "node:url"; import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest";import { Effect, Schema } from "effect";import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";import { EXTENSION_ID, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";import type { ClientCapabilities } from "@modelcontextprotocol/sdk/types.js";import {  ArtifactId,  AuthTemplateSlug,  ConnectionName,  FormElicitation,  IntegrationSlug,  ProviderItemId,  ProviderKey,  ToolAddress,  ToolName,  createExecutor,  definePlugin,  type Artifact,  type ArtifactBindings,  type CredentialProvider,  type ToolDef,} from "@executor-js/sdk";import { makeTestConfig } from "@executor-js/sdk/testing";import {  createExecutionEngine,  type ExecutionEngine,  type ExecutionResult,} from "@executor-js/execution";import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";import { chromium, type Browser, type Frame, type Page } from "playwright-core";import { createServer as createViteServer } from "vite";import type * as Cause from "effect/Cause"; import { createExecutorMcpServer } from "@executor-js/host-mcp/tool-server"; import { loadMcpAppsShellHtml } from "../shell-html"; type ShellServer = {  readonly url: string;  readonly close: () => Promise<void>;}; type HostServer = ShellServer; type OpenApiServer = {  readonly specUrl: string;  readonly postRequests: string[];  readonly close: () => Promise<void>;}; type McpHarness = {  readonly callTool: (params: HostToolCall) => Promise<unknown>;  readonly close: () => Promise<void>;}; type HostToolCall = {  readonly name?: string;  readonly arguments?: Record<string, unknown>;}; type HostState = {  readonly initialized: boolean;  readonly toolCalls: HostToolCall[];  readonly resumeCalls: HostToolCall[];}; type BrowserHostWindow = Window & {  __mcpHostState: HostState;  __sendGeneratedUi: (code: string, artifactId: string) => void;  __setHostContext: (context: Record<string, unknown>) => void;}; type AppsClientCapabilities = ClientCapabilities & {  readonly extensions: Record<string, unknown>;}; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");// playwright-core ships no browser of its own, so point it at an installed// Chrome. `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` wins; otherwise try the// conventional per-platform locations so this runs on a dev machine and in CI// without configuration.const defaultChromePaths = [  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",  "/usr/bin/google-chrome",  "/usr/bin/chromium",  "/usr/bin/chromium-browser",];const chromeExecutablePath =  process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ??  defaultChromePaths.find((candidate) => existsSync(candidate)) ??  "/usr/bin/google-chrome";const formToolId = ToolAddress.make("tools.test.form.main.t"); const EmptySchema = Schema.toStandardSchemaV1(Schema.toStandardJSONSchemaV1(Schema.Struct({})));const CreateItemSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(    Schema.Struct({      body: Schema.Struct({ name: Schema.String }),    }),  ),);const DomainSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(Schema.Struct({ domain: Schema.String })),);// Two cursor shapes, because the proxy's `cursorKey` has to place a page param// at a top-level field AND down a dotted path into a nested input object (the// common `{ query: { … } }` OpenAPI shape).const ListPageSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(    Schema.Struct({      limit: Schema.optional(Schema.Number),      cursor: Schema.optional(Schema.String),    }),  ),);const ListNestedPageSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(    Schema.Struct({      query: Schema.optional(        Schema.Struct({          limit: Schema.optional(Schema.Number),          since: Schema.optional(Schema.String),        }),      ),    }),  ),); /** * A three-page cursor-paginated upstream. * * `cursor === undefined` is page 1 — which is what makes the proxy's * "`initialPageParam: null` sends no cursor at all" convention observable: a * first request carrying `{"cursor":null}` would land here as a page-not-found * rather than page 1. */const PAGE_SIZE = 2;const PAGED_ITEMS = ["alpha", "beta", "gamma", "delta", "epsilon"] as const; const pageOf = (cursor: string | undefined) => {  const offset = cursor === undefined ? 0 : Number(cursor);  if (!Number.isInteger(offset) || offset < 0 || offset >= PAGED_ITEMS.length) {    return { items: [], nextCursor: null };  }  const items = PAGED_ITEMS.slice(offset, offset + PAGE_SIZE);  const next = offset + PAGE_SIZE;  return { items, nextCursor: next < PAGED_ITEMS.length ? String(next) : null };};const UpdateAutoRenewSchema = Schema.toStandardSchemaV1(  Schema.toStandardJSONSchemaV1(    Schema.Struct({      domain: Schema.String,      body: Schema.Struct({ autoRenew: Schema.Boolean }),    }),  ),); // The fixture upstream: one `inventory` integration whose tools the generated// UI calls through the shell's proxy. v2 addresses tools per connection// (`tools.<integration>.<owner>.<connection>.<tool>`), so the harness seeds the// integration and one org `main` connection below and the generated components// call `tools.inventory.<tool>`.const INVENTORY_SLUG = IntegrationSlug.make("inventory");const INVENTORY_CONNECTION = ConnectionName.make("main");const INVENTORY_TEMPLATE = AuthTemplateSlug.make("apiKey"); const inventoryMemoryProvider = (): CredentialProvider => {  const store = new Map<string, string>();  return {    key: ProviderKey.make("inventory-memory"),    writable: true,    get: (id) => Effect.sync(() => store.get(String(id)) ?? null),    set: (id, value) => Effect.sync(() => void store.set(String(id), value)),    has: (id) => Effect.sync(() => store.has(String(id))),    list: () =>      Effect.sync(() =>        Array.from(store.keys()).map((key) => ({          id: ProviderItemId.make(key),          name: key,        })),      ),  };}; const validateWith = (  validator: ReturnType<typeof Schema.toStandardSchemaV1>,  args: unknown,): Effect.Effect<unknown, unknown> =>  Effect.promise(() => Promise.resolve(validator["~standard"].validate(args))).pipe(    Effect.flatMap((result) =>      "value" in result ? Effect.succeed(result.value) : Effect.fail(result),    ),  ); const inventoryPlugin = (postRequests: string[]) => {  let autoRenew = false;   type InventorySpec = {    readonly name: string;    readonly description: string;    readonly inputJsonSchema: unknown;    readonly validator: ReturnType<typeof Schema.toStandardSchemaV1>;    /** Mutating tools pause for approval — that's what exercises the shell's     *  trusted-interaction modal. */    readonly requiresApproval?: boolean;    readonly handler: (args: never) => unknown;  };   const specs: readonly InventorySpec[] = [    {      name: "listItems",      description: "List inventory items",      inputJsonSchema: { type: "object", properties: {} },      validator: EmptySchema,      handler: () => [{ name: "Widget" }],    },    {      name: "createItem",      description: "Create an inventory item",      inputJsonSchema: {        type: "object",        properties: {          body: {            type: "object",            properties: { name: { type: "string" } },            required: ["name"],          },        },        required: ["body"],      },      validator: CreateItemSchema,      requiresApproval: true,      handler: (args: { readonly body: { readonly name: string } }) => {        postRequests.push(JSON.stringify({ name: args.body.name }));        return { name: args.body.name, created: true };      },    },    {      name: "getDomain",      description: "Get domain settings",      inputJsonSchema: {        type: "object",        properties: { domain: { type: "string" } },        required: ["domain"],      },      validator: DomainSchema,      handler: (args: { readonly domain: string }) => ({ domain: args.domain, renew: autoRenew }),    },    {      name: "listPaged",      description: "List items one page at a time, top-level cursor",      inputJsonSchema: {        type: "object",        properties: { limit: { type: "integer" }, cursor: { type: "string" } },      },      validator: ListPageSchema,      handler: (args: { readonly cursor?: string }) => pageOf(args.cursor),    },    {      name: "listPagedNested",      description: "List items one page at a time, cursor nested under query",      inputJsonSchema: {        type: "object",        properties: {          query: {            type: "object",            properties: { limit: { type: "integer" }, since: { type: "string" } },          },        },      },      validator: ListNestedPageSchema,      handler: (args: { readonly query?: { readonly since?: string } }) =>        pageOf(args.query?.since),    },    {      name: "updateDomainAutoRenew",      description: "Update domain auto-renew",      inputJsonSchema: {        type: "object",        properties: {          domain: { type: "string" },          body: {            type: "object",            properties: { autoRenew: { type: "boolean" } },            required: ["autoRenew"],          },        },        required: ["domain", "body"],      },      validator: UpdateAutoRenewSchema,      requiresApproval: true,      handler: (args: {        readonly domain: string;        readonly body: { readonly autoRenew: boolean };      }) => {        autoRenew = args.body.autoRenew;        postRequests.push(JSON.stringify({ autoRenew }));        return { domain: args.domain, renew: autoRenew };      },    },  ];   const byName = new Map(specs.map((spec) => [spec.name, spec] as const));   return definePlugin(() => ({    id: "inventory-test" as const,    credentialProviders: [inventoryMemoryProvider()],    storage: () => ({}),    resolveTools: () =>      Effect.succeed({        tools: specs.map(          (spec): ToolDef => ({            name: ToolName.make(spec.name),            description: spec.description,            inputSchema: spec.inputJsonSchema,            ...(spec.requiresApproval ? { annotations: { requiresApproval: true } } : {}),          }),        ),      }),    invokeTool: ({ toolRow, args }) => {      const spec = byName.get(toolRow.name);      if (!spec) return Effect.succeed(undefined);      return validateWith(spec.validator, args).pipe(        Effect.map((decoded) => spec.handler(decoded as never)),      );    },    extension: (ctx) => ({      seed: () =>        ctx.core.integrations.register({          slug: INVENTORY_SLUG,          description: "Inventory",          config: {},        }),    }),  }))();}; const appsWithoutElicitationCapabilities: AppsClientCapabilities = {  extensions: { [EXTENSION_ID]: { mimeTypes: [RESOURCE_MIME_TYPE] } },}; const networkPrimitives = [  "fetch",  "XMLHttpRequest",  "WebSocket",  "EventSource",  "Worker",  "SharedWorker",] as const; const generatedDataCode = `const primitiveNames = ${JSON.stringify(networkPrimitives)};const blockedMessages = primitiveNames.map((name) => {  try {    globalThis[name]("https://example.com/should-not-load");    return name + ":allowed";  } catch (err) {    return name + ":" + (err instanceof Error ? err.message : String(err));  }}); function App() {  const { data, error, isLoading } = useQuery(    tools.inventory.listItems.queryOptions({})  );  const items = data?.ok ? data.data : data;  return (    <Card>      <CardContent>        <div id="status">{isLoading ? "loading" : error ? error.message : items?.[0]?.name}</div>        <pre id="blocked">{blockedMessages.join("\\n")}</pre>      </CardContent>    </Card>  );}`; const generatedStaticCode = `function App() {  return (    <Card>      <CardContent>        <div id="ready">ready</div>      </CardContent>    </Card>  );}`; /** A different artifact, for asserting what a SECOND delivery does and does not *  do — its own marker so the test waits on the new render, not the old one. */const generatedSecondRenderCode = `function App() {  return (    <Card>      <CardContent>        <div id="second">second render</div>      </CardContent>    </Card>  );}`; /** Two accounts of one integration, told apart by role. Both roles are bound to *  the harness's single connection, so both reads succeed — what is under test *  is that the role survives the whole path (proxy -> cache key -> wire -> *  server) rather than which upstream answers. */const generatedRoleTaggedCode = `function App() {  const primary = useQuery(tools.inventory("inventory").listItems.queryOptions({}));  const secondary = useQuery(tools.inventory("alt").listItems.queryOptions({}));  const nameOf = (q) => {    const rows = q.data?.ok ? q.data.data : q.data;    return rows?.[0]?.name ?? "";  };  return (    <Card>      <CardContent>        <div id="primary">{primary.isLoading ? "loading" : nameOf(primary)}</div>        <div id="secondary">{secondary.isLoading ? "loading" : nameOf(secondary)}</div>      </CardContent>    </Card>  );}`; const generatedApprovalCode = `function App() {  const [status, setStatus] = useState("idle");  const createItem = useMutation(    tools.inventory.createItem.mutationOptions({      onSuccess: (result) => {        const created = result?.ok ? result.data : result;        setStatus(created.name + ":" + created.created);      },      onError: (error) => setStatus(error.message),    })  );  const ask = async () => {    setStatus("pending");    await createItem.mutateAsync({ body: { name: "Approved Widget" } });  };   return (    <Card>      <CardContent>        <Button id="ask" onClick={ask}>Ask</Button>        <div id="mutation-pending">{String(createItem.isPending)}</div>        <div id="approval-status">{status}</div>      </CardContent>    </Card>  );}`; // Two approval-gated tools in one artifact, which is what makes "don't ask// again" observable: a grant remembered for one address must not answer the// other.const generatedTwoToolApprovalCode = `function App() {  const [createStatus, setCreateStatus] = useState("idle");  const [renewStatus, setRenewStatus] = useState("idle");  const createItem = useMutation(    tools.inventory.createItem.mutationOptions({      onSuccess: (result) => {        const created = result?.ok ? result.data : result;        setCreateStatus(created.name);      },      onError: (error) => setCreateStatus(error.message),    })  );  const updateAutoRenew = useMutation(    tools.inventory.updateDomainAutoRenew.mutationOptions({      onSuccess: (result) => {        const updated = result?.ok ? result.data : result;        setRenewStatus(String(updated.renew));      },      onError: (error) => setRenewStatus(error.message),    })  );   return (    <Card>      <CardContent>        <Button id="create-first" onClick={() => createItem.mutate({ body: { name: "First Widget" } })}>          Create first        </Button>        <Button id="create-second" onClick={() => createItem.mutate({ body: { name: "Second Widget" } })}>          Create second        </Button>        {/* Writes the value the fixture already holds, so this test proves the            second address still asks without disturbing the shared upstream            state a later test reads. */}

Lines 1–500 of 2647.