1import { NodeId, ThreadId, TurnItemId, type OrchestrationV2TurnItem } from "@t3tools/contracts";
2import {
3 MAX_TOOL_OUTPUT_IMAGE_BASE64_LENGTH,
4 MAX_TOOL_OUTPUT_IMAGES,
5 toolOutputImages,
6} from "@t3tools/shared/toolOutput";
7import * as DateTime from "effect/DateTime";
8import { describe, expect, it } from "vite-plus/test";
9
10import { stripUnservedToolOutputImageBytes } from "./toolOutputImageBytes.ts";
11
12const PNG_BYTES = 3_000;
13const PNG_BASE64 = Buffer.alloc(PNG_BYTES, 7).toString("base64");
14// 3,001 bytes encode with `=` padding, which must not count as data.
15const PADDED_BASE64 = Buffer.alloc(PNG_BYTES + 1, 9).toString("base64");
16
17const now = DateTime.makeUnsafe("2026-10-06T00:00:00.000Z");
18
19const itemBase = {
20 id: TurnItemId.make("turn-item:tool"),
21 threadId: ThreadId.make("thread:tool"),
22 runId: null,
23 nodeId: NodeId.make("node:tool"),
24 providerThreadId: null,
25 providerTurnId: null,
26 nativeItemRef: null,
27 parentItemId: null,
28 ordinal: 1,
29 status: "completed",
30 title: null,
31 startedAt: now,
32 completedAt: now,
33 updatedAt: now,
34} as const;
35
36function toolItem(output: unknown, toolName = "Read"): OrchestrationV2TurnItem {
37 return {
38 ...itemBase,
39 type: "dynamic_tool",
40 toolName,
41 input: { file_path: "/tmp/shot.png" },
42 output,
43 };
44}
45
46function outputOf(item: OrchestrationV2TurnItem): unknown {
47 return item.type === "dynamic_tool" ? item.output : undefined;
48}
49
50describe("stripUnservedToolOutputImageBytes", () => {
51 it("replaces the base64 of Claude's structured Read result with its measured size", () => {
52 const item = toolItem({
53 type: "image",
54 file: {
55 base64: PNG_BASE64,
56 type: "image/png",
57 originalSize: PNG_BYTES,
58 dimensions: { originalWidth: 180, originalHeight: 180 },
59 },
60 });
61
62 expect(outputOf(stripUnservedToolOutputImageBytes(item))).toEqual({
63 type: "image",
64 file: {
65 type: "image/png",
66 originalSize: PNG_BYTES,
67 dimensions: { originalWidth: 180, originalHeight: 180 },
68 sizeBytes: PNG_BYTES,
69 },
70 });
71 });
72
73 it("strips ACP image shapes: Grok's native ImageContent and an MCP result behind an error", () => {
74 const item = toolItem({
75 rawOutput: { type: "ReadFile", ImageContent: { data: PNG_BASE64, mime_type: "image/png" } },
76 failed: {
77 error: "partial result",
78 result: [{ type: "image", data: PADDED_BASE64, mimeType: "image/png" }],
79 },
80 });
81
82 expect(outputOf(stripUnservedToolOutputImageBytes(item))).toEqual({
83 rawOutput: {
84 type: "ReadFile",
85 ImageContent: { mime_type: "image/png", sizeBytes: PNG_BYTES },
86 },
87 failed: {
88 error: "partial result",
89 result: [{ type: "image", mimeType: "image/png", sizeBytes: PNG_BYTES + 1 }],
90 },
91 });
92 });
93
94 it("strips a Cursor image part that names no mime type", () => {
95 const item = toolItem({
96 content: [{ text: { text: "captured" } }, { image: { data: PNG_BASE64 } }],
97 });
98
99 expect(outputOf(stripUnservedToolOutputImageBytes(item))).toEqual({
100 content: [{ text: { text: "captured" } }, { image: { sizeBytes: PNG_BYTES } }],
101 });
102 });
103
104 it("keeps the screenshots a tool-output-image asset serves", () => {
105 const claudeImage = {
106 type: "image",
107 source: { type: "base64", media_type: "image/png", data: PNG_BASE64 },
108 };
109 const mcpImage = { type: "image", data: PADDED_BASE64, mimeType: "image/jpeg" };
110 const item = toolItem(
111 { content: [{ type: "text", text: "captured" }, claudeImage, mcpImage] },
112 "mcp__t3-code__device_screenshot",
113 );
114
115 const stripped = stripUnservedToolOutputImageBytes(item);
116
117 expect(stripped).toBe(item);
118 expect(toolOutputImages(outputOf(stripped))).toEqual([
119 { mimeType: "image/png", data: PNG_BASE64 },
120 { mimeType: "image/jpeg", data: PADDED_BASE64 },
121 ]);
122 });
123
124 it("strips only the images past the last one an asset can serve", () => {
125 const images = Array.from({ length: MAX_TOOL_OUTPUT_IMAGES + 2 }, () => ({
126 type: "image",
127 data: PNG_BASE64,
128 mimeType: "image/png",
129 }));
130
131 const output = outputOf(stripUnservedToolOutputImageBytes(toolItem(images)));
132
133 expect(Array.isArray(output) ? output.slice(0, MAX_TOOL_OUTPUT_IMAGES) : null).toEqual(
134 images.slice(0, MAX_TOOL_OUTPUT_IMAGES),
135 );
136 expect(Array.isArray(output) ? output.slice(MAX_TOOL_OUTPUT_IMAGES) : null).toEqual([
137 { type: "image", mimeType: "image/png", sizeBytes: PNG_BYTES },
138 { type: "image", mimeType: "image/png", sizeBytes: PNG_BYTES },
139 ]);
140 });
141
142 it("strips a served-position image larger than an asset may serve", () => {
143 const oversized = "A".repeat(MAX_TOOL_OUTPUT_IMAGE_BASE64_LENGTH + 4);
144 const servable = { type: "image", data: PNG_BASE64, mimeType: "image/png" };
145 const item = toolItem({
146 content: [
147 { type: "image", source: { type: "base64", media_type: "image/png", data: oversized } },
148 servable,
149 ],
150 });
151
152 const output = outputOf(stripUnservedToolOutputImageBytes(item)) as {
153 content: ReadonlyArray<unknown>;
154 };
155
156 expect(output.content).toEqual([
157 {
158 type: "image",
159 source: {
160 type: "base64",
161 media_type: "image/png",
162 sizeBytes: ((MAX_TOOL_OUTPUT_IMAGE_BASE64_LENGTH + 4) / 4) * 3,
163 },
164 },
165 servable,
166 ]);
167 expect(output.content[1]).toBe(servable);
168 });
169
170 it("keeps image fields whose value is not a base64 body", () => {
171 const item = toolItem({
172 nested: {
173 byId: { type: "image", data: "asset-id-42" },
174 svg: {
175 mimeType: "image/svg+xml",
176 data: `<svg xmlns='http://www.w3.org/2000/svg'>${"<circle r='4'/>".repeat(8)}</svg>`,
177 },
178 dataUrl: { type: "image", data: `data:image/png;base64,${PNG_BASE64}` },
179 remote: { type: "image", source: { type: "url", url: "https://example.test/shot.png" } },
180 tiny: { type: "image", data: "AA==", mimeType: "image/png" },
181 notImage: { mimeType: "application/pdf", data: PNG_BASE64 },
182 },
183 });
184
185 expect(stripUnservedToolOutputImageBytes(item)).toBe(item);
186 });
187
188 it("measures the size instead of trusting one the provider supplied", () => {
189 const item = toolItem({
190 type: "image",
191 file: { base64: PNG_BASE64, type: "image/png", sizeBytes: 0 },
192 });
193
194 expect(outputOf(stripUnservedToolOutputImageBytes(item))).toEqual({
195 type: "image",
196 file: { type: "image/png", sizeBytes: PNG_BYTES },
197 });
198 });
199
200 it("returns items without image bytes by reference", () => {
201 const nested = { files: [{ path: "src/app.ts" }] };
202 const item = toolItem({ stdout: "data: 42", data: PNG_BASE64, nested });
203 const command: OrchestrationV2TurnItem = {
204 ...itemBase,
205 type: "command_execution",
206 input: "cat shot.png | base64",
207 output: PNG_BASE64,
208 };
209
210 const stripped = stripUnservedToolOutputImageBytes(item);
211
212 expect(stripped).toBe(item);
213 expect(outputOf(stripped)).toBe(outputOf(item));
214 expect(stripUnservedToolOutputImageBytes(command)).toBe(command);
215 expect(stripUnservedToolOutputImageBytes(toolItem("plain text output"))).toEqual(
216 toolItem("plain text output"),
217 );
218 });
219
220 it("does not mutate the provider's item and shares untouched siblings", () => {
221 const untouched = { type: "text", text: "captured" };
222 const output = {
223 error: "partial result",
224 result: [untouched, { type: "image", data: PNG_BASE64, mimeType: "image/png" }],
225 };
226 const item = toolItem(output);
227 const snapshot = structuredClone(output);
228
229 const stripped = outputOf(stripUnservedToolOutputImageBytes(item)) as typeof output;
230
231 expect(outputOf(item)).toEqual(snapshot);
232 expect(stripped.result[0]).toBe(untouched);
233 });
234});