apply-diff.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. import * as assert from "assert"
  2. import * as fs from "fs/promises"
  3. import * as path from "path"
  4. import * as vscode from "vscode"
  5. import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
  6. import { waitFor, sleep } from "../utils"
  7. import { setDefaultSuiteTimeout } from "../test-utils"
  8. suite("Roo Code apply_diff Tool", function () {
  9. // Testing with more capable AI model to see if it can handle apply_diff complexity
  10. setDefaultSuiteTimeout(this)
  11. let workspaceDir: string
  12. // Pre-created test files that will be used across tests
  13. const testFiles = {
  14. simpleModify: {
  15. name: `test-file-simple-${Date.now()}.txt`,
  16. content: "Hello World\nThis is a test file\nWith multiple lines",
  17. path: "",
  18. },
  19. multipleReplace: {
  20. name: `test-func-multiple-${Date.now()}.js`,
  21. content: `function calculate(x, y) {
  22. const sum = x + y
  23. const product = x * y
  24. return { sum: sum, product: product }
  25. }`,
  26. path: "",
  27. },
  28. lineNumbers: {
  29. name: `test-lines-${Date.now()}.js`,
  30. content: `// Header comment
  31. function oldFunction() {
  32. console.log("Old implementation")
  33. }
  34. // Another function
  35. function keepThis() {
  36. console.log("Keep this")
  37. }
  38. // Footer comment`,
  39. path: "",
  40. },
  41. errorHandling: {
  42. name: `test-error-${Date.now()}.txt`,
  43. content: "Original content",
  44. path: "",
  45. },
  46. multiSearchReplace: {
  47. name: `test-multi-search-${Date.now()}.js`,
  48. content: `function processData(data) {
  49. console.log("Processing data")
  50. return data.map(item => item * 2)
  51. }
  52. // Some other code in between
  53. const config = {
  54. timeout: 5000,
  55. retries: 3
  56. }
  57. function validateInput(input) {
  58. console.log("Validating input")
  59. if (!input) {
  60. throw new Error("Invalid input")
  61. }
  62. return true
  63. }`,
  64. path: "",
  65. },
  66. }
  67. // Get the actual workspace directory that VSCode is using and create all test files
  68. suiteSetup(async function () {
  69. // Get the workspace folder from VSCode
  70. const workspaceFolders = vscode.workspace.workspaceFolders
  71. if (!workspaceFolders || workspaceFolders.length === 0) {
  72. throw new Error("No workspace folder found")
  73. }
  74. workspaceDir = workspaceFolders[0]!.uri.fsPath
  75. console.log("Using workspace directory:", workspaceDir)
  76. // Create all test files before any tests run
  77. console.log("Creating test files in workspace...")
  78. for (const [key, file] of Object.entries(testFiles)) {
  79. file.path = path.join(workspaceDir, file.name)
  80. await fs.writeFile(file.path, file.content)
  81. console.log(`Created ${key} test file at:`, file.path)
  82. }
  83. // Verify all files exist
  84. for (const [key, file] of Object.entries(testFiles)) {
  85. const exists = await fs
  86. .access(file.path)
  87. .then(() => true)
  88. .catch(() => false)
  89. if (!exists) {
  90. throw new Error(`Failed to create ${key} test file at ${file.path}`)
  91. }
  92. }
  93. })
  94. // Clean up after all tests
  95. suiteTeardown(async () => {
  96. // Cancel any running tasks before cleanup
  97. try {
  98. await globalThis.api.cancelCurrentTask()
  99. } catch {
  100. // Task might not be running
  101. }
  102. // Clean up all test files
  103. console.log("Cleaning up test files...")
  104. for (const [key, file] of Object.entries(testFiles)) {
  105. try {
  106. await fs.unlink(file.path)
  107. console.log(`Cleaned up ${key} test file`)
  108. } catch (error) {
  109. console.log(`Failed to clean up ${key} test file:`, error)
  110. }
  111. }
  112. })
  113. // Clean up before each test
  114. setup(async () => {
  115. // Cancel any previous task
  116. try {
  117. await globalThis.api.cancelCurrentTask()
  118. } catch {
  119. // Task might not be running
  120. }
  121. // Small delay to ensure clean state
  122. await sleep(100)
  123. })
  124. // Clean up after each test
  125. teardown(async () => {
  126. // Cancel the current task
  127. try {
  128. await globalThis.api.cancelCurrentTask()
  129. } catch {
  130. // Task might not be running
  131. }
  132. // Small delay to ensure clean state
  133. await sleep(100)
  134. })
  135. test("Should apply diff to modify existing file content", async function () {
  136. const api = globalThis.api
  137. const messages: ClineMessage[] = []
  138. const testFile = testFiles.simpleModify
  139. const expectedContent = "Hello Universe\nThis is a test file\nWith multiple lines"
  140. let taskCompleted = false
  141. let toolExecuted = false
  142. // Listen for messages
  143. const messageHandler = ({ message }: { message: ClineMessage }) => {
  144. messages.push(message)
  145. // Check for tool request
  146. if (message.type === "ask" && message.ask === "tool") {
  147. toolExecuted = true
  148. console.log("Tool requested")
  149. }
  150. }
  151. api.on(RooCodeEventName.Message, messageHandler)
  152. // Listen for task completion
  153. const taskCompletedHandler = (id: string) => {
  154. if (id === taskId) {
  155. taskCompleted = true
  156. }
  157. }
  158. api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
  159. let taskId: string
  160. try {
  161. // Start task - let AI read the file first, then apply diff
  162. taskId = await api.startNewTask({
  163. configuration: {
  164. mode: "code",
  165. autoApprovalEnabled: true,
  166. alwaysAllowWrite: true,
  167. alwaysAllowReadOnly: true,
  168. alwaysAllowReadOnlyOutsideWorkspace: true,
  169. },
  170. text: `The file ${testFile.name} exists in the workspace. Use the apply_diff tool to change "Hello World" to "Hello Universe" in this file.`,
  171. })
  172. console.log("Task ID:", taskId)
  173. // Wait for task completion
  174. await waitFor(() => taskCompleted, { timeout: 90_000 })
  175. // Verify tool was executed
  176. assert.ok(toolExecuted, "The apply_diff tool should have been executed")
  177. // Give time for file system operations
  178. await sleep(1000)
  179. // Verify file was modified correctly
  180. const actualContent = await fs.readFile(testFile.path, "utf-8")
  181. assert.strictEqual(
  182. actualContent.trim(),
  183. expectedContent.trim(),
  184. "File content should be modified correctly",
  185. )
  186. console.log("Test passed! File modified successfully")
  187. } finally {
  188. // Clean up
  189. api.off(RooCodeEventName.Message, messageHandler)
  190. api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
  191. }
  192. })
  193. test("Should apply multiple search/replace blocks in single diff", async function () {
  194. const api = globalThis.api
  195. const messages: ClineMessage[] = []
  196. const testFile = testFiles.multipleReplace
  197. let taskCompleted = false
  198. let toolExecuted = false
  199. // Listen for messages
  200. const messageHandler = ({ message }: { message: ClineMessage }) => {
  201. messages.push(message)
  202. // Check for tool request
  203. if (message.type === "ask" && message.ask === "tool") {
  204. toolExecuted = true
  205. console.log("Tool requested")
  206. }
  207. }
  208. api.on(RooCodeEventName.Message, messageHandler)
  209. // Listen for task completion
  210. const taskCompletedHandler = (id: string) => {
  211. if (id === taskId) {
  212. taskCompleted = true
  213. }
  214. }
  215. api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
  216. let taskId: string
  217. try {
  218. // Start task - let AI read file first
  219. taskId = await api.startNewTask({
  220. configuration: {
  221. mode: "code",
  222. autoApprovalEnabled: true,
  223. alwaysAllowWrite: true,
  224. alwaysAllowReadOnly: true,
  225. alwaysAllowReadOnlyOutsideWorkspace: true,
  226. },
  227. text: `The file ${testFile.name} exists in the workspace. Use the apply_diff tool to rename the function "calculate" to "compute" and rename the parameters "x, y" to "a, b". Also rename the variables "sum" to "total" and "product" to "result" throughout the function.`,
  228. })
  229. console.log("Task ID:", taskId)
  230. // Wait for task completion with longer timeout
  231. await waitFor(() => taskCompleted, { timeout: 90_000 })
  232. // Verify tool was executed
  233. assert.ok(toolExecuted, "The apply_diff tool should have been executed")
  234. // Give time for file system operations
  235. await sleep(1000)
  236. // Verify file was modified - check key changes were made
  237. const actualContent = await fs.readFile(testFile.path, "utf-8")
  238. assert.ok(
  239. actualContent.includes("function compute(a, b)"),
  240. "Function should be renamed to compute with params a, b",
  241. )
  242. assert.ok(actualContent.includes("const total = a + b"), "Variable sum should be renamed to total")
  243. assert.ok(actualContent.includes("const result = a * b"), "Variable product should be renamed to result")
  244. // Note: We don't strictly require object keys to be renamed as that's a reasonable interpretation difference
  245. console.log("Test passed! Multiple replacements applied successfully")
  246. } finally {
  247. // Clean up
  248. api.off(RooCodeEventName.Message, messageHandler)
  249. api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
  250. }
  251. })
  252. test("Should handle apply_diff with line number hints", async function () {
  253. const api = globalThis.api
  254. const messages: ClineMessage[] = []
  255. const testFile = testFiles.lineNumbers
  256. const expectedContent = `// Header comment
  257. function newFunction() {
  258. console.log("New implementation")
  259. }
  260. // Another function
  261. function keepThis() {
  262. console.log("Keep this")
  263. }
  264. // Footer comment`
  265. let taskCompleted = false
  266. let toolExecuted = false
  267. // Listen for messages
  268. const messageHandler = ({ message }: { message: ClineMessage }) => {
  269. messages.push(message)
  270. // Check for tool request
  271. if (message.type === "ask" && message.ask === "tool") {
  272. toolExecuted = true
  273. console.log("Tool requested")
  274. }
  275. }
  276. api.on(RooCodeEventName.Message, messageHandler)
  277. // Listen for task completion
  278. const taskCompletedHandler = (id: string) => {
  279. if (id === taskId) {
  280. taskCompleted = true
  281. }
  282. }
  283. api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
  284. let taskId: string
  285. try {
  286. // Start task - let AI read file first
  287. taskId = await api.startNewTask({
  288. configuration: {
  289. mode: "code",
  290. autoApprovalEnabled: true,
  291. alwaysAllowWrite: true,
  292. alwaysAllowReadOnly: true,
  293. alwaysAllowReadOnlyOutsideWorkspace: true,
  294. },
  295. text: `The file ${testFile.name} exists in the workspace. Use the apply_diff tool to change the function name "oldFunction" to "newFunction" and update its console.log message to "New implementation". Keep the rest of the file unchanged.`,
  296. })
  297. console.log("Task ID:", taskId)
  298. // Wait for task completion with longer timeout
  299. await waitFor(() => taskCompleted, { timeout: 90_000 })
  300. // Verify tool was executed
  301. assert.ok(toolExecuted, "The apply_diff tool should have been executed")
  302. // Give time for file system operations
  303. await sleep(1000)
  304. // Verify file was modified correctly
  305. const actualContent = await fs.readFile(testFile.path, "utf-8")
  306. assert.strictEqual(
  307. actualContent.trim(),
  308. expectedContent.trim(),
  309. "Only specified function should be modified",
  310. )
  311. console.log("Test passed! Targeted modification successful")
  312. } finally {
  313. // Clean up
  314. api.off(RooCodeEventName.Message, messageHandler)
  315. api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
  316. }
  317. })
  318. test("Should handle apply_diff errors gracefully", async function () {
  319. const api = globalThis.api
  320. const messages: ClineMessage[] = []
  321. const testFile = testFiles.errorHandling
  322. let taskCompleted = false
  323. let toolExecuted = false
  324. // Listen for messages
  325. const messageHandler = ({ message }: { message: ClineMessage }) => {
  326. messages.push(message)
  327. // Check for tool request
  328. if (message.type === "ask" && message.ask === "tool") {
  329. toolExecuted = true
  330. console.log("Tool requested")
  331. }
  332. }
  333. api.on(RooCodeEventName.Message, messageHandler)
  334. // Listen for task completion
  335. const taskCompletedHandler = (id: string) => {
  336. if (id === taskId) {
  337. taskCompleted = true
  338. }
  339. }
  340. api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
  341. let taskId: string
  342. try {
  343. // Start task with invalid search content
  344. taskId = await api.startNewTask({
  345. configuration: {
  346. mode: "code",
  347. autoApprovalEnabled: true,
  348. alwaysAllowWrite: true,
  349. alwaysAllowReadOnly: true,
  350. alwaysAllowReadOnlyOutsideWorkspace: true,
  351. },
  352. text: `The file ${testFile.name} exists in the workspace with content "Original content". Use the apply_diff tool to replace "This content does not exist" with "New content".
  353. IMPORTANT: The search pattern "This content does not exist" is NOT in the file. When apply_diff cannot find the search pattern, it should fail gracefully. Do NOT try to use write_to_file or any other tool.`,
  354. })
  355. console.log("Task ID:", taskId)
  356. // Wait for task completion
  357. await waitFor(() => taskCompleted, { timeout: 60_000 })
  358. // Verify tool was attempted
  359. assert.ok(toolExecuted, "The apply_diff tool should have been attempted")
  360. // Give time for file system operations
  361. await sleep(1000)
  362. // Verify file content remains unchanged
  363. const actualContent = await fs.readFile(testFile.path, "utf-8")
  364. assert.strictEqual(
  365. actualContent.trim(),
  366. testFile.content.trim(),
  367. "File content should remain unchanged when search pattern not found",
  368. )
  369. console.log("Test passed! Error handled gracefully")
  370. } finally {
  371. // Clean up
  372. api.off(RooCodeEventName.Message, messageHandler)
  373. api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
  374. }
  375. })
  376. test("Should apply multiple search/replace blocks to edit two separate functions", async function () {
  377. const api = globalThis.api
  378. const messages: ClineMessage[] = []
  379. const testFile = testFiles.multiSearchReplace
  380. const expectedContent = `function transformData(data) {
  381. console.log("Transforming data")
  382. return data.map(item => item * 2)
  383. }
  384. // Some other code in between
  385. const config = {
  386. timeout: 5000,
  387. retries: 3
  388. }
  389. function checkInput(input) {
  390. console.log("Checking input")
  391. if (!input) {
  392. throw new Error("Invalid input")
  393. }
  394. return true
  395. }`
  396. let taskCompleted = false
  397. let toolExecuted = false
  398. // Listen for messages
  399. const messageHandler = ({ message }: { message: ClineMessage }) => {
  400. messages.push(message)
  401. // Check for tool request
  402. if (message.type === "ask" && message.ask === "tool") {
  403. toolExecuted = true
  404. console.log("Tool requested")
  405. }
  406. }
  407. api.on(RooCodeEventName.Message, messageHandler)
  408. // Listen for task completion
  409. const taskCompletedHandler = (id: string) => {
  410. if (id === taskId) {
  411. taskCompleted = true
  412. }
  413. }
  414. api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
  415. let taskId: string
  416. try {
  417. // Start task to edit two separate functions
  418. taskId = await api.startNewTask({
  419. configuration: {
  420. mode: "code",
  421. autoApprovalEnabled: true,
  422. alwaysAllowWrite: true,
  423. alwaysAllowReadOnly: true,
  424. alwaysAllowReadOnlyOutsideWorkspace: true,
  425. },
  426. text: `Use the apply_diff tool on the file ${testFile.name} to make these changes using TWO SEPARATE search/replace blocks within a SINGLE apply_diff call:
  427. FIRST search/replace block: Edit the processData function to rename it to "transformData" and change "Processing data" to "Transforming data"
  428. SECOND search/replace block: Edit the validateInput function to rename it to "checkInput" and change "Validating input" to "Checking input"
  429. Important: Use multiple SEARCH/REPLACE blocks in one apply_diff call, NOT multiple apply_diff calls.
  430. The file already exists with this content:
  431. ${testFile.content}
  432. Assume the file exists and you can modify it directly.`,
  433. })
  434. console.log("Task ID:", taskId)
  435. // Wait for task completion
  436. await waitFor(() => taskCompleted, { timeout: 60_000 })
  437. // Verify tool was executed
  438. assert.ok(toolExecuted, "The apply_diff tool should have been executed")
  439. // Give time for file system operations
  440. await sleep(1000)
  441. // Verify file was modified correctly
  442. const actualContent = await fs.readFile(testFile.path, "utf-8")
  443. assert.strictEqual(actualContent.trim(), expectedContent.trim(), "Both functions should be modified")
  444. console.log("Test passed! Multiple search/replace blocks applied successfully")
  445. } finally {
  446. // Clean up
  447. api.off(RooCodeEventName.Message, messageHandler)
  448. api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
  449. }
  450. })
  451. })