Skip to content
Source
Sign inSign up

toolOutput.test.ts

220 lines8.2 KB

1import { describe, expect, it } from "vite-plus/test";
2​
3import {
4 compactDynamicToolOutput,
5 MAX_TOOL_OUTPUT_IMAGES,
6 mcpToolResultOutput,
7 toolOutputImages,
8 toolOutputIndicatesFailure,
9} from "./toolOutput.ts";
10​
11describe("compactDynamicToolOutput", () => {
12 it("extracts IDs through the MCP result envelopes used by T3 summaries", () => {
13 const metadata = { threadId: "thread-1", messageId: "message-1" };
14 const json = JSON.stringify({ ...metadata, response: "Private response body" });
15 for (const value of [
16 { ...metadata, response: "Private response body" },
17 { structuredContent: JSON.parse(json), content: "Ignored alternative body" },
18 [{ type: "text", text: json }],
19 { content: [{ text: { text: json } }], isError: false },
20 json,
21 ]) {
22 expect(compactDynamicToolOutput(value)).toEqual(metadata);
23 }
24 });
25​
26 it("keeps nested thread identity and scalar task IDs without result bodies", () => {
27 const output = {
28 thread: { threadId: "thread-1", messages: ["Private message"] },
29 taskId: "task-1",
30 scheduledTaskId: "scheduled-1",
31 status: "failed",
32 summary: "A child failed; the tool itself succeeded.",
33 unlinked: [{ repository: "acme/app", state: "merged", numbers: [1] }],
34 unlinkedCount: 1,
35 linkedCount: 2,
36 };
37 expect(compactDynamicToolOutput(output)).toEqual({
38 thread: { threadId: "thread-1" },
39 taskId: "task-1",
40 scheduledTaskId: "scheduled-1",
41 linkedCount: 2,
42 unlinkedCount: 1,
43 });
44 expect(output.thread.messages).toEqual(["Private message"]);
45 });
46​
47 it("keeps explicit envelope failure while dropping error text and arbitrary output", () => {
48 for (const failure of [
49 { isError: true },
50 { is_error: true },
51 { _tag: "OrchestratorMcpFailure" },
52 { error: { message: "Private error" } },
53 ]) {
54 expect(
55 compactDynamicToolOutput({
56 ...failure,
57 structuredContent: { threadId: "thread-1", error: "Private details" },
58 }),
59 ).toEqual({ isError: true, threadId: "thread-1" });
60 }
61 expect(compactDynamicToolOutput("Private plain text")).toBeUndefined();
62 expect(compactDynamicToolOutput({ result: { body: "Private result" } })).toBeUndefined();
63 });
64​
65 it("uses the first result data but combines explicit failure flags across content blocks", () => {
66 expect(
67 compactDynamicToolOutput([
68 { text: JSON.stringify({ threadId: "first" }) },
69 { text: JSON.stringify({ threadId: "second", isError: true, error: "Private error" }) },
70 ]),
71 ).toEqual({ threadId: "first", isError: true });
72 expect(
73 compactDynamicToolOutput([{ text: "{}" }, { text: JSON.stringify({ threadId: "second" }) }]),
74 ).toBeUndefined();
75 });
76​
77 it("keeps complete thread creation batches and rollback markers without row bodies", () => {
78 expect(
79 compactDynamicToolOutput({
80 threads: [
81 { threadId: "created", status: "running", messages: ["Private message"] },
82 { threadId: "reverted", status: "rolled_back", error: "Private error" },
83 { status: "rolled_back" },
84 ],
85 }),
86 ).toEqual({
87 threads: [
88 { threadId: "created" },
89 { threadId: "reverted", status: "rolled_back" },
90 { status: "rolled_back" },
91 ],
92 });
93 expect(compactDynamicToolOutput({ threadId: "reverted", status: "rolled_back" })).toEqual({
94 threadId: "reverted",
95 status: "rolled_back",
96 });
97 expect(compactDynamicToolOutput({ threads: [] })).toEqual({ threads: [] });
98 });
99​
100 it("omits incomplete or oversized creation evidence instead of returning a partial count", () => {
101 for (const threads of [
102 [{ threadId: "known" }, { title: "No confirmed ID" }],
103 Array.from({ length: 101 }, (_, index) => ({ threadId: `thread-${index}` })),
104 Array.from({ length: 100 }, (_, index) => ({ threadId: `${index}`.padEnd(256, "x") })),
105 [{ threadId: "x".repeat(257) }],
106 ]) {
107 expect(
108 compactDynamicToolOutput({
109 threadId: "must-not-be-counted-as-one",
110 status: "rolled_back",
111 taskId: "task-1",
112 threads,
113 }),
114 ).toEqual({ taskId: "task-1" });
115 }
116 const normalBatch = {
117 threads: Array.from({ length: 100 }, (_, index) => ({
118 threadId: `00000000-0000-4000-8000-${String(index).padStart(12, "0")}`,
119 })),
120 };
121 expect(compactDynamicToolOutput(normalBatch)).toEqual(normalBatch);
122 expect(
123 new TextEncoder().encode(JSON.stringify(compactDynamicToolOutput(normalBatch))).byteLength,
124 ).toBeLessThanOrEqual(8_192);
125 });
126​
127 it("bounds parsing and envelope scanning while preserving an outer failure marker", () => {
128 const oversizedJson = JSON.stringify({ threadId: "hidden", body: "πŸ˜„".repeat(5_000) });
129 for (const content of [
130 "x".repeat(1_000_000),
131 oversizedJson,
132 Array.from({ length: 33 }, () => ({ text: JSON.stringify({ threadId: "hidden" }) })),
133 { content: { content: { content: { content: { threadId: "too-deep" } } } } },
134 ]) {
135 expect(compactDynamicToolOutput({ isError: true, content })).toEqual({ isError: true });
136 }
137 expect(compactDynamicToolOutput({ threadId: "known", body: "x".repeat(1_000_000) })).toEqual({
138 threadId: "known",
139 });
140 });
141});
142​
143describe("toolOutputIndicatesFailure", () => {
144 it("preserves the command failure phrases across shells without copying the output", () => {
145 for (const text of [
146 "FILE NOT FOUND",
147 "No files found",
148 "ENOENT",
149 "No such file or directory",
150 "CommandNotFoundException",
151 "command not found",
152 "Cannot find path 'a' because it does not exist",
153 "The term 'example' is not recognized",
154 "is not recognized as the name of a cmdlet",
155 "A parameter cannot be found that matches parameter name",
156 "<exited with exit code 2>",
157 "Exited with exit code 1",
158 "exit code: 127",
159 "clean exit first, then ExIt CoDe: 3",
160 ])
161 expect(toolOutputIndicatesFailure(text)).toBe(true);
162 });
163​
164 it("does not mark successful exit codes or incomplete failure phrases", () => {
165 for (const text of [
166 "Done",
167 "exit code: 0",
168 "Exited with exit code 0",
169 "cannot find path",
170 "is not recognized",
171 "exit",
172 "not found",
173 "the path cannot be found",
174 ]) {
175 expect(toolOutputIndicatesFailure(text)).toBe(false);
176 }
177 });
178});
179​
180describe("toolOutputImages", () => {
181 const mcpImage = { type: "image", data: "AAAA", mimeType: "image/png" };
182 const claudeImage = {
183 type: "image",
184 source: { type: "base64", media_type: "image/JPEG", data: "BBBB" },
185 };
186 const text = { type: "text", text: "{}" };
187​
188 it("finds images in each place providers store them, in order", () => {
189 expect(toolOutputImages({ content: [text, claudeImage, mcpImage] })).toEqual([
190 { mimeType: "image/jpeg", data: "BBBB" },
191 { mimeType: "image/png", data: "AAAA" },
192 ]);
193 expect(toolOutputImages([mcpImage])).toEqual([{ mimeType: "image/png", data: "AAAA" }]);
194 expect(toolOutputImages(mcpImage)).toEqual([{ mimeType: "image/png", data: "AAAA" }]);
195 const structuredContent = { screenshot: { mimeType: "image/png", width: 728, height: 354 } };
196 const content = [text, mcpImage];
197 const output = mcpToolResultOutput({ content, structuredContent });
198 expect(toolOutputImages(output)).toEqual([{ mimeType: "image/png", data: "AAAA" }]);
199 expect(output).toEqual({ content, structuredContent });
200 expect(mcpToolResultOutput({ content: [text], structuredContent })).toBe(structuredContent);
201 expect(mcpToolResultOutput({ content })).toBe(content);
202 expect(mcpToolResultOutput({ content: [text], structuredContent: {} })).toEqual([text]);
203 expect(mcpToolResultOutput({ content: [text], structuredContent: [] })).toEqual([text]);
204 });
205​
206 it("skips types the server must not serve inline", () => {
207 expect(
208 toolOutputImages([
209 { ...mcpImage, mimeType: "image/svg+xml" },
210 { ...mcpImage, mimeType: "text/html" },
211 { type: "image", source: { type: "url", url: "https://example.com/a.png" } },
212 ]),
213 ).toEqual([]);
214 });
215​
216 it("stops after the first few images", () => {
217 const images = toolOutputImages(Array.from({ length: 10_000 }, () => mcpImage));
218 expect(images).toHaveLength(MAX_TOOL_OUTPUT_IMAGES);
219 });
220});