768 lines · 29.8 KB · TypeScript
RawDownload
import { expect, it } from "@effect/vitest";import { NodeHttpServer } from "@effect/platform-node";import * as NodeServices from "@effect/platform-node/NodeServices";import { EnvironmentId, PreviewTabId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import { McpProtocol, McpSchema, McpServer } from "effect/unstable/ai";import { HttpBody, HttpClient, HttpRouter, HttpServerResponse } from "effect/unstable/http"; import { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts";import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts";import * as ServerConfig from "../config.ts";import * as McpHttpServer from "./McpHttpServer.ts";import * as McpInvocationContext from "./McpInvocationContext.ts";import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts"; const environmentId = EnvironmentId.make("environment-mcp-test");const threadId = ThreadId.make("thread-mcp-test");const tabId = PreviewTabId.make("tab-mcp-test");const alternateTabId = PreviewTabId.make("tab-mcp-alternate");const decodeJsonText = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));const invocation = {  environmentId,  threadId,  providerSessionId: "provider-session-mcp-test",  providerInstanceId: ProviderInstanceId.make("codex"),  capabilities: new Set(["preview"] as const),  issuedAt: 1,};const client = McpSchema.McpServerClient.of({  clientId: 1,  clientCapabilities: {},  clientInfo: { name: "mcp-test", version: "1.0.0" },  protocolVersion: "2025-06-18",  initializePayload: {    protocolVersion: "2025-06-18",    capabilities: {},    clientInfo: { name: "mcp-test", version: "1.0.0" },  },  getClient: Effect.die("unused"),});const TestLayer = McpHttpServer.PreviewToolkitRegistrationLive.pipe(  Layer.provideMerge(McpServer.McpServer.layer),  Layer.provideMerge(PreviewAutomationBroker.layer),  Layer.provideMerge(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-http-server-test-" })),  Layer.provideMerge(NodeServices.layer),);const PullRequestsTestLayer = McpHttpServer.PullRequestsToolkitRegistrationLive.pipe(  Layer.provideMerge(McpServer.McpServer.layer),  Layer.provide(    Layer.mergeAll(      Layer.mock(ProjectionSnapshotQuery)({        getThreadShellById: () => Effect.succeed(Option.none()),      }),      Layer.mock(OrchestrationEngineService)({}),      NodeServices.layer,    ),  ),); const snapshotResult = {  url: "http://example.test/",  title: "Example",  loading: false,  visibleText: "Example",  interactiveElements: [],  accessibilityTree: {},  consoleEntries: [],  networkEntries: [],  actionTimeline: [],  screenshot: {    mimeType: "image/png",    data: Buffer.from("png").toString("base64"),    width: 10,    height: 5,  },}; /** Answers every snapshot request on a fresh broker host with the given result. */const serveSnapshots = (clientId: string, result: unknown) =>  Effect.gen(function* () {    const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;    const connected = yield* Deferred.make<void>();    const inputs: Array<unknown> = [];    const events = yield* broker.connect({ clientId, environmentId });    yield* Stream.runForEach(events, (event) => {      if (event.type === "connected") return Deferred.succeed(connected, undefined);      inputs.push(event.request.input);      return broker.respond({        clientId,        connectionId: event.connectionId,        requestId: event.request.requestId,        ok: true,        result,      });    }).pipe(Effect.forkScoped);    yield* Deferred.await(connected);    return inputs;  }); const callSnapshot = (args: Record<string, unknown>) =>  Effect.gen(function* () {    const server = yield* McpServer.McpServer;    return yield* server      .callTool({ name: "preview_snapshot", arguments: args })      .pipe(        Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),        Effect.provideService(McpSchema.McpServerClient, client),      );  }); it("normalizes empty successful notification responses to accepted", () => {  const notificationResponse = McpHttpServer.normalizeMcpHttpResponse(    HttpServerResponse.text("", { status: 200, contentType: "application/json" }),  );  expect(notificationResponse.status).toBe(202);   const resultResponse = McpHttpServer.normalizeMcpHttpResponse(    HttpServerResponse.jsonUnsafe({ jsonrpc: "2.0", id: 1, result: {} }),  );  expect(resultResponse.status).toBe(200);}); it.effect.each([{}, { includeImage: false }])(  "returns bounded structural preview snapshot failures %#",  (input) =>    Effect.scoped(      Effect.gen(function* () {        const server = yield* McpServer.McpServer;        const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;        const events = yield* broker.connect({          clientId: "mcp-failure-client",          environmentId,        });        yield* Stream.runForEach(events, (event) =>          event.type === "connected"            ? Effect.void            : broker.respond({                clientId: "mcp-failure-client",                connectionId: event.connectionId,                requestId: event.request.requestId,                ok: false,                error: {                  _tag: "PreviewAutomationExecutionError",                  message: "sensitive renderer failure",                  detail: { consoleOutput: "sensitive browser output" },                },              }),        ).pipe(Effect.forkScoped);        yield* Effect.yieldNow;         const snapshot = yield* server          .callTool({ name: "preview_snapshot", arguments: input })          .pipe(            Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),            Effect.provideService(McpSchema.McpServerClient, client),          );         expect(snapshot.isError).toBe(true);        expect(snapshot.content).toEqual([          { type: "text", text: "Preview snapshot failed: PreviewAutomationExecutionError." },        ]);        expect(snapshot.structuredContent).toEqual({          error: {            _tag: "PreviewAutomationExecutionError",            operation: "snapshot",            failureCount: 1,          },        });      }),    ).pipe(Effect.provide(TestLayer)),); it.effect.each([  { mode: "default", input: {}, images: true },  { mode: "explicit image", input: { includeImage: true }, images: true },  { mode: "text only", input: { includeImage: false }, images: false },])("returns fresh $mode snapshots on repeated MCP calls", ({ input, images }) =>  Effect.scoped(    Effect.gen(function* () {      const server = yield* McpServer.McpServer;      const broker = yield* PreviewAutomationBroker.PreviewAutomationBroker;      const connected = yield* Deferred.make<void>();      const png =        "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=";      const page = {        url: "http://example.test/",        loading: false,        visibleText: "Save your changes",        interactiveElements: [          {            tag: "button",            role: "button",            name: "Save",            selector: "#save",            x: 0,            y: 0,            width: 20,            height: 10,          },        ],        accessibilityTree: { role: "document", name: "Example" },        consoleEntries: [],        networkEntries: [],        actionTimeline: [],      };      const screenshot = { mimeType: "image/png", width: 1, height: 1 };      let requests = 0;      const events = yield* broker.connect({ clientId: "mcp-image-option-client", environmentId });      yield* Stream.runForEach(events, (event) => {        if (event.type === "connected") return Deferred.succeed(connected, undefined);        requests += 1;        expect(event.request).toMatchObject({          operation: "snapshot",          tabId: alternateTabId,          threadId,        });        expect(event.request.input).toEqual({});        return broker.respond({          clientId: "mcp-image-option-client",          connectionId: event.connectionId,          requestId: event.request.requestId,          ok: true,          result: {            ...page,            title: `Snapshot ${requests}`,            screenshot: { ...screenshot, data: png },          },        });      }).pipe(Effect.forkScoped);      yield* Deferred.await(connected);       for (const call of [1, 2, 3, 4, 5, 6]) {        const snapshot = yield* server          .callTool({            name: "preview_snapshot",            arguments: { ...input, tabId: alternateTabId },          })          .pipe(            Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),            Effect.provideService(McpSchema.McpServerClient, client),          );        const metadata = { ...page, title: `Snapshot ${call}`, screenshot };        const { accessibilityTree: _tree, ...boundedMetadata } = metadata;        expect(snapshot.isError).toBe(false);        expect(snapshot.structuredContent).toEqual(metadata);        const [identity, text, ...rest] = snapshot.content;        expect(identity?.type === "text" ? decodeJsonText(identity.text) : null).toEqual({          url: page.url,        });        expect(text?.type === "text" ? decodeJsonText(text.text) : null).toEqual(boundedMetadata);        expect(rest).toEqual([          {            type: "text",            text: "Snapshot text was bounded. Omitted: accessibilityTree (use interactiveElements locators or preview_evaluate).",          },          ...(images            ? [                {                  type: "image",                  mimeType: "image/png",                  data: new Uint8Array(Buffer.from(png, "base64")),                },              ]            : []),        ]);      }       // Output selection belongs to this call, not the MCP session's history.      const nextDefault = yield* server        .callTool({          name: "preview_snapshot",          arguments: { tabId: alternateTabId },        })        .pipe(          Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),          Effect.provideService(McpSchema.McpServerClient, client),        );      expect(nextDefault.content.map((content) => content.type)).toEqual([        "text",        "text",        "text",        "image",      ]);      expect(nextDefault.structuredContent).toEqual({ ...page, title: "Snapshot 7", screenshot });      expect(requests).toBe(7);    }),  ).pipe(Effect.provide(TestLayer)),); it.effect("rejects non-boolean snapshot image options before selecting a browser host", () =>  Effect.gen(function* () {    const server = yield* McpServer.McpServer;    for (const includeImage of ["false", 0, null]) {      const result = yield* server        .callTool({          name: "preview_snapshot",          arguments: { includeImage },        })        .pipe(          Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),          Effect.provideService(McpSchema.McpServerClient, client),        );      expect(result.isError).toBe(true);      expect(result.content).toEqual([{ type: "text", text: "Preview snapshot failed: AiError." }]);      expect(result.structuredContent).toEqual({        error: { _tag: "AiError", operation: "snapshot", failureCount: 1 },      });    }  }).pipe(Effect.provide(TestLayer)),); it.effect("saves the snapshot PNG on request and reports its path", () =>  Effect.scoped(    Effect.gen(function* () {      const config = yield* ServerConfig.ServerConfig;      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const inputs = yield* serveSnapshots("mcp-save-client", snapshotResult);       const snapshot = yield* callSnapshot({ save: true });       expect(snapshot.isError).toBe(false);      // The browser never receives the server-only `save` flag.      expect(inputs).toEqual([{}]);      const structured = snapshot.structuredContent as { readonly screenshotPath?: string };      const screenshotPath = structured.screenshotPath;      expect(typeof screenshotPath).toBe("string");      expect(path.dirname(screenshotPath!)).toBe(config.browserArtifactsDir);      expect(path.basename(screenshotPath!)).toMatch(        /^browser-screenshot-example-test-[0-9a-z]+-[0-9a-f]{8}\.png$/,      );      expect(Buffer.from(yield* fileSystem.readFile(screenshotPath!)).toString()).toBe("png");      const [, text] = snapshot.content;      expect(text?.type === "text" ? text.text : "").toContain(screenshotPath);       const unsaved = yield* callSnapshot({});      expect(unsaved.structuredContent).not.toHaveProperty("screenshotPath");    }),  ).pipe(Effect.provide(TestLayer)),); it.effect("reports a tagged error when the screenshot cannot be saved", () =>  Effect.scoped(    Effect.gen(function* () {      const config = yield* ServerConfig.ServerConfig;      const fileSystem = yield* FileSystem.FileSystem;      // A regular file where the artifacts directory should be makes every write fail.      yield* fileSystem.writeFileString(config.browserArtifactsDir, "");      yield* serveSnapshots("mcp-save-failure-client", snapshotResult);       const snapshot = yield* callSnapshot({ save: true });       expect(snapshot.isError).toBe(true);      expect(snapshot.content).toEqual([        { type: "text", text: "Preview snapshot failed: PreviewScreenshotSaveError." },      ]);      expect(snapshot.structuredContent).toEqual({        error: { _tag: "PreviewScreenshotSaveError", operation: "snapshot", failureCount: 1 },      });    }),  ).pipe(Effect.provide(TestLayer)),); it.effect(  "registers the pull request toolkit and surfaces a missing capability as a tool error",  () =>    Effect.gen(function* () {      const server = yield* McpServer.McpServer;      const names = server.tools.map(({ tool }) => tool.name);      expect(names).toEqual(        expect.arrayContaining([          "link_pull_request",          "unlink_pull_request",          "list_thread_pull_requests",        ]),      );      const linkTool = server.tools.find(({ tool }) => tool.name === "link_pull_request");      expect(linkTool?.tool.annotations?.idempotentHint).toBe(true);      expect(linkTool?.tool.annotations?.openWorldHint).toBe(false);      expect(linkTool?.tool.description).toContain("Register every pull request you open");       const denied = yield* server        .callTool({ name: "list_thread_pull_requests", arguments: {} })        .pipe(          // A preview-only credential: the token predates the toolkit or was minted elsewhere.          Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),          Effect.provideService(McpSchema.McpServerClient, client),        );      expect(denied.isError).toBe(true);      expect(denied.content).toEqual([        { type: "text", text: "MCP credential does not grant the pull-requests capability." },      ]);    }).pipe(Effect.provide(PullRequestsTestLayer)),); it.effect("keeps the snapshot text under the agent's output ceiling", () =>  Effect.scoped(    Effect.gen(function* () {      // Mirrors the real failure: a [role] container whose innerText is the whole      // project list, repeated for several elements, plus a big AX tree.      const pageText = "/Users/theo/Code/project\nClaude, Codex · 79 threads\n".repeat(600);      const element = (name: string, index: number) => ({        tag: "div",        role: "presentation",        name,        selector: `div:nth-of-type(${index})`,        x: 0,        y: 0,        width: 10,        height: 10,      });      const oversized = {        ...snapshotResult,        visibleText: pageText,        interactiveElements: [          element(pageText, 1),          element(pageText, 2),          element(pageText, 3),          element("Continue", 4),        ],        accessibilityTree: { nodes: Array.from({ length: 2_000 }, (_, i) => ({ nodeId: `${i}` })) },        consoleEntries: Array.from({ length: 100 }, (_, i) => ({          level: "log",          text: `entry ${i}`,          timestamp: "t",        })),      };      yield* serveSnapshots("mcp-bounded-client", oversized);       const snapshot = yield* callSnapshot({ includeImage: false });       expect(snapshot.isError).toBe(false);      const [identity, text, notice] = snapshot.content;      expect(identity?.type === "text" ? decodeJsonText(identity.text) : null).toEqual({        url: oversized.url,      });      expect(text?.type).toBe("text");      const body = text?.type === "text" ? text.text : "";      expect(Buffer.byteLength(body, "utf8")).toBeLessThanOrEqual(        McpHttpServer.MAX_SNAPSHOT_TEXT_BYTES,      );      const parsed = decodeJsonText(body) as {        readonly accessibilityTree?: unknown;        readonly visibleText: string;        readonly interactiveElements: ReadonlyArray<{ readonly name: string }>;        readonly consoleEntries: ReadonlyArray<{ readonly text: string }>;      };      expect(parsed.accessibilityTree).toBeUndefined();      expect(parsed.visibleText.length).toBeLessThanOrEqual(8_001);      expect(parsed.interactiveElements).toHaveLength(4);      expect(parsed.interactiveElements[0]?.name.length).toBeLessThanOrEqual(201);      expect(parsed.interactiveElements[3]?.name).toBe("Continue");      expect(parsed.consoleEntries).toHaveLength(40);      expect(parsed.consoleEntries[0]?.text).toBe("entry 60");      expect(notice?.type === "text" ? notice.text : "").toContain("accessibilityTree");      expect(notice?.type === "text" ? notice.text : "").toContain("60 older console entries");      // The structured result is untouched; only the text the agent reads is bounded.      expect(snapshot.structuredContent).toMatchObject({        accessibilityTree: oversized.accessibilityTree,      });    }),  ).pipe(Effect.provide(TestLayer)),); it.effect("bounds the snapshot text even when nothing but logs and the title are large", () =>  Effect.scoped(    Effect.gen(function* () {      const oversized = {        ...snapshotResult,        title: "t".repeat(70_000),        interactiveElements: [],        consoleEntries: [{ level: "log", text: "x".repeat(70_000), timestamp: "t" }],      };      yield* serveSnapshots("mcp-bounded-logs-client", oversized);       const snapshot = yield* callSnapshot({ includeImage: false });       const [, text] = snapshot.content;      const body = text?.type === "text" ? text.text : "";      expect(Buffer.byteLength(body, "utf8")).toBeLessThanOrEqual(        McpHttpServer.MAX_SNAPSHOT_TEXT_BYTES,      );      const parsed = decodeJsonText(body) as {        readonly title: string;        readonly consoleEntries: ReadonlyArray<{ readonly text: string }>;      };      expect(parsed.title.length).toBe(2_049);      expect(parsed.consoleEntries[0]?.text.length).toBe(501);      const notice = snapshot.content[2];      const noticeText = notice?.type === "text" ? notice.text : "";      expect(noticeText).toContain("url or title after 2048 characters");      expect(noticeText).toContain("console entries text after 500 characters");    }),

Lines 1–500 of 768.