From de81e195ded216a2d8dc2a0ca84bfab7b4d37d8a Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Fri, 28 Aug 2026 02:07:10 -0700 Subject: [PATCH] e2e: add self-host coverage for emit() content blocks (#1592) --- e2e/selfhost/execute-emit-envelope.test.ts | 94 ++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 e2e/selfhost/execute-emit-envelope.test.ts diff --git a/e2e/selfhost/execute-emit-envelope.test.ts b/e2e/selfhost/execute-emit-envelope.test.ts new file mode 100644 index 0000000000..946c25d5b8 --- /dev/null +++ b/e2e/selfhost/execute-emit-envelope.test.ts @@ -0,0 +1,94 @@ +// 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 }) + .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); + }), +);