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");
14const PADDED_BASE64 = Buffer.alloc(PNG_BYTES + 1, 9).toString("base64");
15
16const now = DateTime.makeUnsafe("2026-10-06T00:00:00.000Z");
17
18const itemBase = {
19 id: TurnItemId.make("turn-item:tool"),
20 threadId: ThreadId.make("thread:tool"),
21 runId: null,
22 nodeId: NodeId.make("node:tool"),
23 providerThreadId: null,
24 providerTurnId: null,
25 nativeItemRef: null,
26 parentItemId: null,
27 ordinal: 1,
28 status: "completed",
29 title: null,
30 startedAt: now,
31 completedAt: now,
32 updatedAt: now,
33} as const;
34
35function toolItem(output: unknown, toolName = "Read"): OrchestrationV2TurnItem {
36 return {
37 ...itemBase,
38 type: "dynamic_tool",
39 toolName,
40 input: { file_path: "/tmp/shot.png" },
41 output,
42 };
43}
44
45function outputOf(item: OrchestrationV2TurnItem): unknown {
46 return item.type === "dynamic_tool" ? item.output : undefined;
47}
48
49describe("stripUnservedToolOutputImageBytes", () => {
50 it("replaces the base64 of Claude's structured Read result with its measured size", () => {
51 const item = toolItem({
52 type: "image",
53 file: {
54 base64: PNG_BASE64,
55 type: "image/png",
56 originalSize: PNG_BYTES,
57 dimensions: { originalWidth: 180, originalHeight: 180 },
58 },
59 });
60
61 expect(outputOf(stripUnservedToolOutputImageBytes(item))).toEqual({
62 type: "image",
63 file: {
64 type: "image/png",
65 originalSize: PNG_BYTES,
66 dimensions: { originalWidth: 180, originalHeight: 180 },
67 sizeBytes: PNG_BYTES,
68 },
69 });
70 });
71
72 it("strips ACP image shapes: Grok's native ImageContent and an MCP result behind an error", () => {
73 const item = toolItem({
74 rawOutput: { type: "ReadFile", ImageContent: { data: PNG_BASE64, mime_type: "image/png" } },
75 failed: {
76 error: "partial result",
77 result: [{ type: "image", data: PADDED_BASE64, mimeType: "image/png" }],
78 },
79 });
80
81 expect(outputOf(stripUnservedToolOutputImageBytes(item))).toEqual({
82 rawOutput: {
83 type: "ReadFile",
84 ImageContent: { mime_type: "image/png", sizeBytes: PNG_BYTES },
85 },
86 failed: {
87 error: "partial result",
88 result: [{ type: "image", mimeType: "image/png", sizeBytes: PNG_BYTES + 1 }],
89 },
90 });
91 });
92
93 // regression: #16652 stop storing tool image bytes no client reads
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 // regression: #16652 stop storing tool image bytes no client reads
189 it("measures the size instead of trusting one the provider supplied", () => {
190 const item = toolItem({
191 type: "image",
192 file: { base64: PNG_BASE64, type: "image/png", sizeBytes: 0 },
193 });
194
195 expect(outputOf(stripUnservedToolOutputImageBytes(item))).toEqual({
196 type: "image",
197 file: { type: "image/png", sizeBytes: PNG_BYTES },
198 });
199 });
200
201 it("does not mutate the provider's item and shares untouched siblings", () => {
202 const untouched = { type: "text", text: "captured" };
203 const output = {
204 error: "partial result",
205 result: [untouched, { type: "image", data: PNG_BASE64, mimeType: "image/png" }],
206 };
207 const item = toolItem(output);
208 const snapshot = structuredClone(output);
209
210 const stripped = outputOf(stripUnservedToolOutputImageBytes(item)) as typeof output;
211
212 expect(outputOf(item)).toEqual(snapshot);
213 expect(stripped.result[0]).toBe(untouched);
214 });
215});