@@ -81,6 +81,7 @@ describe('EditTool', () => {
8181 getGeminiMdFileCount : ( ) => 0 ,
8282 setGeminiMdFileCount : vi . fn ( ) ,
8383 getToolRegistry : ( ) => ( { } ) as any , // Minimal mock for ToolRegistry
84+ getReadAfterEdit : ( ) => vi . fn ( ) . mockReturnValue ( true ) ,
8485 } as unknown as Config ;
8586
8687 // Reset mocks before each test
@@ -847,3 +848,289 @@ describe('EditTool', () => {
847848 } ) ;
848849 } ) ;
849850} ) ;
851+
852+ describe ( 'EditTool - readAfterEdit' , ( ) => {
853+ let tool : EditTool ;
854+ let tempDir : string ;
855+ let rootDir : string ;
856+ let mockConfig : Config ;
857+ let geminiClient : any ;
858+
859+ beforeEach ( ( ) => {
860+ vi . restoreAllMocks ( ) ;
861+ tempDir = fs . mkdtempSync (
862+ path . join ( os . tmpdir ( ) , 'edit-tool-readafteredit-test-' ) ,
863+ ) ;
864+ rootDir = path . join ( tempDir , 'root' ) ;
865+ fs . mkdirSync ( rootDir ) ;
866+
867+ geminiClient = {
868+ generateJson : mockGenerateJson ,
869+ } ;
870+
871+ mockConfig = {
872+ getGeminiClient : vi . fn ( ) . mockReturnValue ( geminiClient ) ,
873+ getTargetDir : ( ) => rootDir ,
874+ getApprovalMode : vi . fn ( ) ,
875+ getWorkspaceContext : ( ) => createMockWorkspaceContext ( rootDir ) ,
876+ getReadAfterEdit : vi . fn ( ) . mockReturnValue ( true ) , // Default to true for these tests
877+ } as unknown as Config ;
878+
879+ ( mockConfig . getApprovalMode as Mock ) . mockClear ( ) ;
880+ ( mockConfig . getApprovalMode as Mock ) . mockReturnValue ( ApprovalMode . DEFAULT ) ;
881+
882+ mockEnsureCorrectEdit . mockReset ( ) ;
883+ mockEnsureCorrectEdit . mockImplementation (
884+ async ( _ , currentContent , params ) => {
885+ let occurrences = 0 ;
886+ if ( params . old_string && currentContent ) {
887+ let index = currentContent . indexOf ( params . old_string ) ;
888+ while ( index !== - 1 ) {
889+ occurrences ++ ;
890+ index = currentContent . indexOf ( params . old_string , index + 1 ) ;
891+ }
892+ } else if ( params . old_string === '' ) {
893+ occurrences = 0 ;
894+ }
895+ return Promise . resolve ( { params, occurrences } ) ;
896+ } ,
897+ ) ;
898+
899+ mockGenerateJson . mockReset ( ) ;
900+ mockGenerateJson . mockImplementation ( async ( ) => Promise . resolve ( { } ) ) ;
901+
902+ tool = new EditTool ( mockConfig ) ;
903+ } ) ;
904+
905+ afterEach ( ( ) => {
906+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
907+ } ) ;
908+
909+ describe ( 'readAfterEdit enabled' , ( ) => {
910+ beforeEach ( ( ) => {
911+ ( mockConfig . getReadAfterEdit as Mock ) . mockReturnValue ( true ) ;
912+ } ) ;
913+
914+ it ( 'should include file content in llmContent after successful edit' , async ( ) => {
915+ const testFile = 'test.txt' ;
916+ const filePath = path . join ( rootDir , testFile ) ;
917+ const initialContent = 'This is the original content.' ;
918+ const newContent = 'This is the modified content.' ;
919+
920+ fs . writeFileSync ( filePath , initialContent , 'utf8' ) ;
921+
922+ const params : EditToolParams = {
923+ file_path : filePath ,
924+ old_string : 'original' ,
925+ new_string : 'modified' ,
926+ } ;
927+
928+ const invocation = tool . build ( params ) ;
929+ const result = await invocation . execute ( new AbortController ( ) . signal ) ;
930+
931+ expect ( result . llmContent ) . toMatch ( / S u c c e s s f u l l y m o d i f i e d f i l e / ) ;
932+ expect ( result . llmContent ) . toContain ( newContent ) ;
933+ expect ( fs . readFileSync ( filePath , 'utf8' ) ) . toBe ( newContent ) ;
934+ } ) ;
935+
936+ it ( 'should include file content in llmContent when creating a new file' , async ( ) => {
937+ const newFileName = 'new_file.txt' ;
938+ const newFilePath = path . join ( rootDir , newFileName ) ;
939+ const fileContent = 'Content for the new file.' ;
940+
941+ const params : EditToolParams = {
942+ file_path : newFilePath ,
943+ old_string : '' ,
944+ new_string : fileContent ,
945+ } ;
946+
947+ ( mockConfig . getApprovalMode as Mock ) . mockReturnValueOnce (
948+ ApprovalMode . AUTO_EDIT ,
949+ ) ;
950+
951+ const invocation = tool . build ( params ) ;
952+ const result = await invocation . execute ( new AbortController ( ) . signal ) ;
953+
954+ expect ( result . llmContent ) . toMatch ( / C r e a t e d n e w f i l e / ) ;
955+ expect ( result . llmContent ) . toContain ( fileContent ) ;
956+ expect ( fs . existsSync ( newFilePath ) ) . toBe ( true ) ;
957+ expect ( fs . readFileSync ( newFilePath , 'utf8' ) ) . toBe ( fileContent ) ;
958+ } ) ;
959+
960+ it ( 'should include file content in llmContent when replacing multiple occurrences' , async ( ) => {
961+ const testFile = 'test.txt' ;
962+ const filePath = path . join ( rootDir , testFile ) ;
963+ const initialContent = 'old text old text old text' ;
964+ const expectedContent = 'new text new text new text' ;
965+
966+ fs . writeFileSync ( filePath , initialContent , 'utf8' ) ;
967+
968+ const params : EditToolParams = {
969+ file_path : filePath ,
970+ old_string : 'old' ,
971+ new_string : 'new' ,
972+ expected_replacements : 3 ,
973+ } ;
974+
975+ const invocation = tool . build ( params ) ;
976+ const result = await invocation . execute ( new AbortController ( ) . signal ) ;
977+
978+ expect ( result . llmContent ) . toMatch ( / S u c c e s s f u l l y m o d i f i e d f i l e / ) ;
979+ expect ( result . llmContent ) . toContain ( expectedContent ) ;
980+ expect ( fs . readFileSync ( filePath , 'utf8' ) ) . toBe ( expectedContent ) ;
981+ } ) ;
982+
983+ it ( 'should include file content even when user modified the new_string' , async ( ) => {
984+ const testFile = 'test.txt' ;
985+ const filePath = path . join ( rootDir , testFile ) ;
986+ const initialContent = 'This is some old text.' ;
987+ const newContent = 'This is some new text.' ;
988+
989+ fs . writeFileSync ( filePath , initialContent , 'utf8' ) ;
990+
991+ const params : EditToolParams = {
992+ file_path : filePath ,
993+ old_string : 'old' ,
994+ new_string : 'new' ,
995+ modified_by_user : true ,
996+ } ;
997+
998+ ( mockConfig . getApprovalMode as Mock ) . mockReturnValueOnce (
999+ ApprovalMode . AUTO_EDIT ,
1000+ ) ;
1001+
1002+ const invocation = tool . build ( params ) ;
1003+ const result = await invocation . execute ( new AbortController ( ) . signal ) ;
1004+
1005+ expect ( result . llmContent ) . toMatch (
1006+ / U s e r m o d i f i e d t h e ` n e w _ s t r i n g ` c o n t e n t / ,
1007+ ) ;
1008+ expect ( result . llmContent ) . toContain ( newContent ) ;
1009+ } ) ;
1010+ } ) ;
1011+
1012+ describe ( 'readAfterEdit disabled' , ( ) => {
1013+ beforeEach ( ( ) => {
1014+ ( mockConfig . getReadAfterEdit as Mock ) . mockReturnValue ( false ) ;
1015+ } ) ;
1016+
1017+ it ( 'should NOT include file content in llmContent after successful edit when disabled' , async ( ) => {
1018+ const testFile = 'test.txt' ;
1019+ const filePath = path . join ( rootDir , testFile ) ;
1020+ const initialContent = 'This is the original content.' ;
1021+ const newContent = 'This is the modified content.' ;
1022+
1023+ fs . writeFileSync ( filePath , initialContent , 'utf8' ) ;
1024+
1025+ const params : EditToolParams = {
1026+ file_path : filePath ,
1027+ old_string : 'original' ,
1028+ new_string : 'modified' ,
1029+ } ;
1030+
1031+ const invocation = tool . build ( params ) ;
1032+ const result = await invocation . execute ( new AbortController ( ) . signal ) ;
1033+
1034+ expect ( result . llmContent ) . toMatch ( / S u c c e s s f u l l y m o d i f i e d f i l e / ) ;
1035+ expect ( result . llmContent ) . not . toContain ( newContent ) ;
1036+ expect ( fs . readFileSync ( filePath , 'utf8' ) ) . toBe ( newContent ) ;
1037+ } ) ;
1038+
1039+ it ( 'should NOT include file content when creating a new file and feature is disabled' , async ( ) => {
1040+ const newFileName = 'new_file.txt' ;
1041+ const newFilePath = path . join ( rootDir , newFileName ) ;
1042+ const fileContent = 'Content for the new file.' ;
1043+
1044+ const params : EditToolParams = {
1045+ file_path : newFilePath ,
1046+ old_string : '' ,
1047+ new_string : fileContent ,
1048+ } ;
1049+
1050+ ( mockConfig . getApprovalMode as Mock ) . mockReturnValueOnce (
1051+ ApprovalMode . AUTO_EDIT ,
1052+ ) ;
1053+
1054+ const invocation = tool . build ( params ) ;
1055+ const result = await invocation . execute ( new AbortController ( ) . signal ) ;
1056+
1057+ expect ( result . llmContent ) . toMatch ( / C r e a t e d n e w f i l e / ) ;
1058+ expect ( result . llmContent ) . not . toContain ( fileContent ) ;
1059+ expect ( fs . existsSync ( newFilePath ) ) . toBe ( true ) ;
1060+ expect ( fs . readFileSync ( newFilePath , 'utf8' ) ) . toBe ( fileContent ) ;
1061+ } ) ;
1062+
1063+ it ( 'should NOT include file content when replacing multiple occurrences and feature is disabled' , async ( ) => {
1064+ const testFile = 'test.txt' ;
1065+ const filePath = path . join ( rootDir , testFile ) ;
1066+ const initialContent = 'old text old text old text' ;
1067+ const expectedContent = 'new text new text new text' ;
1068+
1069+ fs . writeFileSync ( filePath , initialContent , 'utf8' ) ;
1070+
1071+ const params : EditToolParams = {
1072+ file_path : filePath ,
1073+ old_string : 'old' ,
1074+ new_string : 'new' ,
1075+ expected_replacements : 3 ,
1076+ } ;
1077+
1078+ const invocation = tool . build ( params ) ;
1079+ const result = await invocation . execute ( new AbortController ( ) . signal ) ;
1080+
1081+ expect ( result . llmContent ) . toMatch ( / S u c c e s s f u l l y m o d i f i e d f i l e / ) ;
1082+ expect ( result . llmContent ) . not . toContain ( expectedContent ) ;
1083+ expect ( fs . readFileSync ( filePath , 'utf8' ) ) . toBe ( expectedContent ) ;
1084+ } ) ;
1085+ } ) ;
1086+
1087+ describe ( 'Error cases with readAfterEdit' , ( ) => {
1088+ beforeEach ( ( ) => {
1089+ ( mockConfig . getReadAfterEdit as Mock ) . mockReturnValue ( true ) ;
1090+ } ) ;
1091+
1092+ it ( 'should not include file content in llmContent when edit fails' , async ( ) => {
1093+ const testFile = 'test.txt' ;
1094+ const filePath = path . join ( rootDir , testFile ) ;
1095+ const initialContent = 'Some content.' ;
1096+
1097+ fs . writeFileSync ( filePath , initialContent , 'utf8' ) ;
1098+
1099+ const params : EditToolParams = {
1100+ file_path : filePath ,
1101+ old_string : 'nonexistent' ,
1102+ new_string : 'replacement' ,
1103+ } ;
1104+
1105+ const invocation = tool . build ( params ) ;
1106+ const result = await invocation . execute ( new AbortController ( ) . signal ) ;
1107+
1108+ expect ( result . llmContent ) . toMatch (
1109+ / 0 o c c u r r e n c e s f o u n d f o r o l d _ s t r i n g i n / ,
1110+ ) ;
1111+ expect ( result . llmContent ) . not . toContain ( initialContent ) ; // Should not include file content on error
1112+ expect ( fs . readFileSync ( filePath , 'utf8' ) ) . toBe ( initialContent ) ; // File should be unchanged
1113+ } ) ;
1114+
1115+ it ( 'should not include file content in llmContent when file already exists during creation' , async ( ) => {
1116+ const testFile = 'test.txt' ;
1117+ const filePath = path . join ( rootDir , testFile ) ;
1118+ const existingContent = 'Existing content' ;
1119+
1120+ fs . writeFileSync ( filePath , existingContent , 'utf8' ) ;
1121+
1122+ const params : EditToolParams = {
1123+ file_path : filePath ,
1124+ old_string : '' ,
1125+ new_string : 'new content' ,
1126+ } ;
1127+
1128+ const invocation = tool . build ( params ) ;
1129+ const result = await invocation . execute ( new AbortController ( ) . signal ) ;
1130+
1131+ expect ( result . llmContent ) . toMatch ( / F i l e a l r e a d y e x i s t s , c a n n o t c r e a t e / ) ;
1132+ expect ( result . llmContent ) . not . toContain ( existingContent ) ; // Should not include file content on error
1133+ expect ( fs . readFileSync ( filePath , 'utf8' ) ) . toBe ( existingContent ) ; // File should be unchanged
1134+ } ) ;
1135+ } ) ;
1136+ } ) ;
0 commit comments