Skip to content
Source
Sign inSign up

itemDetail.test.ts

115 lines3.9 KB

1import { describe, expect, it } from "vite-plus/test";
2import { ThreadId, TurnItemId, type OrchestrationV2TurnItem } from "@t3tools/contracts";
3import * as DateTime from "effect/DateTime";
4​
5import { isJsonToolText, turnItemOutputImages, turnItemOutputText } from "./itemDetail.ts";
6​
7describe("isJsonToolText", () => {
8 it.each([
9 ['{"screenshot":{"mimeType":"image/png"}}', true],
10 ['[{"threadId":"thread-1"}]', true],
11 ['{\n\n "status": "completed"\n}', true],
12 ['{\n "count": 1\n}\n\n{\n "count": 2\n}', true],
13 ["false", true],
14 ["null", true],
15 ["42", true],
16 ['"completed"', true],
17 ['{"partial":', false],
18 ['{"count":1}\n\nFinished.', false],
19 ["No output.", false],
20 ["", false],
21 ] as const)("classifies %j as JSON: %s", (text, expected) => {
22 expect(isJsonToolText(text)).toBe(expected);
23 });
24});
25​
26function toolItem(output: unknown): OrchestrationV2TurnItem {
27 return {
28 id: TurnItemId.make("images"),
29 threadId: ThreadId.make("thread"),
30 runId: null,
31 nodeId: null,
32 providerThreadId: null,
33 providerTurnId: null,
34 nativeItemRef: null,
35 parentItemId: null,
36 ordinal: 1,
37 status: "completed",
38 title: null,
39 startedAt: null,
40 completedAt: null,
41 updatedAt: DateTime.makeUnsafe("2026-10-01T00:00:00Z"),
42 type: "dynamic_tool",
43 toolName: "screenshots",
44 input: {},
45 output,
46 };
48​
49describe("tool image output parsing", () => {
50 it.each(["image-only", "summary", "json"] as const)(
51 "keeps structured fields beside %s content",
52 (shape) => {
53 const content = [
54 { type: "image", mimeType: "image/png" },
55 ...(shape === "image-only"
56 ? []
57 : [{ type: "text", text: shape === "summary" ? "Sunny" : '{"temp":21}' }]),
58 ];
59 for (const structuredContent of [
60 { temp: 21 },
61 { type: "text", text: "Sunny", temp: 21 },
62 { content: [{ type: "text", text: "Sunny" }], temp: 21 },
63 ]) {
64 const item = toolItem({ structuredContent, content });
65 expect(turnItemOutputText(item)).toBe(JSON.stringify(structuredContent, null, 2));
66 expect(turnItemOutputImages(item)).toHaveLength(1);
67 }
68 for (const structuredContent of [null, {}, [], ""]) {
69 const item = toolItem({ structuredContent, content: [{ type: "text", text: "Sunny" }] });
70 expect(turnItemOutputText(item)).toBe("Sunny");
71 }
72 expect(
73 turnItemOutputText(
74 toolItem({
75 content: [{ type: "text", text: "File contents" }],
76 structuredContent: { path: "read.ts", ready: true },
77 }),
78 ),
79 ).toBe("File contents");
80 },
81 );
82 it.each(["array", "content", "structured"] as const)(
83 "keeps markers for images beyond the served positions in %s output",
84 (shape) => {
85 // Detail responses retain image metadata after the server strips image bytes.
86 const image = { type: "image", mimeType: "image/png" };
87 const blocks = Array.from({ length: 8 }, () => ({ ...image }));
88 const wrap = (content: unknown[]) =>
89 shape === "array"
90 ? content
91 : shape === "structured"
92 ? { content, structuredContent: { temp: 21 } }
93 : { content };
94 const withinLimit = toolItem(wrap(blocks));
95 expect(turnItemOutputText(withinLimit)).toBe(
96 shape === "structured" ? '{\n "temp": 21\n}' : null,
97 );
98 expect(turnItemOutputImages(withinLimit)).toHaveLength(8);
99​
100 const overflow = toolItem(
101 wrap([
102 ...blocks,
103 { type: "text", text: "Two additional screenshots:" },
104 { ...image, sizeBytes: 100 },
105 { type: "image", source: { type: "base64", media_type: "image/jpeg" } },
106 ]),
107 );
108 expect(turnItemOutputImages(overflow)).toHaveLength(8);
109 expect(turnItemOutputText(overflow)).toBe(
110 (shape === "structured" ? '{\n "temp": 21\n}' : "Two additional screenshots:") +
111 "\n[image]\n[image]",
112 );
113 },
114 );
115});