Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion src/core/tools/__tests__/writeToFileTool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,82 @@ describe("writeToFileTool", () => {
})

it("processes files with very large line counts", async () => {
await executeWriteFileTool({ line_count: "999999" })
// Create content that matches the line count to avoid mismatch error
const largeContent = Array(999).fill("Line content").join("\n")
await executeWriteFileTool({
content: largeContent,
line_count: "999",
})

// Should process normally without issues
expect(mockCline.consecutiveMistakeCount).toBe(0)
})

it("detects potential truncation when line count is 0 and content exceeds 7000 lines", async () => {
// Create content with more than 7000 lines
const largeContent = Array(7500).fill("Line content").join("\n")

await executeWriteFileTool({
content: largeContent,
line_count: "0", // Missing line count
})

expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file")
expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("7500 lines"))
expect(mockCline.diffViewProvider.reset).toHaveBeenCalled()
})

it("detects line count mismatch indicating truncation", async () => {
// Create content with 7001 lines but claim it should have 10000
const truncatedContent = Array(7001).fill("Line content").join("\n")

// Need to capture the tool result
mockPushToolResult = vi.fn((result: ToolResponse) => {
toolResult = result
})

await executeWriteFileTool({
content: truncatedContent,
line_count: "10000", // Expected more lines
})

expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file")
expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("Line count mismatch"))
// The error message should mention truncation since we have >7000 lines
expect(toolResult).toContain("Content appears to be truncated")
expect(toolResult).toContain("7001 lines")
expect(mockCline.diffViewProvider.revertChanges).toHaveBeenCalled()
})

it("allows small line count differences within tolerance", async () => {
// Create content with 95 lines when expecting 100 (5% difference)
const content = Array(95).fill("Line content").join("\n")

await executeWriteFileTool({
content: content,
line_count: "100",
})

// Should process normally as difference is within 10% tolerance
expect(mockCline.consecutiveMistakeCount).toBe(0)
expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled()
})

it("handles exact line count match for large files", async () => {
// Create content with exactly 8000 lines
const largeContent = Array(8000).fill("Line content").join("\n")

await executeWriteFileTool({
content: largeContent,
line_count: "8000",
})

// Should process normally
expect(mockCline.consecutiveMistakeCount).toBe(0)
expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled()
})
})

describe("partial block handling", () => {
Expand Down
59 changes: 59 additions & 0 deletions src/core/tools/writeToFileTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,29 @@ export async function writeToFileTool(
cline.diffViewProvider.editType = fileExists ? "modify" : "create"
}

// Check for potential truncation in large files
const actualLineCount = newContent.split("\n").length
const LARGE_FILE_THRESHOLD = 7000

// If the file has many lines and no line_count was provided, it might be truncated
if (actualLineCount > LARGE_FILE_THRESHOLD && predictedLineCount === 0) {
cline.consecutiveMistakeCount++
cline.recordToolError("write_to_file")

await cline.say(
"error",
`The file content appears to be very large (${actualLineCount} lines). The assistant may have reached its output limit and the content could be truncated. Please verify the file is complete or consider using a different approach for large files.`,
)

pushToolResult(
formatResponse.toolError(
`Large file detected (${actualLineCount} lines). The content may be truncated due to output limits. Consider breaking the file into smaller chunks or using a different approach.`,
),
)
await cline.diffViewProvider.reset()
return
}

// pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock markers (deepseek/llama) or extra escape characters (gemini)
if (newContent.startsWith("```")) {
// cline handles cases where it includes language specifiers like ```python ```js
Expand Down Expand Up @@ -147,6 +170,42 @@ export async function writeToFileTool(
return
}

// Validate line count matches actual content
const actualLineCount = newContent.split("\n").length
const lineCountMismatchThreshold = 0.1 // 10% tolerance
const lineCountDifference = Math.abs(actualLineCount - predictedLineCount)
const lineCountDifferencePercent = lineCountDifference / predictedLineCount

// Check for significant mismatch that might indicate truncation
if (predictedLineCount > 100 && lineCountDifferencePercent > lineCountMismatchThreshold) {
cline.consecutiveMistakeCount++
cline.recordToolError("write_to_file")

await cline.say(
"error",
`Line count mismatch detected. Expected ${predictedLineCount} lines but got ${actualLineCount} lines. This may indicate the content was truncated.`,
)

// Check if this looks like truncation at output limit
const TYPICAL_OUTPUT_LIMIT_LINES = 7000
if (actualLineCount > TYPICAL_OUTPUT_LIMIT_LINES && actualLineCount < predictedLineCount) {
pushToolResult(
formatResponse.toolError(
`Content appears to be truncated. The file should have ${predictedLineCount} lines but only ${actualLineCount} lines were provided. This often happens when files exceed ~7000 lines due to output token limits. Consider using apply_diff for large file modifications or breaking the content into smaller chunks.`,
),
)
} else {
pushToolResult(
formatResponse.toolError(
`Line count mismatch: expected ${predictedLineCount} lines but got ${actualLineCount} lines. Please verify the content is complete.`,
),
)
}

await cline.diffViewProvider.revertChanges()
return
}

cline.consecutiveMistakeCount = 0

// if isEditingFile false, that means we have the full contents of the file already.
Expand Down