History

e2e: add self-host coverage for emit() content blocks (#1592)

Rhys Sullivan committed 2 months ago

Commit de81e191 parent ac2dec61 file · +94 −0Browse files

Unified · Split

e2e/selfhost/execute-emit-envelope.test.ts+94−0
@@ -0,0 +1,94 @@
1// Selfhost: emit() output actually reaches the MCP client's content blocks,
2// not merely the `emitted` count in the envelope. Regression coverage for
3// issue #1592 — a self-hosted user saw a non-zero `emitted` count but no
4// content blocks in the tool result, only `logs` and the return value. There
5// is cloud coverage for the envelope count
6// (`e2e/cloud/execute-emit-envelope.test.ts`), but nothing that asserts on
7// the wire-level `content` array on self-host, and nothing on either target
8// that covers all three emit shapes from the report: a plain string, a
9// ToolFile, and a structured/object value.
10import { expect } from "@effect/vitest";
11import { Effect } from "effect";
12
13import { scenario } from "../src/scenario";
14import { Mcp, Target } from "../src/services";
15
16interface ContentBlock {
17 readonly type: string;
18 readonly text?: string;
19}
20
21// Mirrors the issue's repro exactly: a plain string emit, a ToolFile emit, a
22// structured/object emit, then a return.
23const EXECUTE_CODE = `
24const attachment = {
25 _tag: "ToolFile",
26 name: "report.txt",
27 mimeType: "text/plain",
28 encoding: "base64",
29 data: "aGVsbG8gZmlsZQ==",
30 byteLength: 10,
31};
32emit("plain string emit");
33emit(attachment);
34emit({ hello: "object emit" });
35return { returnedValue: "this is the return" };
36`;
37
38scenario(
39 "Execute · emit() output reaches the MCP client's content blocks",
40 {},
41 Effect.gen(function* () {
42 const target = yield* Target;
43 const mcp = yield* Mcp;
44 const identity = yield* target.newIdentity();
45 const session = mcp.session(identity);
46
47 const result = yield* session.call("execute", { code: EXECUTE_CODE });
48 expect(result.ok, `execute completed (got: ${result.text.slice(0, 300)})`).toBe(true);
49
50 const structured = (result.raw as { structuredContent?: Record<string, unknown> })
51 .structuredContent;
52 expect(structured?.status, "the run completed").toBe("completed");
53 expect(structured?.emitted, "the envelope counts all three emitted items").toBe(3);
54 expect(structured?.result, "the return value still comes back to the caller").toEqual({
55 returnedValue: "this is the return",
56 });
57
58 // The real assertion: the JSON-RPC tool result's `content` array — what
59 // an MCP client actually renders — carries all three emitted items, not
60 // just the `emitted` count.
61 const content = (result.raw as { content?: ContentBlock[] }).content ?? [];
62 expect(Array.isArray(content) && content.length > 0, "the wire result carries content").toBe(
63 true,
64 );
65
66 expect(
67 content.some((block) => block.type === "text" && block.text === "plain string emit"),
68 `content carries the plain string emit; got: ${JSON.stringify(content)}`,
69 ).toBe(true);
70
71 // A text-mimetype ToolFile renders as a summary line plus its decoded
72 // text content (see outputFileContent/toolFileContent in tool-server.ts).
73 expect(
74 content.some(
75 (block) =>
76 block.type === "text" &&
77 typeof block.text === "string" &&
78 block.text.includes("report.txt"),
79 ),
80 `content carries the ToolFile emit's summary line; got: ${JSON.stringify(content)}`,
81 ).toBe(true);
82 expect(
83 content.some((block) => block.type === "text" && block.text === "hello file"),
84 `content carries the ToolFile emit's decoded text; got: ${JSON.stringify(content)}`,
85 ).toBe(true);
86
87 // A plain object emit is not a ToolFile or an MCP content block, so it
88 // falls back to a JSON text block.
89 expect(
90 content.some((block) => block.type === "text" && block.text === '{"hello":"object emit"}'),
91 `content carries the structured/object emit; got: ${JSON.stringify(content)}`,
92 ).toBe(true);
93 }),
94);