2
0

edit.test.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. import { describe, expect, test } from "bun:test"
  2. import { replace } from "../../src/tool/edit"
  3. interface TestCase {
  4. content: string
  5. find: string
  6. replace: string
  7. all?: boolean
  8. fail?: boolean
  9. }
  10. const testCases: TestCase[] = [
  11. // SimpleReplacer cases
  12. {
  13. content: ["function hello() {", ' console.log("world");', "}"].join("\n"),
  14. find: 'console.log("world");',
  15. replace: 'console.log("universe");',
  16. },
  17. {
  18. content: ["if (condition) {", " doSomething();", " doSomethingElse();", "}"].join("\n"),
  19. find: [" doSomething();", " doSomethingElse();"].join("\n"),
  20. replace: [" doNewThing();", " doAnotherThing();"].join("\n"),
  21. },
  22. // LineTrimmedReplacer cases
  23. {
  24. content: ["function test() {", ' console.log("hello");', "}"].join("\n"),
  25. find: 'console.log("hello");',
  26. replace: 'console.log("goodbye");',
  27. },
  28. {
  29. content: ["const x = 5; ", "const y = 10;"].join("\n"),
  30. find: "const x = 5;",
  31. replace: "const x = 15;",
  32. },
  33. {
  34. content: [" if (true) {", " return false;", " }"].join("\n"),
  35. find: ["if (true) {", "return false;", "}"].join("\n"),
  36. replace: ["if (false) {", "return true;", "}"].join("\n"),
  37. },
  38. // BlockAnchorReplacer cases
  39. {
  40. content: [
  41. "function calculate(a, b) {",
  42. " const temp = a + b;",
  43. " const result = temp * 2;",
  44. " return result;",
  45. "}",
  46. ].join("\n"),
  47. find: ["function calculate(a, b) {", " // different middle content", " return result;", "}"].join("\n"),
  48. replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join("\n"),
  49. },
  50. {
  51. content: [
  52. "class MyClass {",
  53. " constructor() {",
  54. " this.value = 0;",
  55. " }",
  56. " ",
  57. " getValue() {",
  58. " return this.value;",
  59. " }",
  60. "}",
  61. ].join("\n"),
  62. find: ["class MyClass {", " // different implementation", "}"].join("\n"),
  63. replace: ["class MyClass {", " constructor() {", " this.value = 42;", " }", "}"].join("\n"),
  64. },
  65. // WhitespaceNormalizedReplacer cases
  66. {
  67. content: ["function test() {", '\tconsole.log("hello");', "}"].join("\n"),
  68. find: ' console.log("hello");',
  69. replace: ' console.log("world");',
  70. },
  71. {
  72. content: "const x = 5;",
  73. find: "const x = 5;",
  74. replace: "const x = 10;",
  75. },
  76. {
  77. content: "if\t( condition\t) {",
  78. find: "if ( condition ) {",
  79. replace: "if (newCondition) {",
  80. },
  81. // IndentationFlexibleReplacer cases
  82. {
  83. content: [" function nested() {", ' console.log("deeply nested");', " return true;", " }"].join(
  84. "\n",
  85. ),
  86. find: ["function nested() {", ' console.log("deeply nested");', " return true;", "}"].join("\n"),
  87. replace: ["function nested() {", ' console.log("updated");', " return false;", "}"].join("\n"),
  88. },
  89. {
  90. content: [" if (true) {", ' console.log("level 1");', ' console.log("level 2");', " }"].join("\n"),
  91. find: ["if (true) {", 'console.log("level 1");', ' console.log("level 2");', "}"].join("\n"),
  92. replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"),
  93. },
  94. // replaceAll option cases
  95. {
  96. content: ['console.log("test");', 'console.log("test");', 'console.log("test");'].join("\n"),
  97. find: 'console.log("test");',
  98. replace: 'console.log("updated");',
  99. all: true,
  100. },
  101. {
  102. content: ['console.log("test");', 'console.log("test");'].join("\n"),
  103. find: 'console.log("test");',
  104. replace: 'console.log("updated");',
  105. all: false,
  106. },
  107. // Error cases
  108. {
  109. content: 'console.log("hello");',
  110. find: "nonexistent string",
  111. replace: "updated",
  112. fail: true,
  113. },
  114. {
  115. content: ["test", "test", "different content", "test"].join("\n"),
  116. find: "test",
  117. replace: "updated",
  118. all: false,
  119. fail: true,
  120. },
  121. // Edge cases
  122. {
  123. content: "",
  124. find: "",
  125. replace: "new content",
  126. },
  127. {
  128. content: "const regex = /[.*+?^${}()|[\\\\]\\\\\\\\]/g;",
  129. find: "/[.*+?^${}()|[\\\\]\\\\\\\\]/g",
  130. replace: "/\\\\w+/g",
  131. },
  132. {
  133. content: 'const message = "Hello 世界! 🌍";',
  134. find: "Hello 世界! 🌍",
  135. replace: "Hello World! 🌎",
  136. },
  137. // EscapeNormalizedReplacer cases
  138. {
  139. content: 'console.log("Hello\nWorld");',
  140. find: 'console.log("Hello\\nWorld");',
  141. replace: 'console.log("Hello\nUniverse");',
  142. },
  143. {
  144. content: "const str = 'It's working';",
  145. find: "const str = 'It\\'s working';",
  146. replace: "const str = 'It's fixed';",
  147. },
  148. {
  149. content: "const template = `Hello ${name}`;",
  150. find: "const template = `Hello \\${name}`;",
  151. replace: "const template = `Hi ${name}`;",
  152. },
  153. {
  154. content: "const path = 'C:\\Users\\test';",
  155. find: "const path = 'C:\\\\Users\\\\test';",
  156. replace: "const path = 'C:\\Users\\admin';",
  157. },
  158. // MultiOccurrenceReplacer cases (with replaceAll)
  159. {
  160. content: ["debug('start');", "debug('middle');", "debug('end');"].join("\n"),
  161. find: "debug",
  162. replace: "log",
  163. all: true,
  164. },
  165. {
  166. content: "const x = 1; const y = 1; const z = 1;",
  167. find: "1",
  168. replace: "2",
  169. all: true,
  170. },
  171. // TrimmedBoundaryReplacer cases
  172. {
  173. content: [" function test() {", " return true;", " }"].join("\n"),
  174. find: ["function test() {", " return true;", "}"].join("\n"),
  175. replace: ["function test() {", " return false;", "}"].join("\n"),
  176. },
  177. {
  178. content: "\n const value = 42; \n",
  179. find: "const value = 42;",
  180. replace: "const value = 24;",
  181. },
  182. {
  183. content: ["", " if (condition) {", " doSomething();", " }", ""].join("\n"),
  184. find: ["if (condition) {", " doSomething();", "}"].join("\n"),
  185. replace: ["if (condition) {", " doNothing();", "}"].join("\n"),
  186. },
  187. // ContextAwareReplacer cases
  188. {
  189. content: [
  190. "function calculate(a, b) {",
  191. " const temp = a + b;",
  192. " const result = temp * 2;",
  193. " return result;",
  194. "}",
  195. ].join("\n"),
  196. find: [
  197. "function calculate(a, b) {",
  198. " // some different content here",
  199. " // more different content",
  200. " return result;",
  201. "}",
  202. ].join("\n"),
  203. replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join("\n"),
  204. },
  205. {
  206. content: [
  207. "class TestClass {",
  208. " constructor() {",
  209. " this.value = 0;",
  210. " }",
  211. " ",
  212. " method() {",
  213. " return this.value;",
  214. " }",
  215. "}",
  216. ].join("\n"),
  217. find: ["class TestClass {", " // different implementation", " // with multiple lines", "}"].join("\n"),
  218. replace: ["class TestClass {", " getValue() { return 42; }", "}"].join("\n"),
  219. },
  220. // Combined edge cases for new replacers
  221. {
  222. content: '\tconsole.log("test");\t',
  223. find: 'console.log("test");',
  224. replace: 'console.log("updated");',
  225. },
  226. {
  227. content: [" ", "function test() {", " return 'value';", "}", " "].join("\n"),
  228. find: ["function test() {", "return 'value';", "}"].join("\n"),
  229. replace: ["function test() {", "return 'new value';", "}"].join("\n"),
  230. },
  231. // Test for same oldString and newString (should fail)
  232. {
  233. content: 'console.log("test");',
  234. find: 'console.log("test");',
  235. replace: 'console.log("test");',
  236. fail: true,
  237. },
  238. // Additional tests for fixes made
  239. // WhitespaceNormalizedReplacer - test regex special characters that could cause errors
  240. {
  241. content: 'const pattern = "test[123]";',
  242. find: "test[123]",
  243. replace: "test[456]",
  244. },
  245. {
  246. content: 'const regex = "^start.*end$";',
  247. find: "^start.*end$",
  248. replace: "^begin.*finish$",
  249. },
  250. // EscapeNormalizedReplacer - test single backslash vs double backslash
  251. {
  252. content: 'const path = "C:\\Users";',
  253. find: 'const path = "C:\\Users";',
  254. replace: 'const path = "D:\\Users";',
  255. },
  256. {
  257. content: 'console.log("Line1\\nLine2");',
  258. find: 'console.log("Line1\\nLine2");',
  259. replace: 'console.log("First\\nSecond");',
  260. },
  261. // BlockAnchorReplacer - test edge case with exact newline boundaries
  262. {
  263. content: ["function test() {", " return true;", "}"].join("\n"),
  264. find: ["function test() {", " // middle", "}"].join("\n"),
  265. replace: ["function test() {", " return false;", "}"].join("\n"),
  266. },
  267. // ContextAwareReplacer - test with trailing newline in find string
  268. {
  269. content: ["class Test {", " method1() {", " return 1;", " }", "}"].join("\n"),
  270. find: [
  271. "class Test {",
  272. " // different content",
  273. "}",
  274. "", // trailing empty line
  275. ].join("\n"),
  276. replace: ["class Test {", " method2() { return 2; }", "}"].join("\n"),
  277. },
  278. // Test validation for empty strings with same oldString and newString
  279. {
  280. content: "",
  281. find: "",
  282. replace: "",
  283. fail: true,
  284. },
  285. // Test multiple occurrences with replaceAll=false (should fail)
  286. {
  287. content: ["const a = 1;", "const b = 1;", "const c = 1;"].join("\n"),
  288. find: "= 1",
  289. replace: "= 2",
  290. all: false,
  291. fail: true,
  292. },
  293. // Test whitespace normalization with multiple spaces and tabs mixed
  294. {
  295. content: "if\t \t( \tcondition\t )\t{",
  296. find: "if ( condition ) {",
  297. replace: "if (newCondition) {",
  298. },
  299. // Test escape sequences in template literals
  300. {
  301. content: "const msg = `Hello\\tWorld`;",
  302. find: "const msg = `Hello\\tWorld`;",
  303. replace: "const msg = `Hi\\tWorld`;",
  304. },
  305. // Test case that reproduces the greedy matching bug - now should fail due to low similarity
  306. {
  307. content: [
  308. "func main() {",
  309. " if condition {",
  310. " doSomething()",
  311. " }",
  312. " processData()",
  313. " if anotherCondition {",
  314. " doOtherThing()",
  315. " }",
  316. " return mainLayout",
  317. "}",
  318. "",
  319. "func helper() {",
  320. " }",
  321. " return mainLayout", // This should NOT be matched due to low similarity
  322. "}",
  323. ].join("\n"),
  324. find: [" }", " return mainLayout"].join("\n"),
  325. replace: [" }", " // Add some code here", " return mainLayout"].join("\n"),
  326. fail: true, // This should fail because the pattern has low similarity score
  327. },
  328. // Test case for the fix - more specific pattern should work
  329. {
  330. content: [
  331. "function renderLayout() {",
  332. " const header = createHeader()",
  333. " const body = createBody()",
  334. " return mainLayout",
  335. "}",
  336. ].join("\n"),
  337. find: ["function renderLayout() {", " // different content", " return mainLayout", "}"].join("\n"),
  338. replace: [
  339. "function renderLayout() {",
  340. " const header = createHeader()",
  341. " const body = createBody()",
  342. " // Add minimap overlay",
  343. " return mainLayout",
  344. "}",
  345. ].join("\n"),
  346. },
  347. // Test that large blocks without arbitrary size limits can work
  348. {
  349. content: Array.from({ length: 100 }, (_, i) => `line ${i}`).join("\n"),
  350. find: Array.from({ length: 50 }, (_, i) => `line ${i + 25}`).join("\n"),
  351. replace: Array.from({ length: 50 }, (_, i) => `updated line ${i + 25}`).join("\n"),
  352. },
  353. // Test case for the fix - more specific pattern should work
  354. {
  355. content: [
  356. "function renderLayout() {",
  357. " const header = createHeader()",
  358. " const body = createBody()",
  359. " return mainLayout",
  360. "}",
  361. ].join("\n"),
  362. find: ["function renderLayout() {", " // different content", " return mainLayout", "}"].join("\n"),
  363. replace: [
  364. "function renderLayout() {",
  365. " const header = createHeader()",
  366. " const body = createBody()",
  367. " // Add minimap overlay",
  368. " return mainLayout",
  369. "}",
  370. ].join("\n"),
  371. },
  372. // Test BlockAnchorReplacer with overly large blocks (should fail)
  373. {
  374. content:
  375. Array.from({ length: 100 }, (_, i) => `line ${i}`).join("\n") +
  376. "\nfunction test() {\n" +
  377. Array.from({ length: 60 }, (_, i) => ` content ${i}`).join("\n") +
  378. "\n return result\n}",
  379. find: ["function test() {", " // different content", " return result", "}"].join("\n"),
  380. replace: ["function test() {", " return 42", "}"].join("\n"),
  381. },
  382. ]
  383. describe("EditTool Replacers", () => {
  384. test.each(testCases)("case %#", (testCase) => {
  385. if (testCase.fail) {
  386. expect(() => {
  387. replace(testCase.content, testCase.find, testCase.replace, testCase.all)
  388. }).toThrow()
  389. } else {
  390. const result = replace(testCase.content, testCase.find, testCase.replace, testCase.all)
  391. expect(result).toContain(testCase.replace)
  392. }
  393. })
  394. })