1package anthropic
2
3import (
4 "errors"
5 "testing"
6
7 "charm.land/fantasy"
8 "github.com/stretchr/testify/require"
9)
10
11func TestToPrompt_DropsEmptyMessages(t *testing.T) {
12 t.Parallel()
13
14 t.Run("should drop assistant messages with only reasoning content", func(t *testing.T) {
15 t.Parallel()
16
17 prompt := fantasy.Prompt{
18 {
19 Role: fantasy.MessageRoleUser,
20 Content: []fantasy.MessagePart{
21 fantasy.TextPart{Text: "Hello"},
22 },
23 },
24 {
25 Role: fantasy.MessageRoleAssistant,
26 Content: []fantasy.MessagePart{
27 fantasy.ReasoningPart{
28 Text: "Let me think about this...",
29 ProviderOptions: fantasy.ProviderOptions{
30 Name: &ReasoningOptionMetadata{
31 Signature: "abc123",
32 },
33 },
34 },
35 },
36 },
37 }
38
39 systemBlocks, messages, warnings := toPrompt(prompt, true)
40
41 require.Empty(t, systemBlocks)
42 require.Len(t, messages, 1, "should only have user message, assistant message should be dropped")
43 require.Len(t, warnings, 1)
44 require.Equal(t, fantasy.CallWarningTypeOther, warnings[0].Type)
45 require.Contains(t, warnings[0].Message, "dropping empty assistant message")
46 require.Contains(t, warnings[0].Message, "neither user-facing content nor tool calls")
47 })
48
49 t.Run("should drop assistant reasoning when sendReasoning disabled", func(t *testing.T) {
50 t.Parallel()
51
52 prompt := fantasy.Prompt{
53 {
54 Role: fantasy.MessageRoleUser,
55 Content: []fantasy.MessagePart{
56 fantasy.TextPart{Text: "Hello"},
57 },
58 },
59 {
60 Role: fantasy.MessageRoleAssistant,
61 Content: []fantasy.MessagePart{
62 fantasy.ReasoningPart{
63 Text: "Let me think about this...",
64 ProviderOptions: fantasy.ProviderOptions{
65 Name: &ReasoningOptionMetadata{
66 Signature: "def456",
67 },
68 },
69 },
70 },
71 },
72 }
73
74 systemBlocks, messages, warnings := toPrompt(prompt, false)
75
76 require.Empty(t, systemBlocks)
77 require.Len(t, messages, 1, "should only have user message, assistant message should be dropped")
78 require.Len(t, warnings, 2)
79 require.Equal(t, fantasy.CallWarningTypeOther, warnings[0].Type)
80 require.Contains(t, warnings[0].Message, "sending reasoning content is disabled")
81 require.Equal(t, fantasy.CallWarningTypeOther, warnings[1].Type)
82 require.Contains(t, warnings[1].Message, "dropping empty assistant message")
83 })
84
85 t.Run("should drop truly empty assistant messages", func(t *testing.T) {
86 t.Parallel()
87
88 prompt := fantasy.Prompt{
89 {
90 Role: fantasy.MessageRoleUser,
91 Content: []fantasy.MessagePart{
92 fantasy.TextPart{Text: "Hello"},
93 },
94 },
95 {
96 Role: fantasy.MessageRoleAssistant,
97 Content: []fantasy.MessagePart{},
98 },
99 }
100
101 systemBlocks, messages, warnings := toPrompt(prompt, true)
102
103 require.Empty(t, systemBlocks)
104 require.Len(t, messages, 1, "should only have user message")
105 require.Len(t, warnings, 1)
106 require.Equal(t, fantasy.CallWarningTypeOther, warnings[0].Type)
107 require.Contains(t, warnings[0].Message, "dropping empty assistant message")
108 })
109
110 t.Run("should keep assistant messages with text content", func(t *testing.T) {
111 t.Parallel()
112
113 prompt := fantasy.Prompt{
114 {
115 Role: fantasy.MessageRoleUser,
116 Content: []fantasy.MessagePart{
117 fantasy.TextPart{Text: "Hello"},
118 },
119 },
120 {
121 Role: fantasy.MessageRoleAssistant,
122 Content: []fantasy.MessagePart{
123 fantasy.TextPart{Text: "Hi there!"},
124 },
125 },
126 }
127
128 systemBlocks, messages, warnings := toPrompt(prompt, true)
129
130 require.Empty(t, systemBlocks)
131 require.Len(t, messages, 2, "should have both user and assistant messages")
132 require.Empty(t, warnings)
133 })
134
135 t.Run("should keep assistant messages with tool calls", func(t *testing.T) {
136 t.Parallel()
137
138 prompt := fantasy.Prompt{
139 {
140 Role: fantasy.MessageRoleUser,
141 Content: []fantasy.MessagePart{
142 fantasy.TextPart{Text: "What's the weather?"},
143 },
144 },
145 {
146 Role: fantasy.MessageRoleAssistant,
147 Content: []fantasy.MessagePart{
148 fantasy.ToolCallPart{
149 ToolCallID: "call_123",
150 ToolName: "get_weather",
151 Input: `{"location":"NYC"}`,
152 },
153 },
154 },
155 }
156
157 systemBlocks, messages, warnings := toPrompt(prompt, true)
158
159 require.Empty(t, systemBlocks)
160 require.Len(t, messages, 2, "should have both user and assistant messages")
161 require.Empty(t, warnings)
162 })
163
164 t.Run("should drop assistant messages with invalid tool input", func(t *testing.T) {
165 t.Parallel()
166
167 prompt := fantasy.Prompt{
168 {
169 Role: fantasy.MessageRoleUser,
170 Content: []fantasy.MessagePart{
171 fantasy.TextPart{Text: "Hi"},
172 },
173 },
174 {
175 Role: fantasy.MessageRoleAssistant,
176 Content: []fantasy.MessagePart{
177 fantasy.ToolCallPart{
178 ToolCallID: "call_123",
179 ToolName: "get_weather",
180 Input: "{not-json",
181 },
182 },
183 },
184 }
185
186 systemBlocks, messages, warnings := toPrompt(prompt, true)
187
188 require.Empty(t, systemBlocks)
189 require.Len(t, messages, 1, "should only have user message")
190 require.Len(t, warnings, 1)
191 require.Equal(t, fantasy.CallWarningTypeOther, warnings[0].Type)
192 require.Contains(t, warnings[0].Message, "dropping empty assistant message")
193 })
194
195 t.Run("should keep assistant messages with reasoning and text", func(t *testing.T) {
196 t.Parallel()
197
198 prompt := fantasy.Prompt{
199 {
200 Role: fantasy.MessageRoleUser,
201 Content: []fantasy.MessagePart{
202 fantasy.TextPart{Text: "Hello"},
203 },
204 },
205 {
206 Role: fantasy.MessageRoleAssistant,
207 Content: []fantasy.MessagePart{
208 fantasy.ReasoningPart{
209 Text: "Let me think...",
210 ProviderOptions: fantasy.ProviderOptions{
211 Name: &ReasoningOptionMetadata{
212 Signature: "abc123",
213 },
214 },
215 },
216 fantasy.TextPart{Text: "Hi there!"},
217 },
218 },
219 }
220
221 systemBlocks, messages, warnings := toPrompt(prompt, true)
222
223 require.Empty(t, systemBlocks)
224 require.Len(t, messages, 2, "should have both user and assistant messages")
225 require.Empty(t, warnings)
226 })
227
228 t.Run("should keep user messages with image content", func(t *testing.T) {
229 t.Parallel()
230
231 prompt := fantasy.Prompt{
232 {
233 Role: fantasy.MessageRoleUser,
234 Content: []fantasy.MessagePart{
235 fantasy.FilePart{
236 Data: []byte{0x01, 0x02, 0x03},
237 MediaType: "image/png",
238 },
239 },
240 },
241 }
242
243 systemBlocks, messages, warnings := toPrompt(prompt, true)
244
245 require.Empty(t, systemBlocks)
246 require.Len(t, messages, 1)
247 require.Empty(t, warnings)
248 })
249
250 t.Run("should drop user messages without visible content", func(t *testing.T) {
251 t.Parallel()
252
253 prompt := fantasy.Prompt{
254 {
255 Role: fantasy.MessageRoleUser,
256 Content: []fantasy.MessagePart{
257 fantasy.FilePart{
258 Data: []byte("not supported"),
259 MediaType: "application/pdf",
260 },
261 },
262 },
263 }
264
265 systemBlocks, messages, warnings := toPrompt(prompt, true)
266
267 require.Empty(t, systemBlocks)
268 require.Empty(t, messages)
269 require.Len(t, warnings, 1)
270 require.Equal(t, fantasy.CallWarningTypeOther, warnings[0].Type)
271 require.Contains(t, warnings[0].Message, "dropping empty user message")
272 require.Contains(t, warnings[0].Message, "neither user-facing content nor tool results")
273 })
274
275 t.Run("should keep user messages with tool results", func(t *testing.T) {
276 t.Parallel()
277
278 prompt := fantasy.Prompt{
279 {
280 Role: fantasy.MessageRoleTool,
281 Content: []fantasy.MessagePart{
282 fantasy.ToolResultPart{
283 ToolCallID: "call_123",
284 Output: fantasy.ToolResultOutputContentText{Text: "done"},
285 },
286 },
287 },
288 }
289
290 systemBlocks, messages, warnings := toPrompt(prompt, true)
291
292 require.Empty(t, systemBlocks)
293 require.Len(t, messages, 1)
294 require.Empty(t, warnings)
295 })
296
297 t.Run("should keep user messages with tool error results", func(t *testing.T) {
298 t.Parallel()
299
300 prompt := fantasy.Prompt{
301 {
302 Role: fantasy.MessageRoleTool,
303 Content: []fantasy.MessagePart{
304 fantasy.ToolResultPart{
305 ToolCallID: "call_456",
306 Output: fantasy.ToolResultOutputContentError{Error: errors.New("boom")},
307 },
308 },
309 },
310 }
311
312 systemBlocks, messages, warnings := toPrompt(prompt, true)
313
314 require.Empty(t, systemBlocks)
315 require.Len(t, messages, 1)
316 require.Empty(t, warnings)
317 })
318
319 t.Run("should keep user messages with tool media results", func(t *testing.T) {
320 t.Parallel()
321
322 prompt := fantasy.Prompt{
323 {
324 Role: fantasy.MessageRoleTool,
325 Content: []fantasy.MessagePart{
326 fantasy.ToolResultPart{
327 ToolCallID: "call_789",
328 Output: fantasy.ToolResultOutputContentMedia{
329 Data: "AQID",
330 MediaType: "image/png",
331 },
332 },
333 },
334 },
335 }
336
337 systemBlocks, messages, warnings := toPrompt(prompt, true)
338
339 require.Empty(t, systemBlocks)
340 require.Len(t, messages, 1)
341 require.Empty(t, warnings)
342 })
343}