diff --git a/.vscode/launch.json b/.vscode/launch.json index 5f023be65ba..eeec8648544 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,6 +24,56 @@ "group": "tasks", "order": 1 } + }, + { + "name": "Debug E2E Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/apps/vscode-e2e/test-workspace", + "--extensionDevelopmentPath=${workspaceFolder}/src", + "--extensionTestsPath=${workspaceFolder}/apps/vscode-e2e/out/suite/index" + ], + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/src/dist/**/*.js", "${workspaceFolder}/apps/vscode-e2e/out/**/*.js"], + "preLaunchTask": "build-e2e-tests", + "envFile": "${workspaceFolder}/apps/vscode-e2e/.env.local", + "env": { + "NODE_ENV": "development", + "VSCODE_DEBUG_MODE": "true" + }, + "resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"], + "presentation": { + "hidden": false, + "group": "tasks", + "order": 2 + } + }, + { + "name": "Debug E2E Tests (Quick - extension pre-built)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/apps/vscode-e2e/test-workspace", + "--extensionDevelopmentPath=${workspaceFolder}/src", + "--extensionTestsPath=${workspaceFolder}/apps/vscode-e2e/out/suite/index" + ], + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/src/dist/**/*.js", "${workspaceFolder}/apps/vscode-e2e/out/**/*.js"], + "preLaunchTask": "compile-e2e-only", + "envFile": "${workspaceFolder}/apps/vscode-e2e/.env.local", + "env": { + "NODE_ENV": "development", + "VSCODE_DEBUG_MODE": "true" + }, + "resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"], + "presentation": { + "hidden": false, + "group": "tasks", + "order": 4 + } } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 549a1174a92..a24f04ac8e9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -69,6 +69,63 @@ "group": "watch", "reveal": "always" } + }, + { + "label": "build-e2e-tests", + "dependsOn": ["build-e2e:bundle", "build-e2e:webview", "build-e2e:compile"], + "dependsOrder": "sequence", + "group": "build", + "problemMatcher": [] + }, + { + "label": "build-e2e:bundle", + "type": "shell", + "command": "pnpm -w bundle", + "group": "build", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "build-e2e:webview", + "type": "shell", + "command": "pnpm --filter @roo-code/vscode-webview build", + "group": "build", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "build-e2e:compile", + "type": "shell", + "command": "npx rimraf out; npx tsc -p tsconfig.json", + "options": { + "cwd": "${workspaceFolder}/apps/vscode-e2e" + }, + "group": "build", + "problemMatcher": "$tsc", + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "compile-e2e-only", + "type": "shell", + "command": "npx rimraf out; npx tsc -p tsconfig.json", + "options": { + "cwd": "${workspaceFolder}/apps/vscode-e2e" + }, + "group": "build", + "problemMatcher": "$tsc", + "presentation": { + "reveal": "always", + "panel": "shared" + } } ] } diff --git a/apps/vscode-e2e/src/suite/tools/apply-diff-native.test.ts b/apps/vscode-e2e/src/suite/tools/apply-diff-native.test.ts new file mode 100644 index 00000000000..41c24685374 --- /dev/null +++ b/apps/vscode-e2e/src/suite/tools/apply-diff-native.test.ts @@ -0,0 +1,1115 @@ +import * as assert from "assert" +import * as fs from "fs/promises" +import * as path from "path" +import * as vscode from "vscode" + +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" + +import { waitFor, sleep } from "../utils" +import { setDefaultSuiteTimeout } from "../test-utils" + +/** + * Native tool calling verification state. + * Tracks multiple indicators to ensure native protocol is actually being used. + * + * NOTE: Some verification approaches have been simplified because the underlying + * data (request body, response body, toolCallId in callbacks) is not exposed in + * the message events. We rely on: + * 1. apiProtocol field in api_req_started message + * 2. Successful tool execution with native configuration + * 3. Absence of XML tool tags in text responses + */ +interface NativeProtocolVerification { + /** Whether the apiProtocol field indicates native format (anthropic/openai) */ + hasNativeApiProtocol: boolean + /** The apiProtocol value received (for debugging) */ + apiProtocol: string | null + /** Whether the response text does NOT contain XML tool tags (confirming non-XML) */ + responseIsNotXML: boolean + /** Whether the tool was successfully executed (appliedDiff callback received) */ + toolWasExecuted: boolean + /** Tool name that was executed (for debugging) */ + executedToolName: string | null +} + +/** + * Creates a fresh verification state for tracking native protocol usage. + */ +function createVerificationState(): NativeProtocolVerification { + return { + hasNativeApiProtocol: false, + apiProtocol: null, + responseIsNotXML: true, // Assume true until we see XML + toolWasExecuted: false, + executedToolName: null, + } +} + +/** + * Asserts that native tool calling was actually used based on the verification state. + * Uses simplified verification based on available data: + * 1. apiProtocol field indicates native format + * 2. Tool was successfully executed + * 3. No XML tool tags in responses + */ +function assertNativeProtocolUsed(verification: NativeProtocolVerification, testName: string): void { + // Check that apiProtocol was set (indicates API was called) + assert.ok( + verification.apiProtocol !== null, + `[${testName}] apiProtocol should be set in api_req_started message. ` + + `This indicates an API request was made.`, + ) + + // Check that native protocol was actually used (anthropic/openai format) + assert.strictEqual( + verification.hasNativeApiProtocol, + true, + `[${testName}] Native API protocol should be used. ` + + `Expected apiProtocol to be "anthropic" or "openai", but got: ${verification.apiProtocol}`, + ) + + // Check that response doesn't contain XML tool tags + assert.strictEqual( + verification.responseIsNotXML, + true, + `[${testName}] Response should NOT contain XML tool tags. ` + + `Found XML tags which indicates XML protocol was used instead of native.`, + ) + + // Check that tool was executed + assert.strictEqual( + verification.toolWasExecuted, + true, + `[${testName}] Tool should have been executed. ` + `Executed tool: ${verification.executedToolName || "none"}`, + ) + + console.log(`[${testName}] ✓ Native protocol verification passed (simplified approach)`) + console.log(` - API Protocol: ${verification.apiProtocol}`) + console.log(` - Response is not XML: ${verification.responseIsNotXML}`) + console.log(` - Tool was executed: ${verification.toolWasExecuted}`) + console.log(` - Executed tool name: ${verification.executedToolName || "none"}`) +} + +/** + * Creates a message handler that tracks native protocol verification. + * Uses simplified verification based on available data: + * 1. apiProtocol field in api_req_started message + * 2. Tool execution callbacks (appliedDiff) + * 3. Absence of XML tool tags in text responses + */ +function createNativeVerificationHandler( + verification: NativeProtocolVerification, + messages: ClineMessage[], + options: { + onError?: (error: string) => void + onApplyDiffExecuted?: () => void + debugLogging?: boolean + } = {}, +): (event: { message: ClineMessage }) => void { + const { onError, onApplyDiffExecuted, debugLogging = true } = options + + return ({ message }: { message: ClineMessage }) => { + messages.push(message) + + // Debug logging + if (debugLogging) { + console.log(`[DEBUG] Message: type=${message.type}, say=${message.say}, ask=${message.ask}`) + } + + // Track errors + if (message.type === "say" && message.say === "error") { + const errorText = message.text || "Unknown error" + console.error("[ERROR]:", errorText) + onError?.(errorText) + } + + // === VERIFICATION 1: Check tool execution callbacks === + if (message.type === "ask" && message.ask === "tool") { + if (debugLogging) { + console.log("[DEBUG] Tool callback:", message.text?.substring(0, 300)) + } + + try { + const toolData = JSON.parse(message.text || "{}") + + // Track tool execution + if (toolData.tool) { + verification.toolWasExecuted = true + verification.executedToolName = toolData.tool + console.log(`[VERIFIED] Tool executed: ${toolData.tool}`) + } + + // Track apply_diff execution specifically + if (toolData.tool === "appliedDiff" || toolData.tool === "apply_diff") { + console.log("[TOOL] apply_diff tool executed") + onApplyDiffExecuted?.() + } + } catch (_e) { + // Not JSON, but still counts as tool execution attempt + if (debugLogging) { + console.log("[DEBUG] Tool callback not JSON:", message.text?.substring(0, 100)) + } + } + } + + // === VERIFICATION 2: Check API request for apiProtocol === + if (message.type === "say" && message.say === "api_req_started" && message.text) { + const rawText = message.text + if (debugLogging) { + console.log("[DEBUG] API request started:", rawText.substring(0, 200)) + } + + // Simple text check first (like original apply-diff.test.ts) + if (rawText.includes("apply_diff") || rawText.includes("appliedDiff")) { + verification.toolWasExecuted = true + verification.executedToolName = verification.executedToolName || "apply_diff" + console.log("[VERIFIED] Tool executed via raw text check: apply_diff") + onApplyDiffExecuted?.() + } + + try { + const requestData = JSON.parse(rawText) + + // Check for apiProtocol field (this IS available in the message) + if (requestData.apiProtocol) { + verification.apiProtocol = requestData.apiProtocol + // Native protocols use "anthropic" or "openai" format + if (requestData.apiProtocol === "anthropic" || requestData.apiProtocol === "openai") { + verification.hasNativeApiProtocol = true + console.log(`[VERIFIED] API Protocol: ${requestData.apiProtocol}`) + } + } + + // Also check parsed request content + if ( + requestData.request && + (requestData.request.includes("apply_diff") || requestData.request.includes("appliedDiff")) + ) { + verification.toolWasExecuted = true + verification.executedToolName = "apply_diff" + console.log(`[VERIFIED] Tool executed via parsed request: apply_diff`) + onApplyDiffExecuted?.() + } + } catch (e) { + console.log("[DEBUG] Failed to parse api_req_started message:", e) + } + } + + // === VERIFICATION 3: Check text responses for XML (should NOT be present) === + if (message.type === "say" && message.say === "text" && message.text) { + // Check for XML tool tags in AI text responses + const hasXMLToolTags = + message.text.includes("") || + message.text.includes("") || + message.text.includes("") || + message.text.includes("") + + if (hasXMLToolTags) { + verification.responseIsNotXML = false + console.log("[WARNING] Found XML tool tags in response - this indicates XML protocol") + } + } + + // Log completion results + if (message.type === "say" && message.say === "completion_result") { + if (debugLogging && message.text) { + console.log("[DEBUG] AI completion:", message.text.substring(0, 200)) + } + } + } +} + +suite("Roo Code apply_diff Tool (Native Tool Calling)", function () { + setDefaultSuiteTimeout(this) + + let workspaceDir: string + + // Pre-created test files that will be used across tests + const testFiles = { + simpleModify: { + name: `test-file-simple-native-${Date.now()}.txt`, + content: "Hello World\nThis is a test file\nWith multiple lines", + path: "", + }, + multipleReplace: { + name: `test-func-multiple-native-${Date.now()}.js`, + content: `function calculate(x, y) { + const sum = x + y + const product = x * y + return { sum: sum, product: product } +}`, + path: "", + }, + lineNumbers: { + name: `test-lines-native-${Date.now()}.js`, + content: `// Header comment +function oldFunction() { + console.log("Old implementation") +} + +// Another function +function keepThis() { + console.log("Keep this") +} + +// Footer comment`, + path: "", + }, + errorHandling: { + name: `test-error-native-${Date.now()}.txt`, + content: "Original content", + path: "", + }, + multiSearchReplace: { + name: `test-multi-search-native-${Date.now()}.js`, + content: `function processData(data) { + console.log("Processing data") + return data.map(item => item * 2) +} + +// Some other code in between +const config = { + timeout: 5000, + retries: 3 +} + +function validateInput(input) { + console.log("Validating input") + if (!input) { + throw new Error("Invalid input") + } + return true +}`, + path: "", + }, + } + + // Get the actual workspace directory that VSCode is using and create all test files + suiteSetup(async function () { + // Get the workspace folder from VSCode + const workspaceFolders = vscode.workspace.workspaceFolders + if (!workspaceFolders || workspaceFolders.length === 0) { + throw new Error("No workspace folder found") + } + workspaceDir = workspaceFolders[0]!.uri.fsPath + console.log("Using workspace directory:", workspaceDir) + + // Create all test files before any tests run + console.log("Creating test files in workspace...") + for (const [key, file] of Object.entries(testFiles)) { + file.path = path.join(workspaceDir, file.name) + await fs.writeFile(file.path, file.content) + console.log(`Created ${key} test file at:`, file.path) + } + + // Verify all files exist + for (const [key, file] of Object.entries(testFiles)) { + const exists = await fs + .access(file.path) + .then(() => true) + .catch(() => false) + if (!exists) { + throw new Error(`Failed to create ${key} test file at ${file.path}`) + } + } + }) + + // Clean up after all tests + suiteTeardown(async () => { + // Cancel any running tasks before cleanup + try { + await globalThis.api.cancelCurrentTask() + } catch { + // Task might not be running + } + + // Clean up all test files + console.log("Cleaning up test files...") + for (const [key, file] of Object.entries(testFiles)) { + try { + await fs.unlink(file.path) + console.log(`Cleaned up ${key} test file`) + } catch (error) { + console.log(`Failed to clean up ${key} test file:`, error) + } + } + }) + + // Clean up before each test + setup(async () => { + // Cancel any previous task + try { + await globalThis.api.cancelCurrentTask() + } catch { + // Task might not be running + } + + // Reset all test files to their original content before each test + // This ensures each test starts with a known clean state, even if a previous + // test or run modified the file content + for (const [key, file] of Object.entries(testFiles)) { + if (file.path) { + try { + await fs.writeFile(file.path, file.content) + console.log(`Reset ${key} test file to original content`) + } catch (error) { + console.log(`Failed to reset ${key} test file:`, error) + } + } + } + + // Small delay to ensure clean state + await sleep(100) + }) + + // Clean up after each test + teardown(async () => { + // Cancel the current task + try { + await globalThis.api.cancelCurrentTask() + } catch { + // Task might not be running + } + + // Small delay to ensure clean state + await sleep(100) + }) + + test("Should apply diff to modify existing file content using native tool calling", async function () { + const api = globalThis.api + const messages: ClineMessage[] = [] + const testFile = testFiles.simpleModify + const expectedContent = "Hello Universe\nThis is a test file\nWith multiple lines" + let taskStarted = false + let taskCompleted = false + let errorOccurred: string | null = null + let applyDiffExecuted = false + + // Create verification state for tracking native protocol + const verification = createVerificationState() + + // Create message handler with native verification + const messageHandler = createNativeVerificationHandler(verification, messages, { + onError: (error) => { + errorOccurred = error + }, + onApplyDiffExecuted: () => { + applyDiffExecuted = true + }, + debugLogging: true, + }) + api.on(RooCodeEventName.Message, messageHandler) + + // Listen for task events + const taskStartedHandler = (id: string) => { + if (id === taskId) { + taskStarted = true + console.log("Task started:", id) + } + } + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) + + const taskCompletedHandler = (id: string) => { + if (id === taskId) { + taskCompleted = true + console.log("Task completed:", id) + } + } + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) + + let taskId: string + try { + // Start task with native tool calling enabled via OpenRouter + taskId = await api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowWrite: true, + alwaysAllowReadOnly: true, + alwaysAllowReadOnlyOutsideWorkspace: true, + toolProtocol: "native", // Enable native tool calling + apiProvider: "openrouter", // Use OpenRouter provider + openRouterModelId: "openai/gpt-5.1", // GPT-5.1 supports native tools + }, + text: `Use apply_diff on the file ${testFile.name} to change "Hello World" to "Hello Universe". The file already exists with this content: +${testFile.content} + +Assume the file exists and you can modify it directly.`, + }) + + console.log("Task ID:", taskId) + console.log("Test filename:", testFile.name) + + // Wait for task to start + await waitFor(() => taskStarted, { timeout: 60_000 }) + + // Check for early errors + if (errorOccurred) { + console.error("Early error detected:", errorOccurred) + } + + // Wait for task completion + await waitFor(() => taskCompleted, { timeout: 60_000 }) + + // Give extra time for file system operations + await sleep(2000) + + // Check if the file was modified correctly + const actualContent = await fs.readFile(testFile.path, "utf-8") + console.log("File content after modification:", actualContent) + + // === COMPREHENSIVE NATIVE PROTOCOL VERIFICATION === + // This is the key assertion that ensures we're ACTUALLY testing native tool calling + assertNativeProtocolUsed(verification, "simpleModify") + + // Verify tool was executed + assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed") + + // Verify file content + assert.strictEqual( + actualContent.trim(), + expectedContent.trim(), + "File content should be modified correctly", + ) + + console.log( + "Test passed! apply_diff tool executed with VERIFIED native protocol and file modified successfully", + ) + } finally { + // Clean up + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) + } + }) + + test("Should apply multiple search/replace blocks in single diff using native tool calling", async function () { + const api = globalThis.api + const messages: ClineMessage[] = [] + const testFile = testFiles.multipleReplace + const expectedContent = `function compute(a, b) { + const total = a + b + const result = a * b + return { total: total, result: result } +}` + let taskStarted = false + let taskCompleted = false + let applyDiffExecuted = false + + // Create verification state for tracking native protocol + const verification = createVerificationState() + + // Create message handler with native verification + const messageHandler = createNativeVerificationHandler(verification, messages, { + onApplyDiffExecuted: () => { + applyDiffExecuted = true + }, + debugLogging: true, + }) + api.on(RooCodeEventName.Message, messageHandler) + + // Listen for task events + const taskStartedHandler = (id: string) => { + if (id === taskId) { + taskStarted = true + console.log("Task started:", id) + } + } + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) + + const taskCompletedHandler = (id: string) => { + if (id === taskId) { + taskCompleted = true + console.log("Task completed:", id) + } + } + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) + + let taskId: string + try { + // Start task with multiple replacements using native tool calling + taskId = await api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowWrite: true, + alwaysAllowReadOnly: true, + alwaysAllowReadOnlyOutsideWorkspace: true, + toolProtocol: "native", // Enable native tool calling + apiProvider: "openrouter", // Use OpenRouter provider + openRouterModelId: "openai/gpt-5.1", // GPT-5.1 supports native tools + }, + text: `Use apply_diff on the file ${testFile.name} to make ALL of these changes: +1. Rename function "calculate" to "compute" +2. Rename parameters "x, y" to "a, b" +3. Rename variable "sum" to "total" (including in the return statement) +4. Rename variable "product" to "result" (including in the return statement) +5. In the return statement, change { sum: sum, product: product } to { total: total, result: result } + +The file already exists with this content: +${testFile.content} + +Assume the file exists and you can modify it directly.`, + }) + + console.log("Task ID:", taskId) + console.log("Test filename:", testFile.name) + + // Wait for task to start + await waitFor(() => taskStarted, { timeout: 60_000 }) + + // Wait for task completion + await waitFor(() => taskCompleted, { timeout: 60_000 }) + + // Give extra time for file system operations + await sleep(2000) + + // Check the file was modified correctly + const actualContent = await fs.readFile(testFile.path, "utf-8") + console.log("File content after modification:", actualContent) + + // === COMPREHENSIVE NATIVE PROTOCOL VERIFICATION === + assertNativeProtocolUsed(verification, "multipleReplace") + + // Verify tool was executed + assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed") + + // Verify file content + assert.strictEqual( + actualContent.trim(), + expectedContent.trim(), + "All replacements should be applied correctly", + ) + + console.log( + "Test passed! apply_diff tool executed with VERIFIED native protocol and multiple replacements applied successfully", + ) + } finally { + // Clean up + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) + } + }) + + test("Should handle apply_diff with line number hints using native tool calling", async function () { + const api = globalThis.api + const messages: ClineMessage[] = [] + const testFile = testFiles.lineNumbers + const expectedContent = `// Header comment +function newFunction() { + console.log("New implementation") +} + +// Another function +function keepThis() { + console.log("Keep this") +} + +// Footer comment` + + let taskStarted = false + let taskCompleted = false + let applyDiffExecuted = false + + // Create verification state for tracking native protocol + const verification = createVerificationState() + + // Create message handler with native verification + const messageHandler = createNativeVerificationHandler(verification, messages, { + onApplyDiffExecuted: () => { + applyDiffExecuted = true + }, + debugLogging: true, + }) + api.on(RooCodeEventName.Message, messageHandler) + + // Listen for task events + const taskStartedHandler = (id: string) => { + if (id === taskId) { + taskStarted = true + } + } + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) + + const taskCompletedHandler = (id: string) => { + if (id === taskId) { + taskCompleted = true + } + } + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) + + let taskId: string + try { + // Start task with line number context using native tool calling + taskId = await api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowWrite: true, + alwaysAllowReadOnly: true, + alwaysAllowReadOnlyOutsideWorkspace: true, + toolProtocol: "native", // Enable native tool calling + apiProvider: "openrouter", // Use OpenRouter provider + openRouterModelId: "openai/gpt-5.1", // GPT-5.1 supports native tools + }, + text: `Use apply_diff on the file ${testFile.name} to change "oldFunction" to "newFunction" and update its console.log to "New implementation". Keep the rest of the file unchanged. + +The file already exists with this content: +${testFile.content} + +Assume the file exists and you can modify it directly.`, + }) + + console.log("Task ID:", taskId) + console.log("Test filename:", testFile.name) + + // Wait for task to start + await waitFor(() => taskStarted, { timeout: 60_000 }) + + // Wait for task completion + await waitFor(() => taskCompleted, { timeout: 60_000 }) + + // Give extra time for file system operations + await sleep(2000) + + // Check the file was modified correctly + const actualContent = await fs.readFile(testFile.path, "utf-8") + console.log("File content after modification:", actualContent) + + // === COMPREHENSIVE NATIVE PROTOCOL VERIFICATION === + assertNativeProtocolUsed(verification, "lineNumbers") + + // Verify tool was executed + assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed") + + // Verify file content + assert.strictEqual( + actualContent.trim(), + expectedContent.trim(), + "Only specified function should be modified", + ) + + console.log( + "Test passed! apply_diff tool executed with VERIFIED native protocol and targeted modification successful", + ) + } finally { + // Clean up + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) + } + }) + + test("Should handle apply_diff errors gracefully using native tool calling", async function () { + const api = globalThis.api + const messages: ClineMessage[] = [] + const testFile = testFiles.errorHandling + let taskStarted = false + let taskCompleted = false + let errorDetected = false + let applyDiffAttempted = false + let writeToFileUsed = false + + // Listen for messages + const messageHandler = ({ message }: { message: ClineMessage }) => { + messages.push(message) + + // Check for error messages + if (message.type === "say" && message.say === "error") { + errorDetected = true + console.log("Error detected:", message.text) + } + + // Check for tool execution attempt + if (message.type === "ask" && message.ask === "tool") { + console.log("Tool ASK request:", message.text?.substring(0, 500)) + try { + const toolData = JSON.parse(message.text || "{}") + if (toolData.tool === "appliedDiff") { + applyDiffAttempted = true + console.log("apply_diff tool attempted via ASK!") + } + // Detect if write_to_file was used (shows as editedExistingFile or newFileCreated) + if (toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") { + writeToFileUsed = true + console.log("write_to_file tool used!") + } + } catch (e) { + console.error(e) + } + } + + // Check for diff_error which indicates apply_diff was attempted but failed + if (message.type === "say" && message.say === "diff_error") { + applyDiffAttempted = true + console.log("diff_error detected - apply_diff was attempted") + } + + if (message.type === "say" && message.say === "api_req_started" && message.text) { + console.log("API request started:", message.text.substring(0, 200)) + } + } + api.on(RooCodeEventName.Message, messageHandler) + + // Listen for task events + const taskStartedHandler = (id: string) => { + if (id === taskId) { + taskStarted = true + } + } + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) + + const taskCompletedHandler = (id: string) => { + if (id === taskId) { + taskCompleted = true + } + } + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) + + let taskId: string + try { + // Start task with invalid search content using native tool calling + // The prompt is crafted to FORCE the AI to attempt the tool call + taskId = await api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowWrite: true, + alwaysAllowReadOnly: true, + alwaysAllowReadOnlyOutsideWorkspace: true, + reasoningEffort: "none", + toolProtocol: "native", // Enable native tool calling + apiProvider: "openrouter", + openRouterModelId: "openai/gpt-5.1", + }, + text: ` +--- +description: Test apply_diff tool error handling with non-existent patterns +argument-hint: [search-pattern] +--- + + +Test the apply_diff tool's error handling by attempting to replace a pattern that does not exist in the target file. +Target File: ${testFile.name} +Search pattern: "PATTERN_THAT_DOES_NOT_EXIST_xyz123" +Replacement: "REPLACEMENT_xyz123" + + + +This command verifies that apply_diff correctly handles and reports errors when: +- A search pattern is not found in the target file +- The tool gracefully fails with an informative error message +- Error handling works as expected for debugging workflows + + + + + Execute apply_diff directly +
+ Call apply_diff on the specified file with a non-existent search pattern. + Do NOT analyze the file first - the goal is to test error handling. +
+
+ + + Observe the error response +
+ The apply_diff tool should report that the pattern was not found. + This is the EXPECTED outcome - not a failure of the test. +
+
+ + + Report results +
+ Confirm whether the error handling worked correctly by reporting: + - The error message received + - Whether the tool behaved as expected +
+
+
+ + + + - YOU MUST call the apply_diff tool - this is non-negotiable + - Use the EXACT search pattern provided (or default: "PATTERN_THAT_DOES_NOT_EXIST_xyz123") + - Do NOT use write_to_file or any other file modification tool + - Do NOT analyze the file contents before calling apply_diff + - Do NOT refuse to call the tool - error handling verification is the purpose + + + + PATTERN_THAT_DOES_NOT_EXIST_xyz123 + REPLACEMENT_xyz123 + + + + + + Use this structure for the apply_diff call: + - path: The file specified by the user + - diff: A SEARCH/REPLACE block with the non-existent pattern + + + + \`\`\` + <<<<<<< SEARCH + :start_line:1 + ------- + PATTERN_THAT_DOES_NOT_EXIST_xyz123 + ======= + REPLACEMENT_xyz123 + >>>>>>> REPLACE + \`\`\` + + + + + + The test succeeds when apply_diff returns an error indicating the pattern was not found. + This confirms the tool's error handling is working correctly. + + + + After executing, report: + - Whether apply_diff was called: YES/NO + - Error message received: [actual error] + - Error handling status: WORKING/FAILED + + + + + - Only use the apply_diff tool + - Accept that "pattern not found" errors are the expected result + - Do not attempt to "fix" the test by finding real patterns + - This is a diagnostic/testing command, not a production workflow +`, + }) + + console.log("Task ID:", taskId) + console.log("Test filename:", testFile.name) + // Wait for task to start + await waitFor(() => taskStarted, { timeout: 90_000 }) + + // Wait for task completion or error + await waitFor(() => taskCompleted || errorDetected, { timeout: 90_000 }) + + // Give time for any final operations + await sleep(2000) + + // Read the file content + const actualContent = await fs.readFile(testFile.path, "utf-8") + console.log("File content after task:", actualContent) + console.log("applyDiffAttempted:", applyDiffAttempted) + console.log("writeToFileUsed:", writeToFileUsed) + + // The AI MUST have attempted to use apply_diff + assert.strictEqual(applyDiffAttempted, true, "apply_diff tool should have been attempted") + + // The AI should NOT have used write_to_file as a fallback + assert.strictEqual( + writeToFileUsed, + false, + "write_to_file should NOT be used when apply_diff fails - the AI should report the error instead", + ) + + // The content should remain unchanged since the search pattern wasn't found + assert.strictEqual( + actualContent.trim(), + testFile.content.trim(), + "File content should remain unchanged when search pattern not found", + ) + + console.log("Test passed! apply_diff attempted with native protocol and error handled gracefully") + } finally { + // Clean up + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) + } + }) + + test("Should apply multiple search/replace blocks to edit two separate functions using native tool calling", async function () { + const api = globalThis.api + const messages: ClineMessage[] = [] + const testFile = testFiles.multiSearchReplace + const expectedContent = `function transformData(data) { + console.log("Transforming data") + return data.map(item => item * 2) +} + +// Some other code in between +const config = { + timeout: 5000, + retries: 3 +} + +function checkInput(input) { + console.log("Checking input") + if (!input) { + throw new Error("Invalid input") + } + return true +}` + let taskStarted = false + let taskCompleted = false + let errorOccurred: string | null = null + let applyDiffExecuted = false + let applyDiffCount = 0 + + // Create verification state for tracking native protocol + const verification = createVerificationState() + + // Listen for messages + const messageHandler = ({ message }: { message: ClineMessage }) => { + messages.push(message) + + // Log important messages for debugging + if (message.type === "say" && message.say === "error") { + errorOccurred = message.text || "Unknown error" + console.error("Error:", message.text) + } + if (message.type === "ask" && message.ask === "tool") { + console.log("Tool request:", message.text?.substring(0, 200)) + try { + const toolData = JSON.parse(message.text || "{}") + // Track tool execution + if (toolData.tool) { + verification.toolWasExecuted = true + verification.executedToolName = toolData.tool + console.log(`[VERIFIED] Tool executed: ${toolData.tool}`) + } + if (toolData.tool === "appliedDiff") { + applyDiffExecuted = true + applyDiffCount++ + console.log(`apply_diff tool executed! (count: ${applyDiffCount})`) + } + } catch (_e) { + // Not JSON + } + } + if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) { + console.log("AI response:", message.text?.substring(0, 200)) + // Check for XML tool tags in text responses + if (message.say === "text" && message.text) { + const hasXMLToolTags = + message.text.includes("") || message.text.includes("") + if (hasXMLToolTags) { + verification.responseIsNotXML = false + console.log("[WARNING] Found XML tool tags in response") + } + } + } + + // Check for apiProtocol in api_req_started + if (message.type === "say" && message.say === "api_req_started" && message.text) { + console.log("API request started:", message.text.substring(0, 200)) + try { + const requestData = JSON.parse(message.text) + // Check for apiProtocol field + if (requestData.apiProtocol) { + verification.apiProtocol = requestData.apiProtocol + if (requestData.apiProtocol === "anthropic" || requestData.apiProtocol === "openai") { + verification.hasNativeApiProtocol = true + console.log(`[VERIFIED] API Protocol: ${requestData.apiProtocol}`) + } + } + } catch (e) { + console.log("Failed to parse api_req_started message:", e) + } + } + } + api.on(RooCodeEventName.Message, messageHandler) + + // Listen for task events + const taskStartedHandler = (id: string) => { + if (id === taskId) { + taskStarted = true + console.log("Task started:", id) + } + } + api.on(RooCodeEventName.TaskStarted, taskStartedHandler) + + const taskCompletedHandler = (id: string) => { + if (id === taskId) { + taskCompleted = true + console.log("Task completed:", id) + } + } + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) + + let taskId: string + try { + // Start task with instruction to edit two separate functions using native tool calling + taskId = await api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowWrite: true, + alwaysAllowReadOnly: true, + alwaysAllowReadOnlyOutsideWorkspace: true, + toolProtocol: "native", // Enable native tool calling + apiProvider: "openrouter", // Use OpenRouter provider + openRouterModelId: "openai/gpt-5.1", // GPT-5.1 supports native tools + }, + text: `Use apply_diff on the file ${testFile.name} to make these changes. You MUST use TWO SEPARATE search/replace blocks within a SINGLE apply_diff call: + +FIRST search/replace block: Edit the processData function to rename it to "transformData" and change "Processing data" to "Transforming data" + +SECOND search/replace block: Edit the validateInput function to rename it to "checkInput" and change "Validating input" to "Checking input" + +Important: Use multiple SEARCH/REPLACE blocks in one apply_diff call, NOT multiple apply_diff calls. Each function should have its own search/replace block. + +The file already exists with this content: +${testFile.content} + +Assume the file exists and you can modify it directly.`, + }) + + console.log("Task ID:", taskId) + console.log("Test filename:", testFile.name) + + // Wait for task to start + await waitFor(() => taskStarted, { timeout: 60_000 }) + + // Check for early errors + if (errorOccurred) { + console.error("Early error detected:", errorOccurred) + } + + // Wait for task completion + await waitFor(() => taskCompleted, { timeout: 60_000 }) + + // Give extra time for file system operations + await sleep(2000) + + // Check if the file was modified correctly + const actualContent = await fs.readFile(testFile.path, "utf-8") + console.log("File content after modification:", actualContent) + + // === COMPREHENSIVE NATIVE PROTOCOL VERIFICATION === + assertNativeProtocolUsed(verification, "multiSearchReplace") + + // Verify tool was executed + assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed") + console.log(`apply_diff was executed ${applyDiffCount} time(s)`) + + // Verify file content + assert.strictEqual( + actualContent.trim(), + expectedContent.trim(), + "Both functions should be modified with separate search/replace blocks", + ) + + console.log( + "Test passed! apply_diff tool executed with VERIFIED native protocol and multiple search/replace blocks applied successfully", + ) + } finally { + // Clean up + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskStarted, taskStartedHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) + } + }) +}) diff --git a/apps/vscode-e2e/src/suite/tools/apply-diff.test.ts b/apps/vscode-e2e/src/suite/tools/apply-diff.test.ts deleted file mode 100644 index c4f279f5f6d..00000000000 --- a/apps/vscode-e2e/src/suite/tools/apply-diff.test.ts +++ /dev/null @@ -1,750 +0,0 @@ -import * as assert from "assert" -import * as fs from "fs/promises" -import * as path from "path" -import * as vscode from "vscode" - -import { RooCodeEventName, type ClineMessage } from "@roo-code/types" - -import { waitFor, sleep } from "../utils" -import { setDefaultSuiteTimeout } from "../test-utils" - -suite.skip("Roo Code apply_diff Tool", function () { - setDefaultSuiteTimeout(this) - - let workspaceDir: string - - // Pre-created test files that will be used across tests - const testFiles = { - simpleModify: { - name: `test-file-simple-${Date.now()}.txt`, - content: "Hello World\nThis is a test file\nWith multiple lines", - path: "", - }, - multipleReplace: { - name: `test-func-multiple-${Date.now()}.js`, - content: `function calculate(x, y) { - const sum = x + y - const product = x * y - return { sum: sum, product: product } -}`, - path: "", - }, - lineNumbers: { - name: `test-lines-${Date.now()}.js`, - content: `// Header comment -function oldFunction() { - console.log("Old implementation") -} - -// Another function -function keepThis() { - console.log("Keep this") -} - -// Footer comment`, - path: "", - }, - errorHandling: { - name: `test-error-${Date.now()}.txt`, - content: "Original content", - path: "", - }, - multiSearchReplace: { - name: `test-multi-search-${Date.now()}.js`, - content: `function processData(data) { - console.log("Processing data") - return data.map(item => item * 2) -} - -// Some other code in between -const config = { - timeout: 5000, - retries: 3 -} - -function validateInput(input) { - console.log("Validating input") - if (!input) { - throw new Error("Invalid input") - } - return true -}`, - path: "", - }, - } - - // Get the actual workspace directory that VSCode is using and create all test files - suiteSetup(async function () { - // Get the workspace folder from VSCode - const workspaceFolders = vscode.workspace.workspaceFolders - if (!workspaceFolders || workspaceFolders.length === 0) { - throw new Error("No workspace folder found") - } - workspaceDir = workspaceFolders[0]!.uri.fsPath - console.log("Using workspace directory:", workspaceDir) - - // Create all test files before any tests run - console.log("Creating test files in workspace...") - for (const [key, file] of Object.entries(testFiles)) { - file.path = path.join(workspaceDir, file.name) - await fs.writeFile(file.path, file.content) - console.log(`Created ${key} test file at:`, file.path) - } - - // Verify all files exist - for (const [key, file] of Object.entries(testFiles)) { - const exists = await fs - .access(file.path) - .then(() => true) - .catch(() => false) - if (!exists) { - throw new Error(`Failed to create ${key} test file at ${file.path}`) - } - } - }) - - // Clean up after all tests - suiteTeardown(async () => { - // Cancel any running tasks before cleanup - try { - await globalThis.api.cancelCurrentTask() - } catch { - // Task might not be running - } - - // Clean up all test files - console.log("Cleaning up test files...") - for (const [key, file] of Object.entries(testFiles)) { - try { - await fs.unlink(file.path) - console.log(`Cleaned up ${key} test file`) - } catch (error) { - console.log(`Failed to clean up ${key} test file:`, error) - } - } - }) - - // Clean up before each test - setup(async () => { - // Cancel any previous task - try { - await globalThis.api.cancelCurrentTask() - } catch { - // Task might not be running - } - - // Small delay to ensure clean state - await sleep(100) - }) - - // Clean up after each test - teardown(async () => { - // Cancel the current task - try { - await globalThis.api.cancelCurrentTask() - } catch { - // Task might not be running - } - - // Small delay to ensure clean state - await sleep(100) - }) - - test("Should apply diff to modify existing file content", async function () { - // Increase timeout for this specific test - - const api = globalThis.api - const messages: ClineMessage[] = [] - const testFile = testFiles.simpleModify - const expectedContent = "Hello Universe\nThis is a test file\nWith multiple lines" - let taskStarted = false - let taskCompleted = false - let errorOccurred: string | null = null - let applyDiffExecuted = false - - // Listen for messages - const messageHandler = ({ message }: { message: ClineMessage }) => { - messages.push(message) - - // Log important messages for debugging - if (message.type === "say" && message.say === "error") { - errorOccurred = message.text || "Unknown error" - console.error("Error:", message.text) - } - if (message.type === "ask" && message.ask === "tool") { - console.log("Tool request:", message.text?.substring(0, 200)) - } - if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) { - console.log("AI response:", message.text?.substring(0, 200)) - } - - // Check for tool execution - if (message.type === "say" && message.say === "api_req_started" && message.text) { - console.log("API request started:", message.text.substring(0, 200)) - try { - const requestData = JSON.parse(message.text) - if (requestData.request && requestData.request.includes("apply_diff")) { - applyDiffExecuted = true - console.log("apply_diff tool executed!") - } - } catch (e) { - console.log("Failed to parse api_req_started message:", e) - } - } - } - api.on(RooCodeEventName.Message, messageHandler) - - // Listen for task events - const taskStartedHandler = (id: string) => { - if (id === taskId) { - taskStarted = true - console.log("Task started:", id) - } - } - api.on(RooCodeEventName.TaskStarted, taskStartedHandler) - - const taskCompletedHandler = (id: string) => { - if (id === taskId) { - taskCompleted = true - console.log("Task completed:", id) - } - } - api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) - - let taskId: string - try { - // Start task with apply_diff instruction - file already exists - taskId = await api.startNewTask({ - configuration: { - mode: "code", - autoApprovalEnabled: true, - alwaysAllowWrite: true, - alwaysAllowReadOnly: true, - alwaysAllowReadOnlyOutsideWorkspace: true, - }, - text: `Use apply_diff on the file ${testFile.name} to change "Hello World" to "Hello Universe". The file already exists with this content: -${testFile.content}\nAssume the file exists and you can modify it directly.`, - }) //Temporary measure since list_files ignores all the files inside a tmp workspace - - console.log("Task ID:", taskId) - console.log("Test filename:", testFile.name) - - // Wait for task to start - await waitFor(() => taskStarted, { timeout: 60_000 }) - - // Check for early errors - if (errorOccurred) { - console.error("Early error detected:", errorOccurred) - } - - // Wait for task completion - await waitFor(() => taskCompleted, { timeout: 60_000 }) - - // Give extra time for file system operations - await sleep(2000) - - // Check if the file was modified correctly - const actualContent = await fs.readFile(testFile.path, "utf-8") - console.log("File content after modification:", actualContent) - - // Verify tool was executed - assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed") - - // Verify file content - assert.strictEqual( - actualContent.trim(), - expectedContent.trim(), - "File content should be modified correctly", - ) - - console.log("Test passed! apply_diff tool executed and file modified successfully") - } finally { - // Clean up - api.off(RooCodeEventName.Message, messageHandler) - api.off(RooCodeEventName.TaskStarted, taskStartedHandler) - api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) - } - }) - - test("Should apply multiple search/replace blocks in single diff", async function () { - // Increase timeout for this specific test - - const api = globalThis.api - const messages: ClineMessage[] = [] - const testFile = testFiles.multipleReplace - const expectedContent = `function compute(a, b) { - const total = a + b - const result = a * b - return { total: total, result: result } -}` - let taskStarted = false - let taskCompleted = false - let applyDiffExecuted = false - - // Listen for messages - const messageHandler = ({ message }: { message: ClineMessage }) => { - messages.push(message) - if (message.type === "ask" && message.ask === "tool") { - console.log("Tool request:", message.text?.substring(0, 200)) - } - if (message.type === "say" && message.text) { - console.log("AI response:", message.text.substring(0, 200)) - } - - // Check for tool execution - if (message.type === "say" && message.say === "api_req_started" && message.text) { - console.log("API request started:", message.text.substring(0, 200)) - try { - const requestData = JSON.parse(message.text) - if (requestData.request && requestData.request.includes("apply_diff")) { - applyDiffExecuted = true - console.log("apply_diff tool executed!") - } - } catch (e) { - console.log("Failed to parse api_req_started message:", e) - } - } - } - api.on(RooCodeEventName.Message, messageHandler) - - // Listen for task events - const taskStartedHandler = (id: string) => { - if (id === taskId) { - taskStarted = true - console.log("Task started:", id) - } - } - api.on(RooCodeEventName.TaskStarted, taskStartedHandler) - - const taskCompletedHandler = (id: string) => { - if (id === taskId) { - taskCompleted = true - console.log("Task completed:", id) - } - } - api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) - - let taskId: string - try { - // Start task with multiple replacements - file already exists - taskId = await api.startNewTask({ - configuration: { - mode: "code", - autoApprovalEnabled: true, - alwaysAllowWrite: true, - alwaysAllowReadOnly: true, - alwaysAllowReadOnlyOutsideWorkspace: true, - }, - text: `Use apply_diff on the file ${testFile.name} to make ALL of these changes: -1. Rename function "calculate" to "compute" -2. Rename parameters "x, y" to "a, b" -3. Rename variable "sum" to "total" (including in the return statement) -4. Rename variable "product" to "result" (including in the return statement) -5. In the return statement, change { sum: sum, product: product } to { total: total, result: result } - -The file already exists with this content: -${testFile.content}\nAssume the file exists and you can modify it directly.`, - }) - - console.log("Task ID:", taskId) - console.log("Test filename:", testFile.name) - - // Wait for task to start - await waitFor(() => taskStarted, { timeout: 60_000 }) - - // Wait for task completion - await waitFor(() => taskCompleted, { timeout: 60_000 }) - - // Give extra time for file system operations - await sleep(2000) - - // Check the file was modified correctly - const actualContent = await fs.readFile(testFile.path, "utf-8") - console.log("File content after modification:", actualContent) - - // Verify tool was executed - assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed") - - // Verify file content - assert.strictEqual( - actualContent.trim(), - expectedContent.trim(), - "All replacements should be applied correctly", - ) - - console.log("Test passed! apply_diff tool executed and multiple replacements applied successfully") - } finally { - // Clean up - api.off(RooCodeEventName.Message, messageHandler) - api.off(RooCodeEventName.TaskStarted, taskStartedHandler) - api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) - } - }) - - test("Should handle apply_diff with line number hints", async function () { - // Increase timeout for this specific test - - const api = globalThis.api - const messages: ClineMessage[] = [] - const testFile = testFiles.lineNumbers - const expectedContent = `// Header comment -function newFunction() { - console.log("New implementation") -} - -// Another function -function keepThis() { - console.log("Keep this") -} - -// Footer comment` - - let taskStarted = false - let taskCompleted = false - let applyDiffExecuted = false - - // Listen for messages - const messageHandler = ({ message }: { message: ClineMessage }) => { - messages.push(message) - if (message.type === "ask" && message.ask === "tool") { - console.log("Tool request:", message.text?.substring(0, 200)) - } - - // Check for tool execution - if (message.type === "say" && message.say === "api_req_started" && message.text) { - console.log("API request started:", message.text.substring(0, 200)) - try { - const requestData = JSON.parse(message.text) - if (requestData.request && requestData.request.includes("apply_diff")) { - applyDiffExecuted = true - console.log("apply_diff tool executed!") - } - } catch (e) { - console.log("Failed to parse api_req_started message:", e) - } - } - } - api.on(RooCodeEventName.Message, messageHandler) - - // Listen for task events - const taskStartedHandler = (id: string) => { - if (id === taskId) { - taskStarted = true - } - } - api.on(RooCodeEventName.TaskStarted, taskStartedHandler) - - const taskCompletedHandler = (id: string) => { - if (id === taskId) { - taskCompleted = true - } - } - api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) - - let taskId: string - try { - // Start task with line number context - file already exists - taskId = await api.startNewTask({ - configuration: { - mode: "code", - autoApprovalEnabled: true, - alwaysAllowWrite: true, - alwaysAllowReadOnly: true, - alwaysAllowReadOnlyOutsideWorkspace: true, - }, - text: `Use apply_diff on the file ${testFile.name} to change "oldFunction" to "newFunction" and update its console.log to "New implementation". Keep the rest of the file unchanged. - -The file already exists with this content: -${testFile.content}\nAssume the file exists and you can modify it directly.`, - }) - - console.log("Task ID:", taskId) - console.log("Test filename:", testFile.name) - - // Wait for task to start - await waitFor(() => taskStarted, { timeout: 60_000 }) - - // Wait for task completion - await waitFor(() => taskCompleted, { timeout: 60_000 }) - - // Give extra time for file system operations - await sleep(2000) - - // Check the file was modified correctly - const actualContent = await fs.readFile(testFile.path, "utf-8") - console.log("File content after modification:", actualContent) - - // Verify tool was executed - assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed") - - // Verify file content - assert.strictEqual( - actualContent.trim(), - expectedContent.trim(), - "Only specified function should be modified", - ) - - console.log("Test passed! apply_diff tool executed and targeted modification successful") - } finally { - // Clean up - api.off(RooCodeEventName.Message, messageHandler) - api.off(RooCodeEventName.TaskStarted, taskStartedHandler) - api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) - } - }) - - test("Should handle apply_diff errors gracefully", async function () { - const api = globalThis.api - const messages: ClineMessage[] = [] - const testFile = testFiles.errorHandling - let taskStarted = false - let taskCompleted = false - let errorDetected = false - let applyDiffAttempted = false - - // Listen for messages - const messageHandler = ({ message }: { message: ClineMessage }) => { - messages.push(message) - - // Check for error messages - if (message.type === "say" && message.say === "error") { - errorDetected = true - console.log("Error detected:", message.text) - } - - // Check if AI mentions it couldn't find the content - if (message.type === "say" && message.text?.toLowerCase().includes("could not find")) { - errorDetected = true - console.log("AI reported search failure:", message.text) - } - - // Check for tool execution attempt - if (message.type === "say" && message.say === "api_req_started" && message.text) { - console.log("API request started:", message.text.substring(0, 200)) - try { - const requestData = JSON.parse(message.text) - if (requestData.request && requestData.request.includes("apply_diff")) { - applyDiffAttempted = true - console.log("apply_diff tool attempted!") - } - } catch (e) { - console.log("Failed to parse api_req_started message:", e) - } - } - } - api.on(RooCodeEventName.Message, messageHandler) - - // Listen for task events - const taskStartedHandler = (id: string) => { - if (id === taskId) { - taskStarted = true - } - } - api.on(RooCodeEventName.TaskStarted, taskStartedHandler) - - const taskCompletedHandler = (id: string) => { - if (id === taskId) { - taskCompleted = true - } - } - api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) - - let taskId: string - try { - // Start task with invalid search content - file already exists - taskId = await api.startNewTask({ - configuration: { - mode: "code", - autoApprovalEnabled: true, - alwaysAllowWrite: true, - alwaysAllowReadOnly: true, - alwaysAllowReadOnlyOutsideWorkspace: true, - }, - text: `Use apply_diff on the file ${testFile.name} to replace "This content does not exist" with "New content". - -The file already exists with this content: -${testFile.content} - -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 and the file content should remain unchanged. Do NOT try to use write_to_file or any other tool to modify the file. Only use apply_diff, and if the search pattern is not found, report that it could not be found. - -Assume the file exists and you can modify it directly.`, - }) - - console.log("Task ID:", taskId) - console.log("Test filename:", testFile.name) - // Wait for task to start - await waitFor(() => taskStarted, { timeout: 90_000 }) - - // Wait for task completion or error - await waitFor(() => taskCompleted || errorDetected, { timeout: 90_000 }) - - // Give time for any final operations - await sleep(2000) - - // The file content should remain unchanged since the search pattern wasn't found - const actualContent = await fs.readFile(testFile.path, "utf-8") - console.log("File content after task:", actualContent) - - // The AI should have attempted to use apply_diff - assert.strictEqual(applyDiffAttempted, true, "apply_diff tool should have been attempted") - - // The content should remain unchanged since the search pattern wasn't found - assert.strictEqual( - actualContent.trim(), - testFile.content.trim(), - "File content should remain unchanged when search pattern not found", - ) - - console.log("Test passed! apply_diff attempted and error handled gracefully") - } finally { - // Clean up - api.off(RooCodeEventName.Message, messageHandler) - api.off(RooCodeEventName.TaskStarted, taskStartedHandler) - api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) - } - }) - - test("Should apply multiple search/replace blocks to edit two separate functions", async function () { - const api = globalThis.api - const messages: ClineMessage[] = [] - const testFile = testFiles.multiSearchReplace - const expectedContent = `function transformData(data) { - console.log("Transforming data") - return data.map(item => item * 2) -} - -// Some other code in between -const config = { - timeout: 5000, - retries: 3 -} - -function checkInput(input) { - console.log("Checking input") - if (!input) { - throw new Error("Invalid input") - } - return true -}` - let taskStarted = false - let taskCompleted = false - let errorOccurred: string | null = null - let applyDiffExecuted = false - let applyDiffCount = 0 - - // Listen for messages - const messageHandler = ({ message }: { message: ClineMessage }) => { - messages.push(message) - - // Log important messages for debugging - if (message.type === "say" && message.say === "error") { - errorOccurred = message.text || "Unknown error" - console.error("Error:", message.text) - } - if (message.type === "ask" && message.ask === "tool") { - console.log("Tool request:", message.text?.substring(0, 200)) - } - if (message.type === "say" && (message.say === "completion_result" || message.say === "text")) { - console.log("AI response:", message.text?.substring(0, 200)) - } - - // Check for tool execution - if (message.type === "say" && message.say === "api_req_started" && message.text) { - console.log("API request started:", message.text.substring(0, 200)) - try { - const requestData = JSON.parse(message.text) - if (requestData.request && requestData.request.includes("apply_diff")) { - applyDiffExecuted = true - applyDiffCount++ - console.log(`apply_diff tool executed! (count: ${applyDiffCount})`) - } - } catch (e) { - console.log("Failed to parse api_req_started message:", e) - } - } - } - api.on(RooCodeEventName.Message, messageHandler) - - // Listen for task events - const taskStartedHandler = (id: string) => { - if (id === taskId) { - taskStarted = true - console.log("Task started:", id) - } - } - api.on(RooCodeEventName.TaskStarted, taskStartedHandler) - - const taskCompletedHandler = (id: string) => { - if (id === taskId) { - taskCompleted = true - console.log("Task completed:", id) - } - } - api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) - - let taskId: string - try { - // Start task with instruction to edit two separate functions using multiple search/replace blocks - taskId = await api.startNewTask({ - configuration: { - mode: "code", - autoApprovalEnabled: true, - alwaysAllowWrite: true, - alwaysAllowReadOnly: true, - alwaysAllowReadOnlyOutsideWorkspace: true, - }, - text: `Use apply_diff on the file ${testFile.name} to make these changes. You MUST use TWO SEPARATE search/replace blocks within a SINGLE apply_diff call: - -FIRST search/replace block: Edit the processData function to rename it to "transformData" and change "Processing data" to "Transforming data" - -SECOND search/replace block: Edit the validateInput function to rename it to "checkInput" and change "Validating input" to "Checking input" - -Important: Use multiple SEARCH/REPLACE blocks in one apply_diff call, NOT multiple apply_diff calls. Each function should have its own search/replace block. - -The file already exists with this content: -${testFile.content} - -Assume the file exists and you can modify it directly.`, - }) - - console.log("Task ID:", taskId) - console.log("Test filename:", testFile.name) - - // Wait for task to start - await waitFor(() => taskStarted, { timeout: 60_000 }) - - // Check for early errors - if (errorOccurred) { - console.error("Early error detected:", errorOccurred) - } - - // Wait for task completion - await waitFor(() => taskCompleted, { timeout: 60_000 }) - - // Give extra time for file system operations - await sleep(2000) - - // Check if the file was modified correctly - const actualContent = await fs.readFile(testFile.path, "utf-8") - console.log("File content after modification:", actualContent) - - // Verify tool was executed - assert.strictEqual(applyDiffExecuted, true, "apply_diff tool should have been executed") - console.log(`apply_diff was executed ${applyDiffCount} time(s)`) - - // Verify file content - assert.strictEqual( - actualContent.trim(), - expectedContent.trim(), - "Both functions should be modified with separate search/replace blocks", - ) - - console.log("Test passed! apply_diff tool executed and multiple search/replace blocks applied successfully") - } finally { - // Clean up - api.off(RooCodeEventName.Message, messageHandler) - api.off(RooCodeEventName.TaskStarted, taskStartedHandler) - api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) - } - }) -})