diff --git a/src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts b/src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts index f5ae600bee..f6bc26d7cc 100644 --- a/src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts +++ b/src/core/assistant-message/__tests__/parseAssistantMessage.spec.ts @@ -271,6 +271,48 @@ const isEmptyTextContent = (block: AssistantMessageContent) => expect(toolUse.partial).toBe(false) }) + it("should handle whitespace and newlines between XML tags (Grok/Qwen format)", () => { + // This is the format that Grok and Qwen3-Coder generate that was causing issues + const message = ` + + +src/shared/infrastructure/supabase/factory.py + + +` + const result = parser(message).filter((block) => !isEmptyTextContent(block)) + + expect(result).toHaveLength(1) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("read_file") + // The args should be captured as a parameter + expect(toolUse.params.args).toBeDefined() + expect(toolUse.params.args).toContain("") + expect(toolUse.params.args).toContain("src/shared/infrastructure/supabase/factory.py") + expect(toolUse.partial).toBe(false) + }) + + it("should handle whitespace-only path values", () => { + const message = ` + + + + + +` + const result = parser(message).filter((block) => !isEmptyTextContent(block)) + + expect(result).toHaveLength(1) + const toolUse = result[0] as ToolUse + expect(toolUse.type).toBe("tool_use") + expect(toolUse.name).toBe("read_file") + expect(toolUse.params.args).toBeDefined() + // The whitespace-only path should be preserved in the args + expect(toolUse.params.args).toContain(" ") + expect(toolUse.partial).toBe(false) + }) + it("should handle multi-line parameters", () => { const message = `file.ts line 1 diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 7ba822dce0..fb6ba3d119 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -1325,6 +1325,132 @@ describe("read_file tool XML output structure", () => { `\n${testFilePath}Access to ${testFilePath} is blocked by the .rooignore file settings. You must try to continue in the task without using this file, or ask the user to update the .rooignore file.\n`, ) }) + + it("should handle XML args with whitespace and newlines (Grok/Qwen format)", async () => { + // This test reproduces the exact issue reported with Grok and Qwen3-Coder + // where XML has newlines and spaces between tags + const argsWithWhitespace = ` + + test/file.txt + +` + const toolUse: ReadFileToolUse = { + type: "tool_use", + name: "read_file", + params: { args: argsWithWhitespace }, + partial: false, + } + + // Setup mocks + mockedCountFileLines.mockResolvedValue(5) + mockedExtractTextFromFile.mockResolvedValue("1 | Line 1\n2 | Line 2\n3 | Line 3\n4 | Line 4\n5 | Line 5") + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) + + // Execute + await readFileTool( + mockCline, + toolUse, + mockCline.ask, + vi.fn(), + (result: ToolResponse) => { + toolResult = result + }, + (param: ToolParamName, content?: string) => content ?? "", + ) + + // Verify the file was read successfully (path was trimmed) + expect(toolResult).toContain("test/file.txt") + expect(toolResult).toContain('') + expect(toolResult).not.toContain("") + }) + + it("should handle multiple files with whitespace in XML", async () => { + // Test multiple files with varying whitespace + const argsWithMultipleFiles = ` + + + test/file1.txt + + + + test/file2.txt + + + test/file3.txt + +` + const toolUse: ReadFileToolUse = { + type: "tool_use", + name: "read_file", + params: { args: argsWithMultipleFiles }, + partial: false, + } + + // Setup mocks + mockedCountFileLines.mockResolvedValue(3) + mockedExtractTextFromFile.mockResolvedValue("1 | Content") + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) + + // Mock path.resolve for each file + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath.trim()}`) + + // Execute + await readFileTool( + mockCline, + toolUse, + mockCline.ask, + vi.fn(), + (result: ToolResponse) => { + toolResult = result + }, + (param: ToolParamName, content?: string) => content ?? "", + ) + + // Verify all files were processed + expect(toolResult).toContain("test/file1.txt") + expect(toolResult).toContain("test/file2.txt") + expect(toolResult).toContain("test/file3.txt") + expect(toolResult).not.toContain("") + }) + + it("should handle empty path after trimming whitespace", async () => { + // Test case where path is only whitespace + const argsWithEmptyPath = ` + + + +` + const toolUse: ReadFileToolUse = { + type: "tool_use", + name: "read_file", + params: { args: argsWithEmptyPath }, + partial: false, + } + + // Execute + await readFileTool( + mockCline, + toolUse, + mockCline.ask, + vi.fn(), + (result: ToolResponse) => { + toolResult = result + }, + (param: ToolParamName, content?: string) => content ?? "", + ) + + // Verify error is returned for empty path + expect(toolResult).toContain("") + expect(mockCline.sayAndCreateMissingParamError).toHaveBeenCalled() + }) }) }) diff --git a/src/utils/__tests__/xml.spec.ts b/src/utils/__tests__/xml.spec.ts index f7a282b0c0..1073d0c7cf 100644 --- a/src/utils/__tests__/xml.spec.ts +++ b/src/utils/__tests__/xml.spec.ts @@ -114,6 +114,120 @@ describe("parseXml", () => { expect(result.root.data.nestedXml).toHaveProperty("item", "Should not parse this") }) }) + + describe("whitespace handling", () => { + it("should handle spaces within path tags", () => { + const xml = ` + + + ./test/file.ts + + + ` + + const result = parseXml(xml) as any + + // The path should be trimmed + expect(result.args.file.path).toBe("./test/file.ts") + }) + + it("should handle newlines and spaces in nested tags", () => { + const xml = ` + + + + src/shared/infrastructure/supabase/factory.py + + + + ` + + const result = parseXml(xml) as any + + // The path should be trimmed + expect(result.args.file.path).toBe("src/shared/infrastructure/supabase/factory.py") + }) + + it("should handle multiple files with varying whitespace", () => { + const xml = ` + + + file1.ts + + + + file2.ts + + + + file3.ts + + + ` + + const result = parseXml(xml) as any + + // All paths should be trimmed + expect(Array.isArray(result.args.file)).toBe(true) + expect(result.args.file[0].path).toBe("file1.ts") + expect(result.args.file[1].path).toBe("file2.ts") + expect(result.args.file[2].path).toBe("file3.ts") + }) + + it("should handle empty or whitespace-only path tags", () => { + const xml = ` + + + + + + ` + + const result = parseXml(xml) as any + + // Empty string after trimming + expect(result.args.file.path).toBe("") + }) + + it("should handle tabs and mixed whitespace", () => { + const xml = ` + + + + ./path/with/tabs.ts + + + + ` + + const result = parseXml(xml) as any + + // Should trim tabs and newlines + expect(result.args.file.path).toBe("./path/with/tabs.ts") + }) + + it("should handle the exact format from Grok that was failing", () => { + // This is the exact format that was causing issues with Grok + const xml = ` + + +src/shared/infrastructure/supabase/factory.py + + +` + + // First extract just the args portion + const argsMatch = xml.match(/([\s\S]*?)<\/args>/) + expect(argsMatch).toBeTruthy() + + if (argsMatch) { + const argsXml = `${argsMatch[1]}` + const result = parseXml(argsXml) as any + + expect(result.args.file.path).toBe("src/shared/infrastructure/supabase/factory.py") + } + }) + }) }) describe("parseXmlForDiff", () => {