@@ -1667,3 +1667,89 @@ describe("read_file tool with image support", () => {
16671667 } )
16681668 } )
16691669} )
1670+
1671+ // Additional coverage for unified FILE_NOT_FOUND handling
1672+ describe ( "read_file tool - FILE_NOT_FOUND unified error" , ( ) => {
1673+ it ( "emits structured FILE_NOT_FOUND and suppresses per-file XML for ENOENT" , async ( ) => {
1674+ // Reuse shared mocks/utilities from this spec file
1675+ const mockedCountFileLines = vi . mocked ( countFileLines )
1676+ const mockedIsBinaryFile = vi . mocked ( isBinaryFile )
1677+ const mockedPathResolve = vi . mocked ( path . resolve )
1678+ const mockedExtractTextFromFile = vi . mocked ( extractTextFromFile )
1679+
1680+ // Create a fresh cline
1681+ const { mockCline, mockProvider } = ( global as any ) . createMockCline
1682+ ? ( global as any ) . createMockCline ( )
1683+ : ( function ( ) {
1684+ // fallback: if hoisted reference isn't globally accessible for some reason
1685+ const provider = { getState : vi . fn ( ) , deref : vi . fn ( ) . mockReturnThis ( ) }
1686+ const cline : any = {
1687+ cwd : "/" ,
1688+ task : "Test" ,
1689+ providerRef : provider ,
1690+ rooIgnoreController : { validateAccess : vi . fn ( ) . mockReturnValue ( true ) } ,
1691+ say : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1692+ ask : vi . fn ( ) . mockResolvedValue ( { response : "yesButtonClicked" } ) ,
1693+ presentAssistantMessage : vi . fn ( ) ,
1694+ handleError : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1695+ pushToolResult : vi . fn ( ) ,
1696+ removeClosingTag : vi . fn ( ( tag : string , content ?: string ) => content ?? "" ) ,
1697+ fileContextTracker : { trackFileContext : vi . fn ( ) . mockResolvedValue ( undefined ) } ,
1698+ recordToolUsage : vi . fn ( ) . mockReturnValue ( undefined ) ,
1699+ recordToolError : vi . fn ( ) . mockReturnValue ( undefined ) ,
1700+ api : { getModel : vi . fn ( ) . mockReturnValue ( { info : { supportsImages : false } } ) } ,
1701+ }
1702+ return { mockCline : cline , mockProvider : provider }
1703+ } ) ( )
1704+
1705+ // Configure state to allow full read path
1706+ mockProvider . getState . mockResolvedValue ( { maxReadFileLine : - 1 , maxImageFileSize : 20 , maxTotalImageSize : 20 } )
1707+
1708+ // Basic path/line setup
1709+ const relPath = "test/file.txt"
1710+ const absPath = "/test/file.txt"
1711+ mockedPathResolve . mockReturnValue ( absPath )
1712+ mockedIsBinaryFile . mockResolvedValue ( false )
1713+ mockedCountFileLines . mockResolvedValue ( 5 )
1714+
1715+ // Cause ENOENT on actual read
1716+ const enoent = new Error ( "ENOENT: no such file or directory" ) as any
1717+ enoent . code = "ENOENT"
1718+ mockedExtractTextFromFile . mockRejectedValueOnce ( enoent )
1719+
1720+ // Build tool_use call (single-file via args)
1721+ const argsContent = `<file><path>${ relPath } </path></file>`
1722+ let pushed : ToolResponse | undefined
1723+ await readFileTool (
1724+ mockCline ,
1725+ {
1726+ type : "tool_use" ,
1727+ name : "read_file" ,
1728+ params : { args : argsContent } ,
1729+ partial : false ,
1730+ } as any ,
1731+ mockCline . ask ,
1732+ vi . fn ( ) , // handleError (should not be called for ENOENT in unified mode)
1733+ ( result : ToolResponse ) => {
1734+ pushed = result
1735+ } ,
1736+ ( _ : ToolParamName , content ?: string ) => content ?? "" ,
1737+ )
1738+
1739+ // Assert unified error was emitted with structured payload
1740+ const sayCalls = mockCline . say . mock . calls
1741+ const errorCall = sayCalls . find ( ( c : any [ ] ) => c [ 0 ] === "error" )
1742+ expect ( errorCall ) . toBeDefined ( )
1743+ const payload = JSON . parse ( errorCall ?. [ 1 ] || "{}" )
1744+ expect ( payload . code ) . toBe ( "FILE_NOT_FOUND" )
1745+ expect ( Array . isArray ( payload . filePaths ) ) . toBe ( true )
1746+ expect ( payload . filePaths ) . toContain ( relPath )
1747+
1748+ // Assert per-file XML error was suppressed for ENOENT (no <error> block for this file)
1749+ expect ( typeof pushed ) . toBe ( "string" )
1750+ const xml = String ( pushed )
1751+ expect ( xml ) . toContain ( "<files>" )
1752+ // no per-file error tags for the not-found case
1753+ expect ( xml ) . not . toMatch ( / < e r r o r > .* F i l e n o t f o u n d .* < \/ e r r o r > / i)
1754+ } )
1755+ } )
0 commit comments