@@ -18,6 +18,7 @@ import {
1818 Attachment ,
1919 deepCopyEvent ,
2020 renderMessage ,
21+ renderMessageImpl ,
2122} from "./logger" ;
2223import {
2324 parseTemplateFormat ,
@@ -62,7 +63,15 @@ test("renderMessage with file content parts", () => {
6263 ] ,
6364 } ;
6465
65- const rendered = renderMessage (
66+ const variables = {
67+ item : "document" ,
68+ image_url : "https://example.com/image.png" ,
69+ file_data : "base64data" ,
70+ file_id : "file-456" ,
71+ filename : "report.pdf" ,
72+ } ;
73+
74+ const rendered = renderMessageImpl (
6675 ( template ) =>
6776 template
6877 . replace ( "{{item}}" , "document" )
@@ -71,6 +80,7 @@ test("renderMessage with file content parts", () => {
7180 . replace ( "{{file_id}}" , "file-456" )
7281 . replace ( "{{filename}}" , "report.pdf" ) ,
7382 message ,
83+ variables ,
7484 ) ;
7585
7686 expect ( rendered . content ) . toEqual ( [
@@ -95,6 +105,293 @@ test("renderMessage with file content parts", () => {
95105 ] ) ;
96106} ) ;
97107
108+ test ( "renderMessage expands attachment array in image_url parts" , ( ) => {
109+ const message = {
110+ role : "user" as const ,
111+ content : [
112+ {
113+ type : "image_url" as const ,
114+ image_url : { url : "{{images}}" } ,
115+ } ,
116+ ] ,
117+ } ;
118+
119+ const variables = {
120+ images : [ "https://example.com/img1.jpg" , "https://example.com/img2.jpg" ] ,
121+ } ;
122+
123+ const rendered = renderMessageImpl (
124+ ( template ) => template , // Template rendering shouldn't happen for attachment arrays
125+ message ,
126+ variables ,
127+ ) ;
128+
129+ expect ( rendered . content ) . toEqual ( [
130+ {
131+ type : "image_url" ,
132+ image_url : {
133+ url : "https://example.com/img1.jpg" ,
134+ } ,
135+ } ,
136+ {
137+ type : "image_url" ,
138+ image_url : {
139+ url : "https://example.com/img2.jpg" ,
140+ } ,
141+ } ,
142+ ] ) ;
143+ } ) ;
144+
145+ test ( "renderMessage expands inline attachment array in image_url parts" , ( ) => {
146+ const message = {
147+ role : "user" as const ,
148+ content : [
149+ {
150+ type : "image_url" as const ,
151+ image_url : { url : "{{images}}" } ,
152+ } ,
153+ ] ,
154+ } ;
155+
156+ const variables = {
157+ images : [
158+ {
159+ type : "inline_attachment" ,
160+ src : "data:image/png;base64,abc" ,
161+ content_type : "image/png" ,
162+ } ,
163+ {
164+ type : "inline_attachment" ,
165+ src : "data:image/jpeg;base64,def" ,
166+ content_type : "image/jpeg" ,
167+ } ,
168+ ] ,
169+ } ;
170+
171+ const rendered = renderMessageImpl (
172+ ( template ) => template ,
173+ message ,
174+ variables ,
175+ ) ;
176+
177+ expect ( rendered . content ) . toEqual ( [
178+ {
179+ type : "image_url" ,
180+ image_url : {
181+ url : {
182+ type : "inline_attachment" ,
183+ src : "data:image/png;base64,abc" ,
184+ content_type : "image/png" ,
185+ } ,
186+ } ,
187+ } ,
188+ {
189+ type : "image_url" ,
190+ image_url : {
191+ url : {
192+ type : "inline_attachment" ,
193+ src : "data:image/jpeg;base64,def" ,
194+ content_type : "image/jpeg" ,
195+ } ,
196+ } ,
197+ } ,
198+ ] ) ;
199+ } ) ;
200+
201+ test ( "renderMessage does NOT expand mixed content (text + variable)" , ( ) => {
202+ const message = {
203+ role : "user" as const ,
204+ content : "Look at {{images}}" ,
205+ } ;
206+
207+ const variables = {
208+ images : [ "https://example.com/img1.jpg" , "https://example.com/img2.jpg" ] ,
209+ } ;
210+
211+ const rendered = renderMessageImpl (
212+ ( template ) => template . replace ( "{{images}}" , "[array]" ) ,
213+ message ,
214+ variables ,
215+ ) ;
216+
217+ // Mixed content is not expanded - just rendered normally
218+ expect ( rendered . content ) . toBe ( "Look at [array]" ) ;
219+ } ) ;
220+
221+ test ( "renderMessage expands nested attachment arrays in image_url parts" , ( ) => {
222+ const message = {
223+ role : "user" as const ,
224+ content : [
225+ {
226+ type : "image_url" as const ,
227+ image_url : { url : "{{data.images}}" } ,
228+ } ,
229+ ] ,
230+ } ;
231+
232+ const variables = {
233+ data : {
234+ images : [ "https://example.com/img1.jpg" , "https://example.com/img2.jpg" ] ,
235+ } ,
236+ } ;
237+
238+ const rendered = renderMessageImpl (
239+ ( template ) => template , // Template rendering shouldn't happen
240+ message ,
241+ variables ,
242+ ) ;
243+
244+ expect ( rendered . content ) . toEqual ( [
245+ {
246+ type : "image_url" ,
247+ image_url : {
248+ url : "https://example.com/img1.jpg" ,
249+ } ,
250+ } ,
251+ {
252+ type : "image_url" ,
253+ image_url : {
254+ url : "https://example.com/img2.jpg" ,
255+ } ,
256+ } ,
257+ ] ) ;
258+ } ) ;
259+
260+ test ( "renderMessage expands deeply nested attachment arrays in image_url parts" , ( ) => {
261+ const message = {
262+ role : "user" as const ,
263+ content : [
264+ {
265+ type : "image_url" as const ,
266+ image_url : { url : "{{user.profile.images}}" } ,
267+ } ,
268+ ] ,
269+ } ;
270+
271+ const variables = {
272+ user : {
273+ profile : {
274+ images : [
275+ {
276+ type : "inline_attachment" ,
277+ src : "data:image/png;base64,abc" ,
278+ content_type : "image/png" ,
279+ } ,
280+ {
281+ type : "inline_attachment" ,
282+ src : "data:image/jpeg;base64,def" ,
283+ content_type : "image/jpeg" ,
284+ } ,
285+ ] ,
286+ } ,
287+ } ,
288+ } ;
289+
290+ const rendered = renderMessageImpl (
291+ ( template ) => template ,
292+ message ,
293+ variables ,
294+ ) ;
295+
296+ expect ( rendered . content ) . toEqual ( [
297+ {
298+ type : "image_url" ,
299+ image_url : {
300+ url : {
301+ type : "inline_attachment" ,
302+ src : "data:image/png;base64,abc" ,
303+ content_type : "image/png" ,
304+ } ,
305+ } ,
306+ } ,
307+ {
308+ type : "image_url" ,
309+ image_url : {
310+ url : {
311+ type : "inline_attachment" ,
312+ src : "data:image/jpeg;base64,def" ,
313+ content_type : "image/jpeg" ,
314+ } ,
315+ } ,
316+ } ,
317+ ] ) ;
318+ } ) ;
319+
320+ test ( "renderMessage handles single image_url (no array)" , ( ) => {
321+ const message = {
322+ role : "user" as const ,
323+ content : [
324+ {
325+ type : "image_url" as const ,
326+ image_url : {
327+ url : "{{image}}" ,
328+ } ,
329+ } ,
330+ ] ,
331+ } ;
332+
333+ const variables = {
334+ image : "https://example.com/single.jpg" ,
335+ } ;
336+
337+ const rendered = renderMessageImpl (
338+ ( template ) =>
339+ template . replace ( "{{image}}" , "https://example.com/single.jpg" ) ,
340+ message ,
341+ variables ,
342+ ) ;
343+
344+ expect ( rendered . content ) . toEqual ( [
345+ {
346+ type : "image_url" ,
347+ image_url : {
348+ url : "https://example.com/single.jpg" ,
349+ } ,
350+ } ,
351+ ] ) ;
352+ } ) ;
353+
354+ test ( "renderMessage expands attachment arrays in structured content" , ( ) => {
355+ // This tests the case where content is already an array with structured parts,
356+ // and one part has a template variable for an attachment array
357+ const message = {
358+ role : "user" as const ,
359+ content : [
360+ { type : "text" as const , text : "Describe these images" } ,
361+ { type : "image_url" as const , image_url : { url : "{{attachments}}" } } ,
362+ ] ,
363+ } ;
364+
365+ const variables = {
366+ attachments : [
367+ "https://example.com/img1.jpg" ,
368+ "https://example.com/img2.jpg" ,
369+ ] ,
370+ } ;
371+
372+ const rendered = renderMessageImpl (
373+ ( template ) => template ,
374+ message ,
375+ variables ,
376+ ) ;
377+
378+ // Should expand {{attachments}} into multiple image_url parts
379+ expect ( rendered . content ) . toEqual ( [
380+ {
381+ type : "text" ,
382+ text : "Describe these images" ,
383+ } ,
384+ {
385+ type : "image_url" ,
386+ image_url : { url : "https://example.com/img1.jpg" } ,
387+ } ,
388+ {
389+ type : "image_url" ,
390+ image_url : { url : "https://example.com/img2.jpg" } ,
391+ } ,
392+ ] ) ;
393+ } ) ;
394+
98395test ( "verify MemoryBackgroundLogger intercepts logs" , async ( ) => {
99396 // Log to memory for the tests.
100397 _exportsForTestingOnly . simulateLoginForTests ( ) ;
0 commit comments