@@ -489,6 +489,11 @@ describe("read_file tool XML output structure", () => {
489489 const mockedExtractTextFromFile = vi . mocked ( extractTextFromFile )
490490 const mockedIsBinaryFile = vi . mocked ( isBinaryFile )
491491 const mockedPathResolve = vi . mocked ( path . resolve )
492+ const mockedFsReadFile = vi . mocked ( fsPromises . readFile )
493+ const imageBuffer = Buffer . from (
494+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" ,
495+ "base64" ,
496+ )
492497
493498 let mockCline : any
494499 let mockProvider : any
@@ -679,6 +684,11 @@ describe("read_file tool XML output structure", () => {
679684 } ,
680685 ( _ : ToolParamName , content ?: string ) => content ?? "" ,
681686 )
687+ // In multi-image scenarios, the result is pushed to pushToolResult, not returned directly.
688+ // We need to check the mock's calls to get the result.
689+ if ( mockCline . pushToolResult . mock . calls . length > 0 ) {
690+ return mockCline . pushToolResult . mock . calls [ 0 ] [ 0 ]
691+ }
682692
683693 return localResult
684694 }
@@ -822,8 +832,8 @@ describe("read_file tool XML output structure", () => {
822832 expect ( Array . isArray ( result ) ) . toBe ( true )
823833 const parts = result as any [ ]
824834
825- const textPart = parts . find ( ( p ) => p . type === "text" ) ?. text
826- const imageParts = parts . filter ( ( p ) => p . type === "image" )
835+ const textPart = Array . isArray ( result ) ? result . find ( ( p ) => p . type === "text" ) ?. text : result
836+ const imageParts = Array . isArray ( result ) ? result . filter ( ( p ) => p . type === "image" ) : [ ]
827837
828838 expect ( textPart ) . toBeDefined ( )
829839
@@ -836,9 +846,9 @@ describe("read_file tool XML output structure", () => {
836846 expect ( imageParts ) . toHaveLength ( 4 ) // First 4 images should be included (~19.6MB total)
837847
838848 // Verify memory limit notice for the fifth image
839- expect ( textPart ) . toContain ( "Total image memory would exceed 20MB limit" )
840- expect ( textPart ) . toContain ( "current : 19.6MB" ) // 4 * 4.9MB
841- expect ( textPart ) . toContain ( "this file: 4.9MB" )
849+ expect ( textPart ) . toContain (
850+ "Image skipped to avoid memory limit (20MB). Current : 19.6MB + this file: 4.9MB. Try fewer or smaller images." ,
851+ )
842852 } )
843853
844854 it ( "should track memory usage correctly across multiple images" , async ( ) => {
@@ -907,15 +917,13 @@ describe("read_file tool XML output structure", () => {
907917 const result = await executeReadMultipleImagesTool ( exactLimitImages . map ( ( img ) => img . path ) )
908918
909919 // Verify
910- expect ( Array . isArray ( result ) ) . toBe ( true )
911- const parts = result as any [ ]
912-
913- const textPart = parts . find ( ( p ) => p . type === "text" ) ?. text
914- const imageParts = parts . filter ( ( p ) => p . type === "image" )
920+ const textPart = Array . isArray ( result ) ? result . find ( ( p ) => p . type === "text" ) ?. text : result
921+ const imageParts = Array . isArray ( result ) ? result . filter ( ( p ) => p . type === "image" ) : [ ]
915922
916923 expect ( imageParts ) . toHaveLength ( 2 ) // First 2 images should fit
917- expect ( textPart ) . toContain ( "Total image memory would exceed 20MB limit" )
918- expect ( textPart ) . toContain ( "current: 20.0MB" ) // Should show exactly 20MB used
924+ expect ( textPart ) . toContain (
925+ "Image skipped to avoid memory limit (20MB). Current: 20.0MB + this file: 1.0MB. Try fewer or smaller images." ,
926+ )
919927 } )
920928
921929 it ( "should handle individual image size limit and total memory limit together" , async ( ) => {
@@ -1000,6 +1008,46 @@ describe("read_file tool XML output structure", () => {
10001008 expect ( textPart ) . toContain ( "Image file is too large (6.0 MB). The maximum allowed size is 5 MB." )
10011009 } )
10021010
1011+ it ( "should correctly calculate total memory and skip the last image" , async ( ) => {
1012+ // Setup
1013+ const testImages = [
1014+ { path : "test/image1.png" , sizeMB : 8 } ,
1015+ { path : "test/image2.png" , sizeMB : 8 } ,
1016+ { path : "test/image3.png" , sizeMB : 8 } , // This one should be skipped
1017+ ]
1018+
1019+ mockProvider . getState . mockResolvedValue ( {
1020+ maxReadFileLine : - 1 ,
1021+ maxImageFileSize : 10 , // 10MB per image
1022+ maxTotalImageMemory : 20 , // 20MB total
1023+ } )
1024+
1025+ mockedIsBinaryFile . mockResolvedValue ( true )
1026+ mockedCountFileLines . mockResolvedValue ( 0 )
1027+ mockedFsReadFile . mockResolvedValue ( imageBuffer )
1028+
1029+ fsPromises . stat . mockImplementation ( async ( filePath ) => {
1030+ const file = testImages . find ( ( f ) => filePath . toString ( ) . includes ( f . path ) )
1031+ if ( file ) {
1032+ return { size : file . sizeMB * 1024 * 1024 }
1033+ }
1034+ return { size : 1024 * 1024 } // Default 1MB
1035+ } )
1036+
1037+ const imagePaths = testImages . map ( ( img ) => img . path )
1038+ const result = await executeReadMultipleImagesTool ( imagePaths )
1039+
1040+ expect ( Array . isArray ( result ) ) . toBe ( true )
1041+ const parts = result as any [ ]
1042+ const textPart = parts . find ( ( p ) => p . type === "text" ) ?. text
1043+ const imageParts = parts . filter ( ( p ) => p . type === "image" )
1044+
1045+ expect ( imageParts ) . toHaveLength ( 2 ) // First two images should be processed
1046+ expect ( textPart ) . toContain ( "Image skipped to avoid memory limit (20MB)" )
1047+ expect ( textPart ) . toContain ( "Current: 16.0MB" )
1048+ expect ( textPart ) . toContain ( "this file: 8.0MB" )
1049+ } )
1050+
10031051 it ( "should reset total memory tracking for each tool invocation" , async ( ) => {
10041052 // Setup mocks (don't clear all mocks)
10051053
0 commit comments