@@ -10,7 +10,7 @@ import { isBinaryFile } from "isbinaryfile"
1010import { ReadFileToolUse , ToolParamName , ToolResponse } from "../../../shared/tools"
1111import { readFileTool } from "../readFileTool"
1212import { formatResponse } from "../../prompts/responses"
13- import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB , DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB } from "../helpers/imageHelpers"
13+ import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB , DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB } from "../helpers/imageHelpers"
1414
1515vi . mock ( "path" , async ( ) => {
1616 const originalPath = await vi . importActual ( "path" )
@@ -156,7 +156,7 @@ vi.mock("../../../i18n", () => ({
156156 const translations : Record < string , string > = {
157157 "tools:readFile.imageWithSize" : "Image file ({{size}} KB)" ,
158158 "tools:readFile.imageTooLarge" :
159- "Image file is too large ({{size}} MB ). The maximum allowed size is {{max}} MB." ,
159+ "Image file is too large ({{size}}). The maximum allowed size is {{max}} MB." ,
160160 "tools:readFile.linesRange" : " (lines {{start}}-{{end}})" ,
161161 "tools:readFile.definitionsOnly" : " (definitions only)" ,
162162 "tools:readFile.maxLines" : " (max {{max}} lines)" ,
@@ -296,7 +296,7 @@ describe("read_file tool with maxReadFileLine setting", () => {
296296 const maxReadFileLine = options . maxReadFileLine ?? 500
297297 const totalLines = options . totalLines ?? 5
298298
299- mockProvider . getState . mockResolvedValue ( { maxReadFileLine, maxImageFileSize : 20 , maxTotalImageMemory : 20 } )
299+ mockProvider . getState . mockResolvedValue ( { maxReadFileLine, maxImageFileSize : 20 , maxTotalImageSize : 20 } )
300300 mockedCountFileLines . mockResolvedValue ( totalLines )
301301
302302 // Reset the spy before each test
@@ -532,7 +532,7 @@ describe("read_file tool XML output structure", () => {
532532 mockInputContent = fileContent
533533
534534 // Setup mock provider with default maxReadFileLine
535- mockProvider . getState . mockResolvedValue ( { maxReadFileLine : - 1 , maxImageFileSize : 20 , maxTotalImageMemory : 20 } ) // Default to full file read
535+ mockProvider . getState . mockResolvedValue ( { maxReadFileLine : - 1 , maxImageFileSize : 20 , maxTotalImageSize : 20 } ) // Default to full file read
536536
537537 // Add additional properties needed for XML tests
538538 mockCline . sayAndCreateMissingParamError = vi . fn ( ) . mockResolvedValue ( "Missing required parameter" )
@@ -557,7 +557,7 @@ describe("read_file tool XML output structure", () => {
557557 const isBinary = options . isBinary ?? false
558558 const validateAccess = options . validateAccess ?? true
559559
560- mockProvider . getState . mockResolvedValue ( { maxReadFileLine, maxImageFileSize : 20 , maxTotalImageMemory : 20 } )
560+ mockProvider . getState . mockResolvedValue ( { maxReadFileLine, maxImageFileSize : 20 , maxTotalImageSize : 20 } )
561561 mockedCountFileLines . mockResolvedValue ( totalLines )
562562 mockedIsBinaryFile . mockResolvedValue ( isBinary )
563563 mockCline . rooIgnoreController . validateAccess = vi . fn ( ) . mockReturnValue ( validateAccess )
@@ -599,8 +599,8 @@ describe("read_file tool XML output structure", () => {
599599 mockProvider . getState . mockResolvedValue ( {
600600 maxReadFileLine : - 1 ,
601601 maxImageFileSize : 20 ,
602- maxTotalImageMemory : 20 ,
603- } ) // Allow up to 20MB per image and total memory
602+ maxTotalImageSize : 20 ,
603+ } ) // Allow up to 20MB per image and total size
604604
605605 // Execute
606606 const result = await executeReadFileTool ( )
@@ -632,8 +632,8 @@ describe("read_file tool XML output structure", () => {
632632 mockProvider . getState . mockResolvedValue ( {
633633 maxReadFileLine : - 1 ,
634634 maxImageFileSize : 20 ,
635- maxTotalImageMemory : 20 ,
636- } ) // Allow up to 20MB per image and total memory
635+ maxTotalImageSize : 20 ,
636+ } ) // Allow up to 20MB per image and total size
637637
638638 // Execute
639639 const result = await executeReadFileTool ( { } , { totalLines : 0 } )
@@ -651,6 +651,12 @@ describe("read_file tool XML output structure", () => {
651651 { path : "test/image3.gif" , sizeKB : 8192 } , // 8MB
652652 ]
653653
654+ // Define imageBuffer for this test suite
655+ const imageBuffer = Buffer . from (
656+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" ,
657+ "base64" ,
658+ )
659+
654660 beforeEach ( ( ) => {
655661 // CRITICAL: Reset fsPromises mocks to prevent cross-test contamination within this suite
656662 fsPromises . stat . mockClear ( )
@@ -707,8 +713,8 @@ describe("read_file tool XML output structure", () => {
707713 mockProvider . getState . mockResolvedValue ( {
708714 maxReadFileLine : - 1 ,
709715 maxImageFileSize : 20 ,
710- maxTotalImageMemory : 20 ,
711- } ) // Allow up to 20MB per image and total memory
716+ maxTotalImageSize : 20 ,
717+ } ) // Allow up to 20MB per image and total size
712718
713719 // Setup mockCline properties (preserve existing API)
714720 mockCline . cwd = "/"
@@ -780,8 +786,8 @@ describe("read_file tool XML output structure", () => {
780786 mockProvider . getState . mockResolvedValue ( {
781787 maxReadFileLine : - 1 ,
782788 maxImageFileSize : 15 ,
783- maxTotalImageMemory : 20 ,
784- } ) // Allow up to 15MB per image and 20MB total memory
789+ maxTotalImageSize : 20 ,
790+ } ) // Allow up to 15MB per image and 20MB total size
785791
786792 // Setup mockCline properties
787793 mockCline . cwd = "/"
@@ -844,9 +850,9 @@ describe("read_file tool XML output structure", () => {
844850 expect ( imageParts ) . toHaveLength ( 4 ) // First 4 images should be included (~19.6MB total)
845851
846852 // Verify memory limit notice for the fifth image
847- expect ( textPart ) . toContain (
848- "Image skipped to avoid memory limit (20MB). Current: 19.6MB + this file: 4.9MB. Try fewer or smaller images." ,
849- )
853+ expect ( textPart ) . toContain ( "Image skipped to avoid size limit (20MB)" )
854+ expect ( textPart ) . toMatch ( / C u r r e n t : \d + ( \. \d + ) ? M B / )
855+ expect ( textPart ) . toMatch ( / t h i s f i l e : \d + ( \. \d + ) ? M B / )
850856 } )
851857
852858 it ( "should track memory usage correctly across multiple images" , async ( ) => {
@@ -866,8 +872,8 @@ describe("read_file tool XML output structure", () => {
866872 mockProvider . getState . mockResolvedValue ( {
867873 maxReadFileLine : - 1 ,
868874 maxImageFileSize : 15 ,
869- maxTotalImageMemory : 20 ,
870- } ) // Allow up to 15MB per image and 20MB total memory
875+ maxTotalImageSize : 20 ,
876+ } ) // Allow up to 15MB per image and 20MB total size
871877
872878 // Setup mockCline properties
873879 mockCline . cwd = "/"
@@ -917,9 +923,9 @@ describe("read_file tool XML output structure", () => {
917923 const imageParts = Array . isArray ( result ) ? result . filter ( ( p ) => p . type === "image" ) : [ ]
918924
919925 expect ( imageParts ) . toHaveLength ( 2 ) // First 2 images should fit
920- expect ( textPart ) . toContain (
921- "Image skipped to avoid memory limit (20MB). Current: 20.0MB + this file: 1.0MB. Try fewer or smaller images." ,
922- )
926+ expect ( textPart ) . toContain ( "Image skipped to avoid size limit (20MB)" )
927+ expect ( textPart ) . toMatch ( / C u r r e n t : \d + ( \. \d + ) ? M B / )
928+ expect ( textPart ) . toMatch ( / t h i s f i l e : \d + ( \. \d + ) ? M B / )
923929 } )
924930
925931 it ( "should handle individual image size limit and total memory limit together" , async ( ) => {
@@ -939,8 +945,8 @@ describe("read_file tool XML output structure", () => {
939945 mockProvider . getState . mockResolvedValue ( {
940946 maxReadFileLine : - 1 ,
941947 maxImageFileSize : 20 ,
942- maxTotalImageMemory : 20 ,
943- } ) // Allow up to 20MB per image and total memory
948+ maxTotalImageSize : 20 ,
949+ } ) // Allow up to 20MB per image and total size
944950
945951 // Setup mockCline properties (complete setup)
946952 mockCline . cwd = "/"
@@ -981,7 +987,7 @@ describe("read_file tool XML output structure", () => {
981987 mockProvider . getState . mockResolvedValue ( {
982988 maxReadFileLine : - 1 ,
983989 maxImageFileSize : 5 ,
984- maxTotalImageMemory : 20 ,
990+ maxTotalImageSize : 20 ,
985991 } )
986992
987993 // Mock path.resolve
@@ -1001,7 +1007,9 @@ describe("read_file tool XML output structure", () => {
10011007 expect ( imageParts ) . toHaveLength ( 2 )
10021008
10031009 // Should show individual size limit violation
1004- expect ( textPart ) . toContain ( "Image file is too large (6.0 MB). The maximum allowed size is 5 MB." )
1010+ expect ( textPart ) . toMatch (
1011+ / I m a g e f i l e i s t o o l a r g e \( \d + ( \. \d + ) ? M B \) \. T h e m a x i m u m a l l o w e d s i z e i s 5 M B \. / ,
1012+ )
10051013 } )
10061014
10071015 it ( "should correctly calculate total memory and skip the last image" , async ( ) => {
@@ -1015,7 +1023,7 @@ describe("read_file tool XML output structure", () => {
10151023 mockProvider . getState . mockResolvedValue ( {
10161024 maxReadFileLine : - 1 ,
10171025 maxImageFileSize : 10 , // 10MB per image
1018- maxTotalImageMemory : 20 , // 20MB total
1026+ maxTotalImageSize : 20 , // 20MB total
10191027 } )
10201028
10211029 mockedIsBinaryFile . mockResolvedValue ( true )
@@ -1040,9 +1048,9 @@ describe("read_file tool XML output structure", () => {
10401048 const imageParts = parts . filter ( ( p ) => p . type === "image" )
10411049
10421050 expect ( imageParts ) . toHaveLength ( 2 ) // First two images should be processed
1043- expect ( textPart ) . toContain ( "Image skipped to avoid memory limit (20MB)" )
1044- expect ( textPart ) . toContain ( " Current: 16.0MB" )
1045- expect ( textPart ) . toContain ( " this file: 8.0MB" )
1051+ expect ( textPart ) . toContain ( "Image skipped to avoid size limit (20MB)" )
1052+ expect ( textPart ) . toMatch ( / C u r r e n t : \d + ( \. \d + ) ? M B / )
1053+ expect ( textPart ) . toMatch ( / t h i s f i l e : \d + ( \. \d + ) ? M B / )
10461054 } )
10471055
10481056 it ( "should reset total memory tracking for each tool invocation" , async ( ) => {
@@ -1062,7 +1070,7 @@ describe("read_file tool XML output structure", () => {
10621070 mockProvider . getState . mockResolvedValue ( {
10631071 maxReadFileLine : - 1 ,
10641072 maxImageFileSize : 20 ,
1065- maxTotalImageMemory : 20 ,
1073+ maxTotalImageSize : 20 ,
10661074 } )
10671075
10681076 // Setup mockCline properties (complete setup)
@@ -1106,7 +1114,7 @@ describe("read_file tool XML output structure", () => {
11061114 mockProvider . getState . mockResolvedValue ( {
11071115 maxReadFileLine : - 1 ,
11081116 maxImageFileSize : 20 ,
1109- maxTotalImageMemory : 20 ,
1117+ maxTotalImageSize : 20 ,
11101118 } )
11111119
11121120 // Reset path resolving for second batch
@@ -1162,6 +1170,123 @@ describe("read_file tool XML output structure", () => {
11621170
11631171 expect ( imageParts ) . toHaveLength ( 1 ) // Second image should be processed
11641172 } )
1173+
1174+ it ( "should handle a folder with many images just under the individual size limit" , async ( ) => {
1175+ // Setup - Create many images that are each just under the 5MB individual limit
1176+ // but together approach the 20MB total limit
1177+ const manyImages = [
1178+ { path : "test/img1.png" , sizeKB : 4900 } , // 4.78MB
1179+ { path : "test/img2.png" , sizeKB : 4900 } , // 4.78MB
1180+ { path : "test/img3.png" , sizeKB : 4900 } , // 4.78MB
1181+ { path : "test/img4.png" , sizeKB : 4900 } , // 4.78MB
1182+ { path : "test/img5.png" , sizeKB : 4900 } , // 4.78MB - This should be skipped (total would be ~23.9MB)
1183+ ]
1184+
1185+ // Setup mocks
1186+ mockedIsBinaryFile . mockResolvedValue ( true )
1187+ mockedCountFileLines . mockResolvedValue ( 0 )
1188+ fsPromises . readFile . mockResolvedValue ( imageBuffer )
1189+
1190+ // Setup provider with 5MB individual limit and 20MB total limit
1191+ mockProvider . getState . mockResolvedValue ( {
1192+ maxReadFileLine : - 1 ,
1193+ maxImageFileSize : 5 ,
1194+ maxTotalImageSize : 20 ,
1195+ } )
1196+
1197+ // Mock file stats for each image
1198+ fsPromises . stat = vi . fn ( ) . mockImplementation ( ( filePath ) => {
1199+ const normalizedFilePath = path . normalize ( filePath . toString ( ) )
1200+ const image = manyImages . find ( ( img ) => normalizedFilePath . includes ( path . normalize ( img . path ) ) )
1201+ return Promise . resolve ( { size : ( image ?. sizeKB || 1024 ) * 1024 } )
1202+ } )
1203+
1204+ // Mock path.resolve
1205+ mockedPathResolve . mockImplementation ( ( cwd , relPath ) => `/${ relPath } ` )
1206+
1207+ // Execute
1208+ const result = await executeReadMultipleImagesTool ( manyImages . map ( ( img ) => img . path ) )
1209+
1210+ // Verify
1211+ expect ( Array . isArray ( result ) ) . toBe ( true )
1212+ const parts = result as any [ ]
1213+ const textPart = parts . find ( ( p ) => p . type === "text" ) ?. text
1214+ const imageParts = parts . filter ( ( p ) => p . type === "image" )
1215+
1216+ // Should process first 4 images (total ~19.12MB, under 20MB limit)
1217+ expect ( imageParts ) . toHaveLength ( 4 )
1218+
1219+ // Should show memory limit notice for the 5th image
1220+ expect ( textPart ) . toContain ( "Image skipped to avoid size limit (20MB)" )
1221+ expect ( textPart ) . toContain ( "test/img5.png" )
1222+
1223+ // Verify memory tracking worked correctly
1224+ // The notice should show current memory usage around 20MB (4 * 4900KB ≈ 19.14MB, displayed as 20.1MB)
1225+ expect ( textPart ) . toMatch ( / C u r r e n t : \d + ( \. \d + ) ? M B / )
1226+ } )
1227+
1228+ it ( "should reset memory tracking between separate tool invocations more explicitly" , async ( ) => {
1229+ // This test verifies that totalImageMemoryUsed is reset between calls
1230+ // by making two separate tool invocations and ensuring the second one
1231+ // starts with fresh memory tracking
1232+
1233+ // Setup mocks
1234+ mockedIsBinaryFile . mockResolvedValue ( true )
1235+ mockedCountFileLines . mockResolvedValue ( 0 )
1236+ fsPromises . readFile . mockResolvedValue ( imageBuffer )
1237+
1238+ // Setup provider
1239+ mockProvider . getState . mockResolvedValue ( {
1240+ maxReadFileLine : - 1 ,
1241+ maxImageFileSize : 20 ,
1242+ maxTotalImageSize : 20 ,
1243+ } )
1244+
1245+ // First invocation - use 15MB of memory
1246+ const firstBatch = [ { path : "test/large1.png" , sizeKB : 15360 } ] // 15MB
1247+
1248+ fsPromises . stat = vi . fn ( ) . mockResolvedValue ( { size : 15360 * 1024 } )
1249+ mockedPathResolve . mockImplementation ( ( cwd , relPath ) => `/${ relPath } ` )
1250+
1251+ // Execute first batch
1252+ const result1 = await executeReadMultipleImagesTool ( firstBatch . map ( ( img ) => img . path ) )
1253+
1254+ // Verify first batch processed successfully
1255+ expect ( Array . isArray ( result1 ) ) . toBe ( true )
1256+ const parts1 = result1 as any [ ]
1257+ const imageParts1 = parts1 . filter ( ( p ) => p . type === "image" )
1258+ expect ( imageParts1 ) . toHaveLength ( 1 )
1259+
1260+ // Second invocation - should start with 0 memory used, not 15MB
1261+ // If memory tracking wasn't reset, this 18MB image would be rejected
1262+ const secondBatch = [ { path : "test/large2.png" , sizeKB : 18432 } ] // 18MB
1263+
1264+ // Reset mocks for second invocation
1265+ fsPromises . stat . mockClear ( )
1266+ fsPromises . readFile . mockClear ( )
1267+ mockedPathResolve . mockClear ( )
1268+
1269+ fsPromises . stat = vi . fn ( ) . mockResolvedValue ( { size : 18432 * 1024 } )
1270+ fsPromises . readFile . mockResolvedValue ( imageBuffer )
1271+ mockedPathResolve . mockImplementation ( ( cwd , relPath ) => `/${ relPath } ` )
1272+
1273+ // Execute second batch
1274+ const result2 = await executeReadMultipleImagesTool ( secondBatch . map ( ( img ) => img . path ) )
1275+
1276+ // Verify second batch processed successfully
1277+ expect ( Array . isArray ( result2 ) ) . toBe ( true )
1278+ const parts2 = result2 as any [ ]
1279+ const imageParts2 = parts2 . filter ( ( p ) => p . type === "image" )
1280+ const textPart2 = parts2 . find ( ( p ) => p . type === "text" ) ?. text
1281+
1282+ // The 18MB image should be processed successfully because memory was reset
1283+ expect ( imageParts2 ) . toHaveLength ( 1 )
1284+
1285+ // Should NOT contain any memory limit notices
1286+ expect ( textPart2 ) . not . toContain ( "Image skipped to avoid memory limit" )
1287+
1288+ // This proves memory tracking was reset between invocations
1289+ } )
11651290 } )
11661291 } )
11671292
0 commit comments