@@ -305,6 +305,13 @@ describe("read_file tool with maxReadFileLine setting", () => {
305305 mockedPathResolve . mockReturnValue ( absoluteFilePath )
306306 mockedIsBinaryFile . mockResolvedValue ( false )
307307
308+ // Mock fsPromises.stat to return a file (not directory) by default
309+ fsPromises . stat . mockResolvedValue ( {
310+ isDirectory : ( ) => false ,
311+ isFile : ( ) => true ,
312+ isSymbolicLink : ( ) => false ,
313+ } as any )
314+
308315 mockInputContent = fileContent
309316
310317 // Setup the extractTextFromFile mock implementation with the current mockInputContent
@@ -612,7 +619,12 @@ describe("read_file tool output structure", () => {
612619
613620 // CRITICAL: Reset fsPromises mocks to prevent cross-test contamination
614621 fsPromises . stat . mockClear ( )
615- fsPromises . stat . mockResolvedValue ( { size : 1024 } )
622+ fsPromises . stat . mockResolvedValue ( {
623+ size : 1024 ,
624+ isDirectory : ( ) => false ,
625+ isFile : ( ) => true ,
626+ isSymbolicLink : ( ) => false ,
627+ } as any )
616628 fsPromises . readFile . mockClear ( )
617629
618630 // Use shared mock setup function
@@ -852,7 +864,7 @@ describe("read_file tool output structure", () => {
852864 fsPromises . stat = vi . fn ( ) . mockImplementation ( ( filePath ) => {
853865 const normalizedFilePath = path . normalize ( filePath . toString ( ) )
854866 const image = smallImages . find ( ( img ) => normalizedFilePath . includes ( path . normalize ( img . path ) ) )
855- return Promise . resolve ( { size : ( image ?. sizeKB || 1024 ) * 1024 } )
867+ return Promise . resolve ( { size : ( image ?. sizeKB || 1024 ) * 1024 , isDirectory : ( ) => false } )
856868 } )
857869
858870 // Mock path.resolve for each image
@@ -928,7 +940,7 @@ describe("read_file tool output structure", () => {
928940 fsPromises . stat = vi . fn ( ) . mockImplementation ( ( filePath ) => {
929941 const normalizedFilePath = path . normalize ( filePath . toString ( ) )
930942 const image = largeImages . find ( ( img ) => normalizedFilePath . includes ( path . normalize ( img . path ) ) )
931- return Promise . resolve ( { size : ( image ?. sizeKB || 1024 ) * 1024 } )
943+ return Promise . resolve ( { size : ( image ?. sizeKB || 1024 ) * 1024 , isDirectory : ( ) => false } )
932944 } )
933945
934946 // Mock path.resolve for each image
@@ -1012,9 +1024,9 @@ describe("read_file tool output structure", () => {
10121024 const normalizedFilePath = path . normalize ( filePath . toString ( ) )
10131025 const image = exactLimitImages . find ( ( img ) => normalizedFilePath . includes ( path . normalize ( img . path ) ) )
10141026 if ( image ) {
1015- return Promise . resolve ( { size : image . sizeKB * 1024 } )
1027+ return Promise . resolve ( { size : image . sizeKB * 1024 , isDirectory : ( ) => false } )
10161028 }
1017- return Promise . resolve ( { size : 1024 * 1024 } ) // Default 1MB
1029+ return Promise . resolve ( { size : 1024 * 1024 , isDirectory : ( ) => false } ) // Default 1MB
10181030 } )
10191031
10201032 // Mock path.resolve
@@ -1085,7 +1097,7 @@ describe("read_file tool output structure", () => {
10851097 const fileName = path . basename ( filePath )
10861098 const baseName = path . parse ( fileName ) . name
10871099 const image = mixedImages . find ( ( img ) => img . path . includes ( baseName ) )
1088- return Promise . resolve ( { size : ( image ?. sizeKB || 1024 ) * 1024 } )
1100+ return Promise . resolve ( { size : ( image ?. sizeKB || 1024 ) * 1024 , isDirectory : ( ) => false } )
10891101 } )
10901102
10911103 // Mock provider state with 5MB individual limit
@@ -1139,9 +1151,9 @@ describe("read_file tool output structure", () => {
11391151 const normalizedFilePath = path . normalize ( filePath . toString ( ) )
11401152 const file = testImages . find ( ( f ) => normalizedFilePath . includes ( path . normalize ( f . path ) ) )
11411153 if ( file ) {
1142- return { size : file . sizeMB * 1024 * 1024 }
1154+ return { size : file . sizeMB * 1024 * 1024 , isDirectory : ( ) => false }
11431155 }
1144- return { size : 1024 * 1024 } // Default 1MB
1156+ return { size : 1024 * 1024 , isDirectory : ( ) => false } // Default 1MB
11451157 } )
11461158
11471159 const imagePaths = testImages . map ( ( img ) => img . path )
@@ -1201,7 +1213,7 @@ describe("read_file tool output structure", () => {
12011213 // Setup - first call with images that use memory
12021214 const firstBatch = [ { path : "test/first.png" , sizeKB : 10240 } ] // 10MB
12031215
1204- fsPromises . stat = vi . fn ( ) . mockResolvedValue ( { size : 10240 * 1024 } )
1216+ fsPromises . stat = vi . fn ( ) . mockResolvedValue ( { size : 10240 * 1024 , isDirectory : ( ) => false } )
12051217 mockedPathResolve . mockImplementation ( ( cwd , relPath ) => `/${ relPath } ` )
12061218
12071219 // Execute first batch
@@ -1254,7 +1266,7 @@ describe("read_file tool output structure", () => {
12541266 mockedCountFileLines . mockClear ( )
12551267
12561268 // Reset mocks for second batch
1257- fsPromises . stat = vi . fn ( ) . mockResolvedValue ( { size : 15360 * 1024 } )
1269+ fsPromises . stat = vi . fn ( ) . mockResolvedValue ( { size : 15360 * 1024 , isDirectory : ( ) => false } )
12581270 fsPromises . readFile . mockResolvedValue (
12591271 Buffer . from (
12601272 "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" ,
@@ -1303,7 +1315,7 @@ describe("read_file tool output structure", () => {
13031315 fsPromises . stat = vi . fn ( ) . mockImplementation ( ( filePath ) => {
13041316 const normalizedFilePath = path . normalize ( filePath . toString ( ) )
13051317 const image = manyImages . find ( ( img ) => normalizedFilePath . includes ( path . normalize ( img . path ) ) )
1306- return Promise . resolve ( { size : ( image ?. sizeKB || 1024 ) * 1024 } )
1318+ return Promise . resolve ( { size : ( image ?. sizeKB || 1024 ) * 1024 , isDirectory : ( ) => false } )
13071319 } )
13081320
13091321 // Mock path.resolve
@@ -1350,7 +1362,7 @@ describe("read_file tool output structure", () => {
13501362 // First invocation - use 15MB of memory
13511363 const firstBatch = [ { path : "test/large1.png" , sizeKB : 15360 } ] // 15MB
13521364
1353- fsPromises . stat = vi . fn ( ) . mockResolvedValue ( { size : 15360 * 1024 } )
1365+ fsPromises . stat = vi . fn ( ) . mockResolvedValue ( { size : 15360 * 1024 , isDirectory : ( ) => false } )
13541366 mockedPathResolve . mockImplementation ( ( cwd , relPath ) => `/${ relPath } ` )
13551367
13561368 // Execute first batch
@@ -1371,7 +1383,7 @@ describe("read_file tool output structure", () => {
13711383 fsPromises . readFile . mockClear ( )
13721384 mockedPathResolve . mockClear ( )
13731385
1374- fsPromises . stat = vi . fn ( ) . mockResolvedValue ( { size : 18432 * 1024 } )
1386+ fsPromises . stat = vi . fn ( ) . mockResolvedValue ( { size : 18432 * 1024 , isDirectory : ( ) => false } )
13751387 fsPromises . readFile . mockResolvedValue ( imageBuffer )
13761388 mockedPathResolve . mockImplementation ( ( cwd , relPath ) => `/${ relPath } ` )
13771389
@@ -1429,6 +1441,37 @@ describe("read_file tool output structure", () => {
14291441 `File: ${ testFilePath } \nError: Access to ${ testFilePath } is blocked by the .rooignore file settings. You must try to continue in the task without using this file, or ask the user to update the .rooignore file.` ,
14301442 )
14311443 } )
1444+
1445+ it ( "should provide helpful error when trying to read a directory" , async ( ) => {
1446+ // Setup - mock fsPromises.stat to indicate the path is a directory
1447+ const dirPath = "test/my-directory"
1448+ const absoluteDirPath = "/test/my-directory"
1449+
1450+ mockedPathResolve . mockReturnValue ( absoluteDirPath )
1451+
1452+ // Mock fs/promises stat to return directory
1453+ fsPromises . stat . mockResolvedValue ( {
1454+ isDirectory : ( ) => true ,
1455+ isFile : ( ) => false ,
1456+ isSymbolicLink : ( ) => false ,
1457+ } as any )
1458+
1459+ // Mock isBinaryFile won't be called since we check directory first
1460+ mockedIsBinaryFile . mockResolvedValue ( false )
1461+
1462+ // Execute
1463+ const result = await executeReadFileTool ( { args : `<file><path>${ dirPath } </path></file>` } )
1464+
1465+ // Verify - native format for error
1466+ expect ( result ) . toContain ( `File: ${ dirPath } ` )
1467+ expect ( result ) . toContain ( `Error: Error reading file: Cannot read '${ dirPath } ' because it is a directory` )
1468+ expect ( result ) . toContain ( "use the list_files tool instead" )
1469+
1470+ // Verify that task.say was called with the error
1471+ expect ( mockCline . say ) . toHaveBeenCalledWith ( "error" , expect . stringContaining ( "Cannot read" ) )
1472+ expect ( mockCline . say ) . toHaveBeenCalledWith ( "error" , expect . stringContaining ( "is a directory" ) )
1473+ expect ( mockCline . say ) . toHaveBeenCalledWith ( "error" , expect . stringContaining ( "list_files tool" ) )
1474+ } )
14321475 } )
14331476} )
14341477
@@ -1460,7 +1503,12 @@ describe("read_file tool with image support", () => {
14601503
14611504 // CRITICAL: Reset fsPromises.stat to prevent cross-test contamination
14621505 fsPromises . stat . mockClear ( )
1463- fsPromises . stat . mockResolvedValue ( { size : 1024 } )
1506+ fsPromises . stat . mockResolvedValue ( {
1507+ size : 1024 ,
1508+ isDirectory : ( ) => false ,
1509+ isFile : ( ) => true ,
1510+ isSymbolicLink : ( ) => false ,
1511+ } as any )
14641512
14651513 // Use shared mock setup function with local variables
14661514 const mocks = createMockCline ( )
@@ -1801,6 +1849,13 @@ describe("read_file tool concurrent file reads limit", () => {
18011849 mockedIsBinaryFile . mockResolvedValue ( false )
18021850 mockedCountFileLines . mockResolvedValue ( 10 )
18031851
1852+ // Mock fsPromises.stat to return a file (not directory) by default
1853+ fsPromises . stat . mockResolvedValue ( {
1854+ isDirectory : ( ) => false ,
1855+ isFile : ( ) => true ,
1856+ isSymbolicLink : ( ) => false ,
1857+ } as any )
1858+
18041859 toolResult = undefined
18051860 } )
18061861
0 commit comments