@@ -18,9 +18,10 @@ describe('tools', () => {
1818
1919 describe ( 'TOOLS' , ( ) => {
2020 it ( 'exports tool definitions' , ( ) => {
21- expect ( TOOLS ) . toHaveLength ( 6 ) ;
21+ expect ( TOOLS ) . toHaveLength ( 7 ) ;
2222 expect ( TOOLS . map ( ( t ) => t . function . name ) ) . toContain ( 'read_file' ) ;
2323 expect ( TOOLS . map ( ( t ) => t . function . name ) ) . toContain ( 'write_file' ) ;
24+ expect ( TOOLS . map ( ( t ) => t . function . name ) ) . toContain ( 'edit_file' ) ;
2425 expect ( TOOLS . map ( ( t ) => t . function . name ) ) . toContain ( 'run_shell' ) ;
2526 expect ( TOOLS . map ( ( t ) => t . function . name ) ) . toContain ( 'list_dir' ) ;
2627 expect ( TOOLS . map ( ( t ) => t . function . name ) ) . toContain ( 'grep_search' ) ;
@@ -29,8 +30,9 @@ describe('tools', () => {
2930 } ) ;
3031
3132 describe ( 'TOOLS_REQUIRING_APPROVAL' , ( ) => {
32- it ( 'contains write_file and run_shell' , ( ) => {
33+ it ( 'contains write_file, edit_file, and run_shell' , ( ) => {
3334 expect ( TOOLS_REQUIRING_APPROVAL . has ( 'write_file' ) ) . toBe ( true ) ;
35+ expect ( TOOLS_REQUIRING_APPROVAL . has ( 'edit_file' ) ) . toBe ( true ) ;
3436 expect ( TOOLS_REQUIRING_APPROVAL . has ( 'run_shell' ) ) . toBe ( true ) ;
3537 expect ( TOOLS_REQUIRING_APPROVAL . has ( 'read_file' as 'write_file' ) ) . toBe (
3638 false ,
@@ -66,6 +68,24 @@ describe('tools', () => {
6668 ) ;
6769 } ) ;
6870
71+ it ( 'executes edit_file tool' , async ( ) => {
72+ vi . mocked ( existsSync ) . mockReturnValue ( true ) ;
73+ vi . mocked ( readFileSync ) . mockReturnValue ( 'before target after' ) ;
74+
75+ const result = await executeTool ( 'edit_file' , {
76+ path : '/test.txt' ,
77+ oldText : 'target' ,
78+ newText : 'updated' ,
79+ } ) ;
80+
81+ expect ( result . content ) . toContain ( 'File edited successfully' ) ;
82+ expect ( vi . mocked ( writeFileSync ) ) . toHaveBeenCalledWith (
83+ '/test.txt' ,
84+ 'before updated after' ,
85+ 'utf8' ,
86+ ) ;
87+ } ) ;
88+
6989 it ( 'executes list_dir tool' , async ( ) => {
7090 vi . mocked ( existsSync ) . mockReturnValue ( true ) ;
7191 vi . mocked ( readdirSync ) . mockReturnValue ( [
@@ -205,6 +225,88 @@ describe('tools', () => {
205225 } ) ;
206226 } ) ;
207227
228+ describe ( 'editFile error handling' , ( ) => {
229+ it ( 'returns error when file does not exist' , async ( ) => {
230+ vi . mocked ( existsSync ) . mockReturnValue ( false ) ;
231+
232+ const result = await executeTool ( 'edit_file' , {
233+ path : '/missing.txt' ,
234+ oldText : 'before' ,
235+ newText : 'after' ,
236+ } ) ;
237+ expect ( result . error ) . toContain ( 'File not found' ) ;
238+ } ) ;
239+
240+ it ( 'returns error when text is not found' , async ( ) => {
241+ vi . mocked ( existsSync ) . mockReturnValue ( true ) ;
242+ vi . mocked ( readFileSync ) . mockReturnValue ( 'content' ) ;
243+
244+ const result = await executeTool ( 'edit_file' , {
245+ path : '/test.txt' ,
246+ oldText : 'missing' ,
247+ newText : 'after' ,
248+ } ) ;
249+ expect ( result . error ) . toContain ( 'Exact text not found' ) ;
250+ } ) ;
251+
252+ it ( 'returns error when text matches multiple locations' , async ( ) => {
253+ vi . mocked ( existsSync ) . mockReturnValue ( true ) ;
254+ vi . mocked ( readFileSync ) . mockReturnValue ( 'repeat repeat' ) ;
255+
256+ const result = await executeTool ( 'edit_file' , {
257+ path : '/test.txt' ,
258+ oldText : 'repeat' ,
259+ newText : 'after' ,
260+ } ) ;
261+ expect ( result . error ) . toContain ( 'matched multiple locations' ) ;
262+ } ) ;
263+
264+ it ( 'returns error when read fails' , async ( ) => {
265+ vi . mocked ( existsSync ) . mockReturnValue ( true ) ;
266+ vi . mocked ( readFileSync ) . mockImplementation ( ( ) => {
267+ throw new Error ( 'Permission denied' ) ;
268+ } ) ;
269+
270+ const result = await executeTool ( 'edit_file' , {
271+ path : '/test.txt' ,
272+ oldText : 'before' ,
273+ newText : 'after' ,
274+ } ) ;
275+ expect ( result . error ) . toContain ( 'Failed to edit file' ) ;
276+ } ) ;
277+
278+ it ( 'returns error when write fails' , async ( ) => {
279+ vi . mocked ( existsSync ) . mockReturnValue ( true ) ;
280+ vi . mocked ( readFileSync ) . mockReturnValue ( 'before' ) ;
281+ vi . mocked ( writeFileSync ) . mockImplementation ( ( ) => {
282+ throw new Error ( 'Disk full' ) ;
283+ } ) ;
284+
285+ const result = await executeTool ( 'edit_file' , {
286+ path : '/test.txt' ,
287+ oldText : 'before' ,
288+ newText : 'after' ,
289+ } ) ;
290+ expect ( result . error ) . toContain ( 'Failed to edit file' ) ;
291+ } ) ;
292+
293+ it ( 'handles non-Error exceptions in editFile' , async ( ) => {
294+ vi . mocked ( existsSync ) . mockReturnValue ( true ) ;
295+ vi . mocked ( readFileSync ) . mockImplementation ( ( ) => {
296+ // eslint-disable-next-line @typescript-eslint/only-throw-error
297+ throw 'edit failed' ;
298+ } ) ;
299+
300+ const result = await executeTool ( 'edit_file' , {
301+ path : '/test.txt' ,
302+ oldText : 'before' ,
303+ newText : 'after' ,
304+ } ) ;
305+ expect ( result . error ) . toContain ( 'Failed to edit file' ) ;
306+ expect ( result . error ) . toContain ( 'edit failed' ) ;
307+ } ) ;
308+ } ) ;
309+
208310 describe ( 'listDir error handling' , ( ) => {
209311 it ( 'returns error when directory does not exist' , async ( ) => {
210312 vi . mocked ( existsSync ) . mockReturnValue ( false ) ;
0 commit comments