@@ -10,15 +10,17 @@ describe('processStrReplace', () => {
1010
1111 const result = await processStrReplace (
1212 'test.ts' ,
13- [ { old : oldStr , new : newStr } ] ,
14-
13+ [ { old : oldStr , new : newStr , allowMultiple : false } ] ,
1514 Promise . resolve ( initialContent ) ,
1615 )
1716
1817 expect ( result ) . not . toBeNull ( )
19- expect ( ( result as any ) . content ) . toBe ( 'const x = 1;\nconst y = 3;\n' )
20- expect ( ( result as any ) . path ) . toBe ( 'test.ts' )
21- expect ( ( result as any ) . tool ) . toBe ( 'str_replace' )
18+ expect ( 'content' in result ) . toBe ( true )
19+ if ( 'content' in result ) {
20+ expect ( result . content ) . toBe ( 'const x = 1;\nconst y = 3;\n' )
21+ expect ( result . path ) . toBe ( 'test.ts' )
22+ expect ( result . tool ) . toBe ( 'str_replace' )
23+ }
2224 } )
2325
2426 it ( 'should handle Windows line endings' , async ( ) => {
@@ -28,13 +30,16 @@ describe('processStrReplace', () => {
2830
2931 const result = await processStrReplace (
3032 'test.ts' ,
31- [ { old : oldStr , new : newStr } ] ,
33+ [ { old : oldStr , new : newStr , allowMultiple : false } ] ,
3234 Promise . resolve ( initialContent ) ,
3335 )
3436
3537 expect ( result ) . not . toBeNull ( )
36- expect ( ( result as any ) . content ) . toBe ( 'const x = 1;\r\nconst y = 3;\r\n' )
37- expect ( ( result as any ) . patch ) . toContain ( '\r\n' )
38+ expect ( 'content' in result ) . toBe ( true )
39+ if ( 'content' in result ) {
40+ expect ( result . content ) . toBe ( 'const x = 1;\r\nconst y = 3;\r\n' )
41+ expect ( result . patch ) . toContain ( '\r\n' )
42+ }
3843 } )
3944
4045 it ( 'should handle indentation differences' , async ( ) => {
@@ -44,13 +49,15 @@ describe('processStrReplace', () => {
4449
4550 const result = await processStrReplace (
4651 'test.ts' ,
47- [ { old : oldStr , new : newStr } ] ,
48-
52+ [ { old : oldStr , new : newStr , allowMultiple : false } ] ,
4953 Promise . resolve ( initialContent ) ,
5054 )
5155
5256 expect ( result ) . not . toBeNull ( )
53- expect ( ( result as any ) . content ) . toBe ( ' const x = 1;\n const y = 3;\n' )
57+ expect ( 'content' in result ) . toBe ( true )
58+ if ( 'content' in result ) {
59+ expect ( result . content ) . toBe ( ' const x = 1;\n const y = 3;\n' )
60+ }
5461 } )
5562
5663 it ( 'should handle whitespace-only differences' , async ( ) => {
@@ -60,19 +67,21 @@ describe('processStrReplace', () => {
6067
6168 const result = await processStrReplace (
6269 'test.ts' ,
63- [ { old : oldStr , new : newStr } ] ,
64-
70+ [ { old : oldStr , new : newStr , allowMultiple : false } ] ,
6571 Promise . resolve ( initialContent ) ,
6672 )
6773
6874 expect ( result ) . not . toBeNull ( )
69- expect ( ( result as any ) . content ) . toBe ( 'const x = 1;\nconst y = 3;\n' )
75+ expect ( 'content' in result ) . toBe ( true )
76+ if ( 'content' in result ) {
77+ expect ( result . content ) . toBe ( 'const x = 1;\nconst y = 3;\n' )
78+ }
7079 } )
7180
72- it ( 'should return null if file content is null and oldStr is not empty' , async ( ) => {
81+ it ( 'should return error if file content is null and oldStr is not empty' , async ( ) => {
7382 const result = await processStrReplace (
7483 'test.ts' ,
75- [ { old : 'old' , new : 'new' } ] ,
84+ [ { old : 'old' , new : 'new' , allowMultiple : false } ] ,
7685 Promise . resolve ( null ) ,
7786 )
7887
@@ -83,10 +92,10 @@ describe('processStrReplace', () => {
8392 }
8493 } )
8594
86- it ( 'should return null if oldStr is empty and file exists' , async ( ) => {
95+ it ( 'should return error if oldStr is empty and file exists' , async ( ) => {
8796 const result = await processStrReplace (
8897 'test.ts' ,
89- [ { old : '' , new : 'new' } ] ,
98+ [ { old : '' , new : 'new' , allowMultiple : false } ] ,
9099 Promise . resolve ( 'content' ) ,
91100 )
92101
@@ -101,7 +110,7 @@ describe('processStrReplace', () => {
101110 const newContent = 'const x = 1;\nconst y = 2;\n'
102111 const result = await processStrReplace (
103112 'test.ts' ,
104- [ { old : '' , new : newContent } ] ,
113+ [ { old : '' , new : newContent , allowMultiple : false } ] ,
105114 Promise . resolve ( null ) ,
106115 )
107116
@@ -112,15 +121,14 @@ describe('processStrReplace', () => {
112121 }
113122 } )
114123
115- it ( 'should return null if no changes were made' , async ( ) => {
124+ it ( 'should return error if no changes were made' , async ( ) => {
116125 const initialContent = 'const x = 1;\nconst y = 2;\n'
117126 const oldStr = 'const z = 3;' // This string doesn't exist in the content
118127 const newStr = 'const z = 4;'
119128
120129 const result = await processStrReplace (
121130 'test.ts' ,
122- [ { old : oldStr , new : newStr } ] ,
123-
131+ [ { old : oldStr , new : newStr , allowMultiple : false } ] ,
124132 Promise . resolve ( initialContent ) ,
125133 )
126134
@@ -133,20 +141,22 @@ describe('processStrReplace', () => {
133141 }
134142 } )
135143
136- it ( 'should handle multiple occurrences of the same string' , async ( ) => {
144+ it ( 'should handle multiple occurrences of the same string with allowMultiple: true ' , async ( ) => {
137145 const initialContent = 'const x = 1;\nconst x = 2;\nconst x = 3;\n'
138146 const oldStr = 'const x'
139147 const newStr = 'let x'
140148
141149 const result = await processStrReplace (
142150 'test.ts' ,
143- [ { old : oldStr , new : newStr } ] ,
144-
151+ [ { old : oldStr , new : newStr , allowMultiple : true } ] ,
145152 Promise . resolve ( initialContent ) ,
146153 )
147154
148155 expect ( result ) . not . toBeNull ( )
149- expect ( ( result as any ) . content ) . toBe ( 'let x = 1;\nlet x = 2;\nlet x = 3;\n' )
156+ expect ( 'content' in result ) . toBe ( true )
157+ if ( 'content' in result ) {
158+ expect ( result . content ) . toBe ( 'let x = 1;\nlet x = 2;\nlet x = 3;\n' )
159+ }
150160 } )
151161
152162 it ( 'should generate a valid patch' , async ( ) => {
@@ -156,15 +166,18 @@ describe('processStrReplace', () => {
156166
157167 const result = await processStrReplace (
158168 'test.ts' ,
159- [ { old : oldStr , new : newStr } ] ,
169+ [ { old : oldStr , new : newStr , allowMultiple : false } ] ,
160170 Promise . resolve ( initialContent ) ,
161171 )
162172
163173 expect ( result ) . not . toBeNull ( )
164- const patch = ( result as any ) . patch
165- expect ( patch ) . toBeDefined ( )
166- expect ( patch ) . toContain ( '-const y = 2;' )
167- expect ( patch ) . toContain ( '+const y = 3;' )
174+ expect ( 'content' in result ) . toBe ( true )
175+ if ( 'content' in result ) {
176+ const patch = result . patch
177+ expect ( patch ) . toBeDefined ( )
178+ expect ( patch ) . toContain ( '-const y = 2;' )
179+ expect ( patch ) . toContain ( '+const y = 3;' )
180+ }
168181 } )
169182
170183 it ( 'should handle special characters in strings' , async ( ) => {
@@ -174,22 +187,25 @@ describe('processStrReplace', () => {
174187
175188 const result = await processStrReplace (
176189 'test.ts' ,
177- [ { old : oldStr , new : newStr } ] ,
190+ [ { old : oldStr , new : newStr , allowMultiple : false } ] ,
178191 Promise . resolve ( initialContent ) ,
179192 )
180193
181194 expect ( result ) . not . toBeNull ( )
182- expect ( ( result as any ) . content ) . toBe (
183- 'const x = "hello & world";\nconst y = "<span>";\n' ,
184- )
195+ expect ( 'content' in result ) . toBe ( true )
196+ if ( 'content' in result ) {
197+ expect ( result . content ) . toBe (
198+ 'const x = "hello & world";\nconst y = "<span>";\n' ,
199+ )
200+ }
185201 } )
186202
187203 it ( 'should continue processing other replacements even if one fails' , async ( ) => {
188204 const initialContent = 'const x = 1;\nconst y = 2;\nconst z = 3;\n'
189205 const replacements = [
190- { old : 'const x = 1;' , new : 'const x = 10;' } , // This exists
191- { old : 'const w = 4;' , new : 'const w = 40;' } , // This doesn't exist
192- { old : 'const z = 3;' , new : 'const z = 30;' } , // This also exists
206+ { old : 'const x = 1;' , new : 'const x = 10;' , allowMultiple : false } , // This exists
207+ { old : 'const w = 4;' , new : 'const w = 40;' , allowMultiple : false } , // This doesn't exist
208+ { old : 'const z = 3;' , new : 'const z = 30;' , allowMultiple : false } , // This also exists
193209 ]
194210
195211 const result = await processStrReplace (
@@ -210,4 +226,179 @@ describe('processStrReplace', () => {
210226 )
211227 }
212228 } )
229+
230+ // New comprehensive tests for allowMultiple functionality
231+ describe ( 'allowMultiple functionality' , ( ) => {
232+ it ( 'should error when multiple occurrences exist and allowMultiple is false' , async ( ) => {
233+ const initialContent = 'const x = 1;\nconst x = 2;\nconst x = 3;\n'
234+ const oldStr = 'const x'
235+ const newStr = 'let x'
236+
237+ const result = await processStrReplace (
238+ 'test.ts' ,
239+ [ { old : oldStr , new : newStr , allowMultiple : false } ] ,
240+ Promise . resolve ( initialContent ) ,
241+ )
242+
243+ expect ( result ) . not . toBeNull ( )
244+ expect ( 'error' in result ) . toBe ( true )
245+ if ( 'error' in result ) {
246+ expect ( result . error ) . toContain ( 'Found 3 occurrences' )
247+ expect ( result . error ) . toContain ( 'set allowMultiple to true' )
248+ }
249+ } )
250+
251+ it ( 'should replace all occurrences when allowMultiple is true' , async ( ) => {
252+ const initialContent = 'foo bar foo baz foo'
253+ const oldStr = 'foo'
254+ const newStr = 'FOO'
255+
256+ const result = await processStrReplace (
257+ 'test.ts' ,
258+ [ { old : oldStr , new : newStr , allowMultiple : true } ] ,
259+ Promise . resolve ( initialContent ) ,
260+ )
261+
262+ expect ( result ) . not . toBeNull ( )
263+ expect ( 'content' in result ) . toBe ( true )
264+ if ( 'content' in result ) {
265+
266+ expect ( result . content ) . toBe ( 'FOO bar FOO baz FOO' )
267+ }
268+ } )
269+
270+ it ( 'should handle single occurrence with allowMultiple: true' , async ( ) => {
271+ const initialContent = 'const x = 1;\nconst y = 2;\n'
272+ const oldStr = 'const y = 2;'
273+ const newStr = 'const y = 3;'
274+
275+ const result = await processStrReplace (
276+ 'test.ts' ,
277+ [ { old : oldStr , new : newStr , allowMultiple : true } ] ,
278+ Promise . resolve ( initialContent ) ,
279+ )
280+
281+ expect ( result ) . not . toBeNull ( )
282+ expect ( 'content' in result ) . toBe ( true )
283+ if ( 'content' in result ) {
284+ expect ( result . content ) . toBe ( 'const x = 1;\nconst y = 3;\n' )
285+ }
286+ } )
287+
288+ it ( 'should handle mixed allowMultiple settings in multiple replacements' , async ( ) => {
289+ const initialContent = 'foo bar foo\nbaz baz baz\nqux qux'
290+ const replacements = [
291+ { old : 'foo' , new : 'FOO' , allowMultiple : true } , // Replace all 'foo'
292+ { old : 'baz' , new : 'BAZ' , allowMultiple : false } , // Should error on multiple 'baz'
293+ { old : 'qux qux' , new : 'QUX' , allowMultiple : false } , // Single occurrence, should work
294+ ]
295+
296+ const result = await processStrReplace (
297+ 'test.ts' ,
298+ replacements ,
299+ Promise . resolve ( initialContent ) ,
300+ )
301+
302+ expect ( result ) . not . toBeNull ( )
303+ expect ( 'content' in result ) . toBe ( true )
304+ if ( 'content' in result ) {
305+ // Should have applied foo->FOO and qux qux->QUX, but not baz->BAZ
306+
307+ expect ( result . content ) . toBe ( 'FOO bar FOO\nbaz baz baz\nQUX' )
308+ expect ( result . messages ) . toContain ( 'Found 3 occurrences of "baz"' )
309+ expect ( result . messages ) . toContain ( 'set allowMultiple to true' )
310+ }
311+ } )
312+
313+ it ( 'should replace multiple lines with allowMultiple: true' , async ( ) => {
314+ const initialContent = `function test() {
315+ console.log('debug');
316+ }
317+ function test2() {
318+ console.log('debug');
319+ }
320+ function test3() {
321+ console.log('info');
322+ }`
323+ const oldStr = "console.log('debug');"
324+ const newStr = "// removed debug log"
325+
326+ const result = await processStrReplace (
327+ 'test.ts' ,
328+ [ { old : oldStr , new : newStr , allowMultiple : true } ] ,
329+ Promise . resolve ( initialContent ) ,
330+ )
331+
332+ expect ( result ) . not . toBeNull ( )
333+ expect ( 'content' in result ) . toBe ( true )
334+ if ( 'content' in result ) {
335+ expect ( result . content ) . toContain ( '// removed debug log' )
336+ // Should have replaced both debug logs but not the info log
337+ expect ( ( result . content . match ( / r e m o v e d d e b u g l o g / g) || [ ] ) . length ) . toBe ( 2 )
338+ expect ( result . content ) . toContain ( "console.log('info');" )
339+ }
340+ } )
341+
342+ it ( 'should handle empty new string with allowMultiple: true (deletion)' , async ( ) => {
343+ const initialContent = 'remove this, keep this, remove this, keep this'
344+ const oldStr = 'remove this, '
345+ const newStr = ''
346+
347+ const result = await processStrReplace (
348+ 'test.ts' ,
349+ [ { old : oldStr , new : newStr , allowMultiple : true } ] ,
350+ Promise . resolve ( initialContent ) ,
351+ )
352+
353+ expect ( result ) . not . toBeNull ( )
354+ expect ( 'content' in result ) . toBe ( true )
355+ if ( 'content' in result ) {
356+ expect ( result . content ) . toBe ( 'keep this, keep this' )
357+ }
358+ } )
359+
360+ it ( 'should handle allowMultiple with indentation matching' , async ( ) => {
361+ const initialContent = ` if (condition) {
362+ doSomething();
363+ }
364+ if (condition) {
365+ doSomething();
366+ }`
367+ const oldStr = 'doSomething();'
368+ const newStr = 'doSomethingElse();'
369+
370+ const result = await processStrReplace (
371+ 'test.ts' ,
372+ [ { old : oldStr , new : newStr , allowMultiple : true } ] ,
373+ Promise . resolve ( initialContent ) ,
374+ )
375+
376+ expect ( result ) . not . toBeNull ( )
377+ expect ( 'content' in result ) . toBe ( true )
378+ if ( 'content' in result ) {
379+ expect ( result . content ) . toContain ( 'doSomethingElse();' )
380+ expect ( ( result . content . match ( / d o S o m e t h i n g E l s e / g) || [ ] ) . length ) . toBe ( 2 )
381+ }
382+ } )
383+
384+ it ( 'should handle zero occurrences with allowMultiple: true' , async ( ) => {
385+ const initialContent = 'const x = 1;\nconst y = 2;\n'
386+ const oldStr = 'const z = 3;' // This string doesn't exist
387+ const newStr = 'const z = 4;'
388+
389+ const result = await processStrReplace (
390+ 'test.ts' ,
391+ [ { old : oldStr , new : newStr , allowMultiple : true } ] ,
392+ Promise . resolve ( initialContent ) ,
393+ )
394+
395+ expect ( result ) . not . toBeNull ( )
396+ expect ( 'error' in result ) . toBe ( true )
397+ if ( 'error' in result ) {
398+ expect ( result . error ) . toContain (
399+ 'The old string "const z = 3;" was not found' ,
400+ )
401+ }
402+ } )
403+ } )
213404} )
0 commit comments