1import { ThreadId, TurnItemId } from "@t3tools/contracts";
2import * as DateTime from "effect/DateTime";
3import { describe, expect, it } from "vite-plus/test";
4
5import { turnItemOutputImages, turnItemOutputText } from "./itemDetail.ts";
6
7const screenshot = {
8 id: TurnItemId.make("tool-screenshot"),
9 type: "dynamic_tool" as const,
10 threadId: ThreadId.make("thread-1"),
11 runId: null,
12 nodeId: null,
13 providerThreadId: null,
14 providerTurnId: null,
15 nativeItemRef: null,
16 parentItemId: null,
17 ordinal: 1,
18 status: "completed" as const,
19 title: null,
20 toolName: "mcp__t3-code__device_screenshot",
21 input: { deviceId: "phone" },
22 // What a detail read returns: the image's position without its bytes.
23 output: {
24 content: [
25 { type: "text", text: "Captured the home screen." },
26 { type: "image", mimeType: "image/png" },
27 ],
28 },
29 startedAt: DateTime.makeUnsafe("2026-10-05T00:00:00.000Z"),
30 completedAt: DateTime.makeUnsafe("2026-10-05T00:00:01.000Z"),
31 updatedAt: DateTime.makeUnsafe("2026-10-05T00:00:01.000Z"),
32};
33
34describe("tool output images", () => {
35 it("shows a screenshot as an image asset, not as text", () => {
36 expect(turnItemOutputImages(screenshot)).toEqual([
37 {
38 _tag: "tool-output-image",
39 threadId: screenshot.threadId,
40 itemId: screenshot.id,
41 index: 0,
42 },
43 ]);
44 expect(turnItemOutputText(screenshot)).toBe("Captured the home screen.");
45 expect(
46 turnItemOutputText({ ...screenshot, output: { content: [screenshot.output.content[1]] } }),
47 ).toBeNull();
48 });
49
50 it("keeps a placeholder for images it cannot show", () => {
51 const output = { content: [{ type: "image", mimeType: "image/svg+xml" }] };
52 expect(turnItemOutputImages({ ...screenshot, output })).toEqual([]);
53 expect(turnItemOutputText({ ...screenshot, output })).toBe("[image]");
54 });
55
56 it("omits Claude Read's image placeholder when the file has a preview", () => {
57 const output = {
58 type: "image",
59 file: { filePath: "/tmp/blk.png", type: "image/png", sizeBytes: 1024 },
60 };
61 const item = { ...screenshot, toolName: "Read", viewedImagePath: "/tmp/blk.png", output };
62
63 expect(turnItemOutputImages(item)).toEqual([]);
64 expect(turnItemOutputText(item)).toBeNull();
65 expect(turnItemOutputText({ ...item, viewedImagePath: undefined })).toBe("[image]");
66 });
67
68 it("keeps accompanying text and literal image markers when the file has a preview", () => {
69 const output = {
70 content: [
71 { type: "text", text: "Read /tmp/blk.png. Literal marker: [image]" },
72 screenshot.output.content[1],
73 ],
74 };
75 expect(turnItemOutputText({ ...screenshot, viewedImagePath: "/tmp/blk.png", output })).toBe(
76 "Read /tmp/blk.png. Literal marker: [image]",
77 );
78 });
79
80 it("keeps placeholders for unserved images in a list despite a file preview", () => {
81 const blocks = [
82 { type: "text", text: "Read /tmp/blk.png." },
83 { type: "image", file: { type: "image/png", sizeBytes: 1024 } },
84 screenshot.output.content[1],
85 { type: "image", mimeType: "image/svg+xml" },
86 ];
87 for (const output of [blocks, { content: blocks }, { content: blocks, isError: false }]) {
88 const item = { ...screenshot, viewedImagePath: "/tmp/blk.png", output };
89 expect(turnItemOutputText(item)).toBe("Read /tmp/blk.png.\n[image]\n[image]");
90 expect(turnItemOutputImages(item)).toHaveLength(1);
91 }
92 });
93});