@@ -4,21 +4,25 @@ import { processImagesForMessage } from '../image-processor'
44
55import type { PendingImageAttachment } from '../../state/chat-store'
66
7- const createPendingImage = ( path : string ) : PendingImageAttachment => ( {
7+ const createPendingImage = ( path : string , processedImage ?: { base64 : string ; mediaType : string } ) : PendingImageAttachment => ( {
88 kind : 'image' ,
99 path,
1010 filename : path . split ( '/' ) . pop ( ) ?? 'image.png' ,
1111 status : 'ready' ,
12+ ...( processedImage && { processedImage } ) ,
1213} )
1314
1415describe ( 'processImagesForMessage' , ( ) => {
15- test ( 'deduplicates image paths and returns message content' , async ( ) => {
16- const pendingImages = [ createPendingImage ( '/tmp/pic.png' ) ]
16+ test ( 'uses pre-processed image data from pendingImages without re-reading from disk' , async ( ) => {
17+ const pendingImages = [ createPendingImage ( '/tmp/pic.png' , {
18+ base64 : 'pre-processed-base64-data' ,
19+ mediaType : 'image/png' ,
20+ } ) ]
1721 const processor = mock ( async ( ) => ( {
1822 success : true ,
1923 imagePart : {
2024 type : 'image' as const ,
21- image : 'base64-data' ,
25+ image : 'disk- base64-data' ,
2226 mediaType : 'image/png' ,
2327 } ,
2428 } ) )
@@ -30,24 +34,123 @@ describe('processImagesForMessage', () => {
3034 processor : processor as any ,
3135 } )
3236
33- expect ( processor ) . toHaveBeenCalledTimes ( 1 )
37+ // Should NOT call processor since we have pre-processed data
38+ expect ( processor ) . not . toHaveBeenCalled ( )
3439 expect ( result . attachments ) . toHaveLength ( 1 )
3540 expect ( result . messageContent ?. [ 0 ] ) . toMatchObject ( {
3641 type : 'image' ,
37- image : 'base64-data' ,
42+ image : 'pre-processed-base64-data' ,
43+ mediaType : 'image/png' ,
44+ } )
45+ } )
46+
47+ test ( 'processes inline image paths that are not in pendingImages' , async ( ) => {
48+ const pendingImages = [ createPendingImage ( '/tmp/pic.png' , {
49+ base64 : 'pre-processed-base64-data' ,
50+ mediaType : 'image/png' ,
51+ } ) ]
52+ const processor = mock ( async ( ) => ( {
53+ success : true ,
54+ imagePart : {
55+ type : 'image' as const ,
56+ image : 'inline-base64-data' ,
57+ mediaType : 'image/jpeg' ,
58+ } ,
59+ } ) )
60+
61+ const result = await processImagesForMessage ( {
62+ content : 'Here is another image @/tmp/other.jpg' ,
63+ pendingImages,
64+ projectRoot : '/repo' ,
65+ processor : processor as any ,
66+ } )
67+
68+ // Should call processor only for the inline path
69+ expect ( processor ) . toHaveBeenCalledTimes ( 1 )
70+ expect ( processor ) . toHaveBeenCalledWith ( '/tmp/other.jpg' , '/repo' )
71+ expect ( result . messageContent ) . toHaveLength ( 2 )
72+ expect ( result . messageContent ?. [ 0 ] ) . toMatchObject ( {
73+ type : 'image' ,
74+ image : 'pre-processed-base64-data' ,
75+ } )
76+ expect ( result . messageContent ?. [ 1 ] ) . toMatchObject ( {
77+ type : 'image' ,
78+ image : 'inline-base64-data' ,
79+ } )
80+ } )
81+
82+ test ( 'backwards compatibility: processes from disk when processedImage is missing' , async ( ) => {
83+ // This tests the edge case where processedImage is missing but status is 'ready'
84+ const pendingImages = [ createPendingImage ( '/tmp/pic.png' ) ] // No processedImage
85+ const warn = mock ( ( ) => { } )
86+ const processor = mock ( async ( ) => ( {
87+ success : true ,
88+ imagePart : {
89+ type : 'image' as const ,
90+ image : 'disk-base64-data' ,
91+ mediaType : 'image/png' ,
92+ } ,
93+ } ) )
94+
95+ const result = await processImagesForMessage ( {
96+ content : '' ,
97+ pendingImages,
98+ projectRoot : '/repo' ,
99+ processor : processor as any ,
100+ log : { warn } as any ,
101+ } )
102+
103+ // Should warn about missing processedImage and fall back to disk
104+ expect ( warn ) . toHaveBeenCalled ( )
105+ expect ( processor ) . toHaveBeenCalledTimes ( 1 )
106+ expect ( result . messageContent ?. [ 0 ] ) . toMatchObject ( {
107+ type : 'image' ,
108+ image : 'disk-base64-data' ,
38109 } )
39110 } )
40111
41- test ( 'logs warnings when processing fails' , async ( ) => {
112+ test ( 'skips images with processing or error status' , async ( ) => {
113+ const pendingImages : PendingImageAttachment [ ] = [
114+ { kind : 'image' , path : '/tmp/processing.png' , filename : 'processing.png' , status : 'processing' } ,
115+ { kind : 'image' , path : '/tmp/error.png' , filename : 'error.png' , status : 'error' , note : 'failed' } ,
116+ createPendingImage ( '/tmp/ready.png' , { base64 : 'ready-data' , mediaType : 'image/png' } ) ,
117+ ]
118+ const processor = mock ( async ( ) => ( {
119+ success : true ,
120+ imagePart : {
121+ type : 'image' as const ,
122+ image : 'should-not-be-used' ,
123+ mediaType : 'image/png' ,
124+ } ,
125+ } ) )
126+
127+ const result = await processImagesForMessage ( {
128+ content : '' ,
129+ pendingImages,
130+ projectRoot : '/repo' ,
131+ processor : processor as any ,
132+ } )
133+
134+ // Should not call processor at all (ready image has processedImage)
135+ expect ( processor ) . not . toHaveBeenCalled ( )
136+ // Only the ready image should be in messageContent
137+ expect ( result . messageContent ) . toHaveLength ( 1 )
138+ expect ( result . messageContent ?. [ 0 ] ) . toMatchObject ( {
139+ type : 'image' ,
140+ image : 'ready-data' ,
141+ } )
142+ } )
143+
144+ test ( 'logs warnings when inline path processing fails' , async ( ) => {
42145 const warn = mock ( ( ) => { } )
43- const pendingImages = [ createPendingImage ( '/tmp/fail.png' ) ]
146+ const pendingImages : PendingImageAttachment [ ] = [ ]
44147 const processor = mock ( async ( ) => ( {
45148 success : false ,
46149 error : 'boom' ,
47150 } ) )
48151
49152 const result = await processImagesForMessage ( {
50- content : '' ,
153+ content : 'Here is an image @/tmp/fail.png ' ,
51154 pendingImages,
52155 projectRoot : '/repo' ,
53156 processor : processor as any ,
@@ -57,4 +160,35 @@ describe('processImagesForMessage', () => {
57160 expect ( warn ) . toHaveBeenCalled ( )
58161 expect ( result . messageContent ) . toBeUndefined ( )
59162 } )
163+
164+ test ( 'deduplicates: does not process inline path that matches pending image path' , async ( ) => {
165+ const pendingImages = [ createPendingImage ( '/tmp/pic.png' , {
166+ base64 : 'pre-processed-data' ,
167+ mediaType : 'image/png' ,
168+ } ) ]
169+ const processor = mock ( async ( ) => ( {
170+ success : true ,
171+ imagePart : {
172+ type : 'image' as const ,
173+ image : 'disk-data' ,
174+ mediaType : 'image/png' ,
175+ } ,
176+ } ) )
177+
178+ const result = await processImagesForMessage ( {
179+ content : 'Here is the same image @/tmp/pic.png and again /tmp/pic.png' ,
180+ pendingImages,
181+ projectRoot : '/repo' ,
182+ processor : processor as any ,
183+ } )
184+
185+ // Should not call processor since the path is already in pendingImages
186+ expect ( processor ) . not . toHaveBeenCalled ( )
187+ // Should only have one image in messageContent (no duplicates)
188+ expect ( result . messageContent ) . toHaveLength ( 1 )
189+ expect ( result . messageContent ?. [ 0 ] ) . toMatchObject ( {
190+ type : 'image' ,
191+ image : 'pre-processed-data' ,
192+ } )
193+ } )
60194} )
0 commit comments