94 lines · 3.6 KB · TypeScript
RawDownload
// Selfhost: emit() output actually reaches the MCP client's content blocks,// not merely the `emitted` count in the envelope. Regression coverage for// issue #1592 — a self-hosted user saw a non-zero `emitted` count but no// content blocks in the tool result, only `logs` and the return value. There// is cloud coverage for the envelope count// (`e2e/cloud/execute-emit-envelope.test.ts`), but nothing that asserts on// the wire-level `content` array on self-host, and nothing on either target// that covers all three emit shapes from the report: a plain string, a// ToolFile, and a structured/object value.import { expect } from "@effect/vitest";import { Effect } from "effect"; import { scenario } from "../src/scenario";import { Mcp, Target } from "../src/services"; interface ContentBlock {  readonly type: string;  readonly text?: string;} // Mirrors the issue's repro exactly: a plain string emit, a ToolFile emit, a// structured/object emit, then a return.const EXECUTE_CODE = `const attachment = {  _tag: "ToolFile",  name: "report.txt",  mimeType: "text/plain",  encoding: "base64",  data: "aGVsbG8gZmlsZQ==",  byteLength: 10,};emit("plain string emit");emit(attachment);emit({ hello: "object emit" });return { returnedValue: "this is the return" };`; scenario(  "Execute · emit() output reaches the MCP client's content blocks",  {},  Effect.gen(function* () {    const target = yield* Target;    const mcp = yield* Mcp;    const identity = yield* target.newIdentity();    const session = mcp.session(identity);     const result = yield* session.call("execute", { code: EXECUTE_CODE });    expect(result.ok, `execute completed (got: ${result.text.slice(0, 300)})`).toBe(true);     const structured = (result.raw as { structuredContent?: Record<string, unknown> })      .structuredContent;    expect(structured?.status, "the run completed").toBe("completed");    expect(structured?.emitted, "the envelope counts all three emitted items").toBe(3);    expect(structured?.result, "the return value still comes back to the caller").toEqual({      returnedValue: "this is the return",    });     // The real assertion: the JSON-RPC tool result's `content` array — what    // an MCP client actually renders — carries all three emitted items, not    // just the `emitted` count.    const content = (result.raw as { content?: ContentBlock[] }).content ?? [];    expect(Array.isArray(content) && content.length > 0, "the wire result carries content").toBe(      true,    );     expect(      content.some((block) => block.type === "text" && block.text === "plain string emit"),      `content carries the plain string emit; got: ${JSON.stringify(content)}`,    ).toBe(true);     // A text-mimetype ToolFile renders as a summary line plus its decoded    // text content (see outputFileContent/toolFileContent in tool-server.ts).    expect(      content.some(        (block) =>          block.type === "text" &&          typeof block.text === "string" &&          block.text.includes("report.txt"),      ),      `content carries the ToolFile emit's summary line; got: ${JSON.stringify(content)}`,    ).toBe(true);    expect(      content.some((block) => block.type === "text" && block.text === "hello file"),      `content carries the ToolFile emit's decoded text; got: ${JSON.stringify(content)}`,    ).toBe(true);     // A plain object emit is not a ToolFile or an MCP content block, so it    // falls back to a JSON text block.    expect(      content.some((block) => block.type === "text" && block.text === '{"hello":"object emit"}'),      `content carries the structured/object emit; got: ${JSON.stringify(content)}`,    ).toBe(true);  }),);