test: add comprehensive tests for array accessor and optional chaining in search and replace

- Added unit tests to verify correct handling of patterns like data?.[0]?.value
- Tests confirm the escapeRegExp function works correctly with these patterns
- Issue appears to be Gemini-specific parameter handling, not core functionality
This commit is contained in:
Roo Code 2025-09-23 18:15:16 +00:00
parent 44cbee5e75
commit 862163e79a
3 changed files with 382 additions and 0 deletions

View file

@ -0,0 +1,305 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { searchAndReplaceTool } from "../searchAndReplaceTool"
import { Task } from "../../task/Task"
import * as fs from "fs/promises"
import * as pathUtils from "../../../utils/path"
import * as fsUtils from "../../../utils/fs"
// Mock dependencies
vi.mock("fs/promises")
vi.mock("../../../utils/path")
vi.mock("../../../utils/fs")
vi.mock("delay", () => ({
default: vi.fn(() => Promise.resolve()),
}))
describe("searchAndReplaceTool", () => {
let mockCline: any
let mockAskApproval: any
let mockHandleError: any
let mockPushToolResult: any
let mockRemoveClosingTag: any
beforeEach(() => {
vi.clearAllMocks()
// Setup mock Cline instance
mockCline = {
cwd: "/test/workspace",
consecutiveMistakeCount: 0,
didEditFile: false,
recordToolError: vi.fn(),
recordToolUsage: vi.fn(),
sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing parameter error"),
say: vi.fn().mockResolvedValue(undefined),
ask: vi.fn().mockResolvedValue(undefined),
rooIgnoreController: {
validateAccess: vi.fn().mockReturnValue(true),
},
rooProtectedController: {
isWriteProtected: vi.fn().mockReturnValue(false),
},
fileContextTracker: {
trackFileContext: vi.fn().mockResolvedValue(undefined),
},
diffViewProvider: {
editType: undefined,
originalContent: undefined,
reset: vi.fn().mockResolvedValue(undefined),
open: vi.fn().mockResolvedValue(undefined),
update: vi.fn().mockResolvedValue(undefined),
scrollToFirstDiff: vi.fn(),
revertChanges: vi.fn().mockResolvedValue(undefined),
saveChanges: vi.fn().mockResolvedValue(undefined),
saveDirectly: vi.fn().mockResolvedValue(undefined),
pushToolWriteResult: vi.fn().mockResolvedValue("File modified successfully"),
},
providerRef: {
deref: vi.fn().mockReturnValue({
getState: vi.fn().mockResolvedValue({
diagnosticsEnabled: true,
writeDelayMs: 100,
experiments: {},
}),
}),
},
processQueuedMessages: vi.fn(),
}
// Setup other mocks
mockAskApproval = vi.fn().mockResolvedValue(true)
mockHandleError = vi.fn()
mockPushToolResult = vi.fn()
mockRemoveClosingTag = vi.fn((tag, value) => value || "")
// Mock file system operations
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true)
vi.mocked(pathUtils.getReadablePath).mockImplementation((cwd, path) => path || "")
})
describe("array accessor and optional chaining handling", () => {
it("should correctly handle simple array accessor patterns", async () => {
const fileContent = `const result = data?.items?.[0]?.values?.[1];`
const expectedContent = `const result = data?.items?.[0]?.values?.[2];`
vi.mocked(fs.readFile).mockResolvedValue(fileContent)
const block = {
name: "search_and_replace",
params: {
path: "test.js",
search: "data?.items?.[0]?.values?.[1]",
replace: "data?.items?.[0]?.values?.[2]",
use_regex: "false",
},
}
await searchAndReplaceTool(
mockCline as unknown as Task,
block as any,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify the file was read
expect(fs.readFile).toHaveBeenCalledWith("/test/workspace/test.js", "utf-8")
// Verify the diff view was updated with correct content
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(expectedContent, true)
// Verify no errors occurred
expect(mockHandleError).not.toHaveBeenCalled()
expect(mockCline.recordToolError).not.toHaveBeenCalled()
})
it("should handle complex nested array accessors", async () => {
const fileContent = `const value = obj?.nested?.arrays?.[0]?.[1]?.id;`
const expectedContent = `const value = obj?.nested?.arrays?.[1]?.[0]?.id;`
vi.mocked(fs.readFile).mockResolvedValue(fileContent)
const block = {
name: "search_and_replace",
params: {
path: "test.js",
search: "obj?.nested?.arrays?.[0]?.[1]?.id",
replace: "obj?.nested?.arrays?.[1]?.[0]?.id",
use_regex: "false",
},
}
await searchAndReplaceTool(
mockCline as unknown as Task,
block as any,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify the diff view was updated with correct content
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(expectedContent, true)
// Verify success
expect(mockPushToolResult).toHaveBeenCalledWith("File modified successfully")
expect(mockCline.recordToolUsage).toHaveBeenCalledWith("search_and_replace")
})
it("should handle multiple occurrences of array accessor patterns", async () => {
const fileContent = `
const a = data?.[0]?.value;
const b = data?.[0]?.value;
const c = other?.[0]?.value;
`
const expectedContent = `
const a = data?.[1]?.value;
const b = data?.[1]?.value;
const c = other?.[0]?.value;
`
vi.mocked(fs.readFile).mockResolvedValue(fileContent)
const block = {
name: "search_and_replace",
params: {
path: "test.js",
search: "data?.[0]?.value",
replace: "data?.[1]?.value",
use_regex: "false",
},
}
await searchAndReplaceTool(
mockCline as unknown as Task,
block as any,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify the diff view was updated with correct content
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(expectedContent, true)
})
it("should handle bracket notation without optional chaining", async () => {
const fileContent = `const value = data[0].items[1];`
const expectedContent = `const value = data[1].items[2];`
vi.mocked(fs.readFile).mockResolvedValue(fileContent)
const block = {
name: "search_and_replace",
params: {
path: "test.js",
search: "data[0].items[1]",
replace: "data[1].items[2]",
use_regex: "false",
},
}
await searchAndReplaceTool(
mockCline as unknown as Task,
block as any,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify the diff view was updated with correct content
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(expectedContent, true)
})
it("should handle mixed dot and bracket notation", async () => {
const fileContent = `const result = obj.prop?.[0]?.nested.value?.[1];`
const expectedContent = `const result = obj.prop?.[1]?.nested.value?.[0];`
vi.mocked(fs.readFile).mockResolvedValue(fileContent)
const block = {
name: "search_and_replace",
params: {
path: "test.js",
search: "obj.prop?.[0]?.nested.value?.[1]",
replace: "obj.prop?.[1]?.nested.value?.[0]",
use_regex: "false",
},
}
await searchAndReplaceTool(
mockCline as unknown as Task,
block as any,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify the diff view was updated with correct content
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(expectedContent, true)
})
})
describe("existing functionality", () => {
it("should handle regular text replacement", async () => {
const fileContent = `function oldFunction() { return "old"; }`
const expectedContent = `function newFunction() { return "old"; }`
vi.mocked(fs.readFile).mockResolvedValue(fileContent)
const block = {
name: "search_and_replace",
params: {
path: "test.js",
search: "oldFunction",
replace: "newFunction",
use_regex: "false",
},
}
await searchAndReplaceTool(
mockCline as unknown as Task,
block as any,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify the diff view was updated with correct content
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(expectedContent, true)
})
it("should handle regex patterns when use_regex is true", async () => {
const fileContent = `var x = 10; var y = 20; var z = 30;`
const expectedContent = `let x = 10; let y = 20; let z = 30;`
vi.mocked(fs.readFile).mockResolvedValue(fileContent)
const block = {
name: "search_and_replace",
params: {
path: "test.js",
search: "\\bvar\\b",
replace: "let",
use_regex: "true",
},
}
await searchAndReplaceTool(
mockCline as unknown as Task,
block as any,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify the diff view was updated with correct content
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(expectedContent, true)
})
})
})

30
test-array-accessor.js Normal file
View file

@ -0,0 +1,30 @@
// Test file for array accessor and optional chaining issue
const data = {
items: [
{ name: "first", values: [1, 2, 3] },
{ name: "second", values: [4, 5, 6] },
],
}
// Original code with array accessors and optional chaining
const result1 = data?.items?.[0]?.values?.[1]
const result2 = data?.items?.[1]?.values?.[2]
// More complex example
const complexData = {
nested: {
arrays: [
[{ id: 1 }, { id: 2 }],
[{ id: 3 }, { id: 4 }],
],
},
}
const complexResult = complexData?.nested?.arrays?.[0]?.[1]?.id
// Function with optional chaining
function getValue(obj) {
return obj?.data?.[0]?.value?.[1]
}
console.log("Results:", result1, result2, complexResult)

47
test-escape-regex.js Normal file
View file

@ -0,0 +1,47 @@
// Test the escapeRegExp function behavior
function escapeRegExp(input) {
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
// Test cases with array accessors and optional chaining
const testCases = [
"data?.[0]?.value",
"obj?.items?.[0]?.values?.[1]",
"array[0].item[1]",
"data?.nested?.arrays?.[0]?.[1]?.id",
]
console.log("Testing escapeRegExp function:")
console.log("==============================")
testCases.forEach((testCase) => {
const escaped = escapeRegExp(testCase)
console.log(`Original: ${testCase}`)
console.log(`Escaped: ${escaped}`)
// Create a regex with the escaped pattern
const regex = new RegExp(escaped, "g")
// Test if it matches the original string
const matches = testCase.match(regex)
console.log(`Matches original: ${matches ? "YES" : "NO"}`)
console.log("---")
})
// Now test what happens when we try to replace
const fileContent = `const result = data?.[0]?.value;`
const searchPattern = "data?.[0]?.value"
const replacePattern = "data?.[1]?.value"
console.log("\nTesting replacement:")
console.log("====================")
console.log(`File content: ${fileContent}`)
console.log(`Search for: ${searchPattern}`)
console.log(`Replace with: ${replacePattern}`)
// Using escaped pattern (current behavior)
const escapedSearch = escapeRegExp(searchPattern)
const regexEscaped = new RegExp(escapedSearch, "g")
const resultEscaped = fileContent.replace(regexEscaped, replacePattern)
console.log(`\nWith escaping: ${resultEscaped}`)
console.log(`Success: ${resultEscaped !== fileContent ? "YES" : "NO"}`)